From 5b1aeb3cb3ce99fb5b0cf14ba08b5dff0889a2b7 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 3 Jun 2020 16:50:54 +0100 Subject: [PATCH 01/38] WIP --- .../app/browser/BrowserTabFragment.kt | 22 +++++- .../app/browser/BrowserTabViewModel.kt | 15 +++- .../duckduckgo/app/cta/model/DismissedCta.kt | 4 +- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 27 ++++++- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 25 +++++-- .../duckduckgo/app/di/DaggerWorkerFactory.kt | 12 ++++ .../duckduckgo/app/di/NotificationModule.kt | 17 ++++- .../com/duckduckgo/app/di/WorkerModule.kt | 3 + .../duckduckgo/app/global/view/DaxDialog.kt | 17 ++++- .../AndroidNotificationScheduler.kt | 24 ++++++- .../NotificationHandlerService.kt | 21 ++++++ .../app/notification/NotificationRegistrar.kt | 1 + .../model/FacebookNotification.kt | 71 +++++++++++++++++++ .../app/onboarding/store/UserStage.kt | 1 + .../app/onboarding/store/UserStageStore.kt | 10 +++ .../app/statistics/VariantManager.kt | 4 +- .../main/res/values/string-untranslated.xml | 7 ++ 17 files changed, 261 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 118ee8a02140..60688dd46a59 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -478,6 +478,16 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi viewModel.onUserSubmittedQuery(query) } + private fun navigateAndAddShortcut(shortCut: Command.AddHomeShortcut) { + hideKeyboard() + renderer.hideFindInPage() + viewModel.registerDaxBubbleCtaDismissed() + webView?.loadUrl(shortCut.url) +// context?.let { context -> +// addHomeShortcut(shortCut, context) +// } + } + private fun navigate(url: String) { hideKeyboard() renderer.hideFindInPage() @@ -584,6 +594,9 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi is Command.DaxCommand.HideDaxDialog -> showHideTipsDialog(it.cta) is Command.HideWebContent -> webView?.hide() is Command.ShowWebContent -> webView?.show() + is Command.NavigateAndAddShortCut -> { + navigateAndAddShortcut(it.shortcut) + } } } @@ -1224,6 +1237,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi viewModel.onUserClickCtaOkButton() } + override fun onDaxDialogSecondaryCtaClick() { + viewModel.onUserClickCtaSecondaryButton() + } + + private fun launchHideTipsDialog(context: Context, cta: Cta) { AlertDialog.Builder(context) .setTitle(R.string.hideTipsTitle) @@ -1773,14 +1791,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi when (configuration) { is HomePanelCta -> showHomeCta(configuration) is DaxBubbleCta -> showDaxCta(configuration) - is DaxDialogCta -> showDaxDialogCta(configuration) + is DialogCta -> showDaxDialogCta(configuration) is HomeTopPanelCta -> showHomeTopCta(configuration) } viewModel.onCtaShown() } - private fun showDaxDialogCta(configuration: DaxDialogCta) { + private fun showDaxDialogCta(configuration: DialogCta) { hideHomeCta() hideDaxCta() activity?.let { activity -> diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index bb83dee2ffa9..79c067732333 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -213,6 +213,7 @@ class BrowserTabViewModel( object LaunchTabSwitcher : Command() object HideWebContent : Command() object ShowWebContent : Command() + class NavigateAndAddShortCut(val shortcut: AddHomeShortcut) : Command() class ShowErrorWithAction(val action: () -> Unit) : Command() sealed class DaxCommand : Command() { @@ -1077,12 +1078,22 @@ class BrowserTabViewModel( is HomePanelCta.Survey -> LaunchSurvey(cta.survey) is HomePanelCta.AddWidgetAuto -> LaunchAddWidget is HomePanelCta.AddWidgetInstructions -> LaunchLegacyAddWidget + is DaxFacebookCta -> navigatetoUrlAndAddShorcut(url = "https://www.facebook.com", title = "Facebook") else -> return } } - fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) { - ctaViewModel.onUserClickCtaSecondaryButton(cta) + private fun navigatetoUrlAndAddShorcut(url: String, title: String): AddHomeShortcut { + val convertedUrl = queryUrlConverter.convertQueryToUrl(url) + onUserSubmittedQuery(convertedUrl) + return AddHomeShortcut(title, convertedUrl) + } + + fun onUserClickCtaSecondaryButton() { + viewModelScope.launch { + val cta = currentCtaViewState().cta ?: return@launch + ctaViewModel.onUserDismissedCta(cta) + } } fun onUserHideDaxDialog() { diff --git a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt index e5edb06269e7..f93c0879fe96 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt @@ -28,7 +28,9 @@ enum class CtaId { DAX_DIALOG_TRACKERS_FOUND, DAX_DIALOG_NETWORK, DAX_DIALOG_OTHER, - DAX_END + DAX_END, + FB_FLOW, + DELETE_FB } @Entity( diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 27a817ee4bb4..4ee955d7d77d 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -68,10 +68,31 @@ interface Cta { fun pixelOkParameters(): Map } -interface SecondaryButtonCta { - val secondaryButtonPixel: Pixel.PixelName? +class DaxFacebookCta( + @StringRes val text: Int = R.string.facebookDialogText, + @StringRes val okButton: Int = R.string.facebookDialogButtonText, + @StringRes val cancelButton: Int = R.string.facebookDialogCancelButtonText, + override val ctaId: CtaId = CtaId.FB_FLOW, + override val shownPixel: Pixel.PixelName? = null, + override val okPixel: Pixel.PixelName?= null, + override val cancelPixel: Pixel.PixelName? = null +) : Cta, DialogCta { + + override fun createCta(activity: FragmentActivity): DaxDialog = + TypewriterDaxDialog.newInstance( + daxText = activity.resources.getString(text), + primaryButtonText = activity.resources.getString(okButton), + secondaryButtonText = activity.resources.getString(cancelButton), + dismissible = false, + showHideButton = false + ) + + override fun pixelCancelParameters(): Map = emptyMap() + + override fun pixelOkParameters(): Map = emptyMap() + + override fun pixelShownParameters(): Map = emptyMap() - fun pixelSecondaryButtonParameters(): Map } sealed class DaxDialogCta( diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 061ae9d0f9ed..da50656c6d55 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -32,6 +32,7 @@ import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.store.daxOnboardingActive +import com.duckduckgo.app.onboarding.store.fbFlowActive import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.Variant @@ -111,6 +112,13 @@ class CtaViewModel @Inject constructor( } } + private suspend fun completeStageIfUserInFBFlowState() { + if (fbFlowIsActive()) { + Timber.d("Completing FB FLOW") + userStageStore.stageCompleted(AppStage.FLOW_FB) + } + } + suspend fun onUserDismissedCta(cta: Cta) { withContext(dispatchers.io()) { cta.cancelPixel?.let { @@ -124,6 +132,7 @@ class CtaViewModel @Inject constructor( dismissedCtaDao.insert(DismissedCta(cta.ctaId)) } + completeStageIfUserInFBFlowState() completeStageIfDaxOnboardingCompleted() } } @@ -134,12 +143,6 @@ class CtaViewModel @Inject constructor( } } - fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) { - cta.secondaryButtonPixel?.let { - pixel.fire(it, cta.pixelSecondaryButtonParameters()) - } - } - suspend fun refreshCta(dispatcher: CoroutineContext, isBrowserShowing: Boolean, site: Site? = null): Cta? { surveyCta()?.let { return it @@ -162,6 +165,9 @@ class CtaViewModel @Inject constructor( canShowDaxCtaEndOfJourney() -> { DaxBubbleCta.DaxEndCta(onboardingStore, appInstallStore) } + canShowFacebookDialog() -> { + DaxFacebookCta() + } canShowWidgetCta() -> { if (widgetCapabilities.supportsAutomaticWidgetAdd) AddWidgetAuto else AddWidgetInstructions } @@ -190,6 +196,9 @@ class CtaViewModel @Inject constructor( return null } + @WorkerThread + private suspend fun canShowFacebookDialog(): Boolean = fbFlowIsActive() && !fbDialogShown() + @WorkerThread private fun canShowWidgetCta(): Boolean { return widgetCapabilities.supportsStandardWidgetAdd && @@ -250,6 +259,8 @@ class CtaViewModel @Inject constructor( private fun variant(): Variant = variantManager.getVariant() + private fun fbDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.FB_FLOW) + private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) private fun daxDialogEndShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_END) @@ -264,6 +275,8 @@ class CtaViewModel @Inject constructor( private fun isSerpUrl(url: String): Boolean = url.contains(DaxDialogCta.SERP) + private suspend fun fbFlowIsActive(): Boolean = userStageStore.fbFlowActive() + private suspend fun daxOnboardingActive(): Boolean = userStageStore.daxOnboardingActive() private suspend fun allOnboardingCtasShown(): Boolean { diff --git a/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt b/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt index c3e78ee288d0..a9e227a6459f 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt @@ -26,10 +26,13 @@ import com.duckduckgo.app.global.job.AppConfigurationWorker import com.duckduckgo.app.global.view.ClearDataAction import com.duckduckgo.app.job.ConfigurationDownloader import com.duckduckgo.app.notification.NotificationFactory +import com.duckduckgo.app.notification.NotificationScheduler import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker +import com.duckduckgo.app.notification.NotificationScheduler.FacebookNotificationWorker import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.ClearDataNotification +import com.duckduckgo.app.notification.model.FacebookNotification import com.duckduckgo.app.notification.model.PrivacyProtectionNotification import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.api.OfflinePixelScheduler @@ -46,6 +49,7 @@ class DaggerWorkerFactory( private val notificationFactory: NotificationFactory, private val clearDataNotification: ClearDataNotification, private val privacyProtectionNotification: PrivacyProtectionNotification, + private val facebookNotification: FacebookNotification, private val configurationDownloader: ConfigurationDownloader, private val pixel: Pixel ) : WorkerFactory() { @@ -63,6 +67,7 @@ class DaggerWorkerFactory( is ClearDataNotificationWorker -> injectClearDataNotificationWorker(instance) is PrivacyNotificationWorker -> injectPrivacyNotificationWorker(instance) is AppConfigurationWorker -> injectAppConfigurationWorker(instance) + is FacebookNotificationWorker -> injectFacebookNotificationWorker(instance) else -> Timber.i("No injection required for worker $workerClassName") } @@ -103,4 +108,11 @@ class DaggerWorkerFactory( worker.notification = privacyProtectionNotification } + private fun injectFacebookNotificationWorker(worker: FacebookNotificationWorker) { + worker.manager = notificationManager + worker.notificationDao = notificationDao + worker.factory = notificationFactory + worker.pixel = pixel + worker.notification = facebookNotification + } } diff --git a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt index 11ad9ceffc82..94334d00956f 100644 --- a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt @@ -26,7 +26,9 @@ import com.duckduckgo.app.notification.NotificationFactory import com.duckduckgo.app.notification.NotificationScheduler import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.ClearDataNotification +import com.duckduckgo.app.notification.model.FacebookNotification import com.duckduckgo.app.notification.model.PrivacyProtectionNotification +import com.duckduckgo.app.onboarding.store.AppUserStageStore import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao import com.duckduckgo.app.settings.db.SettingsDataStore import dagger.Module @@ -54,6 +56,15 @@ class NotificationModule { return LocalBroadcastManager.getInstance(context) } + @Provides + fun provideFacebookNotification( + context: Context, + notificationDao: NotificationDao, + userStageStore: AppUserStageStore + ): FacebookNotification { + return FacebookNotification(context, notificationDao, userStageStore) + } + @Provides fun provideClearDataNotification( context: Context, @@ -77,12 +88,14 @@ class NotificationModule { fun providesNotificationScheduler( workManager: WorkManager, clearDataNotification: ClearDataNotification, - privacyProtectionNotification: PrivacyProtectionNotification + privacyProtectionNotification: PrivacyProtectionNotification, + facebookNotification: FacebookNotification ): AndroidNotificationScheduler { return NotificationScheduler( workManager, clearDataNotification, - privacyProtectionNotification + privacyProtectionNotification, + facebookNotification ) } diff --git a/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt b/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt index e74bdb113335..5bc714784ddb 100644 --- a/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt @@ -26,6 +26,7 @@ import com.duckduckgo.app.job.ConfigurationDownloader import com.duckduckgo.app.notification.NotificationFactory import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.ClearDataNotification +import com.duckduckgo.app.notification.model.FacebookNotification import com.duckduckgo.app.notification.model.PrivacyProtectionNotification import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.api.OfflinePixelSender @@ -58,6 +59,7 @@ class WorkerModule { notificationFactory: NotificationFactory, clearDataNotification: ClearDataNotification, privacyProtectionNotification: PrivacyProtectionNotification, + facebookNotification: FacebookNotification, configurationDownloader: ConfigurationDownloader, pixel: Pixel ): WorkerFactory { @@ -70,6 +72,7 @@ class WorkerModule { notificationFactory, clearDataNotification, privacyProtectionNotification, + facebookNotification, configurationDownloader, pixel ) diff --git a/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt b/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt index d35d9e276bb7..899dfddca6e3 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt @@ -41,6 +41,7 @@ interface DaxDialog { interface DaxDialogListener { fun onDaxDialogDismiss() fun onDaxDialogPrimaryCtaClick() + fun onDaxDialogSecondaryCtaClick() fun onDaxDialogHideClick() } @@ -52,6 +53,7 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { private var toolbarDimmed: Boolean = true private var dismissible: Boolean = true private var typingDelayInMs: Long = DEFAULT_TYPING_DELAY + private var showHideButton: Boolean = true private var daxDialogListener: DaxDialogListener? = null @@ -94,6 +96,9 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { if (containsKey(ARG_TYPING_DELAY)) { typingDelayInMs = getLong(ARG_TYPING_DELAY) } + if (containsKey(ARG_SHOW_HIDE_BUTTON)) { + showHideButton = getBoolean(ARG_SHOW_HIDE_BUTTON) + } } } @@ -150,6 +155,12 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { dismiss() } + secondaryCta.setOnClickListener { + dialogText.cancelAnimation() + daxDialogListener?.onDaxDialogSecondaryCtaClick() + dismiss() + } + if (dismissible) { dialogContainer.setOnClickListener { dialogText.cancelAnimation() @@ -172,6 +183,7 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { secondaryCta.text = secondaryButtonText secondaryCta.visibility = if (secondaryButtonText.isEmpty()) View.GONE else View.VISIBLE dialogText.typingDelayInMs = typingDelayInMs + hideText.visibility = if (showHideButton) View.VISIBLE else View.GONE } } @@ -183,7 +195,8 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { secondaryButtonText: String? = "", toolbarDimmed: Boolean = true, dismissible: Boolean = true, - typingDelayInMs: Long = DEFAULT_TYPING_DELAY + typingDelayInMs: Long = DEFAULT_TYPING_DELAY, + showHideButton: Boolean = true ): TypewriterDaxDialog { return TypewriterDaxDialog().apply { arguments = Bundle().apply { @@ -193,6 +206,7 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { putBoolean(ARG_TOOLBAR_DIMMED, toolbarDimmed) putBoolean(ARG_DISMISSIBLE, dismissible) putLong(ARG_TYPING_DELAY, typingDelayInMs) + putBoolean(ARG_SHOW_HIDE_BUTTON, showHideButton) } } } @@ -204,5 +218,6 @@ class TypewriterDaxDialog : DialogFragment(), DaxDialog { private const val ARG_TOOLBAR_DIMMED = "toolbarDimmed" private const val ARG_DISMISSIBLE = "isDismissible" private const val ARG_TYPING_DELAY = "typingDelay" + private const val ARG_SHOW_HIDE_BUTTON = "showHideButton" } } diff --git a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt index 2cbc0651f6b1..08ea34f5332b 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt @@ -38,11 +38,21 @@ interface AndroidNotificationScheduler { class NotificationScheduler( private val workManager: WorkManager, private val clearDataNotification: SchedulableNotification, - private val privacyNotification: SchedulableNotification + private val privacyNotification: SchedulableNotification, + private val facebookNotification: SchedulableNotification ) : AndroidNotificationScheduler { override suspend fun scheduleNextNotification() { scheduleInactiveUserNotifications() + scheduleFacebookNotification() + } + + private suspend fun scheduleFacebookNotification() { + when { + facebookNotification.canShow() -> { + scheduleUniqueNotification(OneTimeWorkRequestBuilder(), 15, TimeUnit.SECONDS, FACEBOOK_WORK_REQUEST_TAG) + } + } } private suspend fun scheduleInactiveUserNotifications() { @@ -59,6 +69,16 @@ class NotificationScheduler( } } + private fun scheduleUniqueNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String) { + Timber.v("MARCOS Scheduling unique notification") + val request = builder + .addTag(tag) + .setInitialDelay(duration, unit) + .build() + + workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request) + } + private fun scheduleNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String) { Timber.v("Scheduling notification") val request = builder @@ -75,6 +95,7 @@ class NotificationScheduler( open class ClearDataNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) class PrivacyNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) + class FacebookNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) open class SchedulableNotificationWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -105,5 +126,6 @@ class NotificationScheduler( companion object { const val UNUSED_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.schedule" + const val FACEBOOK_WORK_REQUEST_TAG = "com.duckduckgo.notification.facebook" } } diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt index ece8967ffea2..80c04c7d5223 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt @@ -21,19 +21,27 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent +import android.os.Bundle import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat import com.duckduckgo.app.browser.BrowserActivity +import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.APP_LAUNCH import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CLEAR_DATA_LAUNCH +import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.START_FB_FLOW import com.duckduckgo.app.notification.model.NotificationSpec +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.settings.SettingsActivity import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.NOTIFICATION_CANCELLED import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.NOTIFICATION_LAUNCHED import dagger.android.AndroidInjection +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -54,6 +62,12 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { @Inject lateinit var settingsDataStore: SettingsDataStore + @Inject + lateinit var userStageStore: UserStageStore + + @Inject + lateinit var dispatcher: DispatcherProvider + override fun onCreate() { super.onCreate() AndroidInjection.inject(this) @@ -66,6 +80,12 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { APP_LAUNCH -> onAppLaunched(pixelSuffix) CLEAR_DATA_LAUNCH -> onClearDataLaunched(pixelSuffix) CANCEL -> onCancelled(pixelSuffix) + START_FB_FLOW -> { + GlobalScope.launch(dispatcher.io()) { + userStageStore.registerInStage(AppStage.FLOW_FB) + } + onAppLaunched(pixelSuffix) + } } if (intent.getBooleanExtra(NOTIFICATION_AUTO_CANCEL, true)) { @@ -109,6 +129,7 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { const val APP_LAUNCH = "com.duckduckgo.notification.launch.app" const val CLEAR_DATA_LAUNCH = "com.duckduckgo.notification.launch.clearData" const val CANCEL = "com.duckduckgo.notification.cancel" + const val START_FB_FLOW = "com.duckduckgo.notification.flow.fb" } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt index cb9e238d34c3..88fb400ce9ee 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt @@ -48,6 +48,7 @@ class NotificationRegistrar @Inject constructor( object NotificationId { const val ClearData = 100 const val PrivacyProtection = 101 + const val Facebook = 105 } object ChannelType { diff --git a/app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt b/app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt new file mode 100644 index 000000000000..b488c35cfa9a --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt @@ -0,0 +1,71 @@ +/* + * 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.notification.model + +import android.content.Context +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.notification.NotificationHandlerService +import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.APP_LAUNCH +import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL +import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CLEAR_DATA_LAUNCH +import com.duckduckgo.app.notification.NotificationRegistrar +import com.duckduckgo.app.notification.db.NotificationDao +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore +import timber.log.Timber + +class FacebookNotification( + private val context: Context, + private val notificationDao: NotificationDao, + private val userStageStore: UserStageStore +) : SchedulableNotification { + + override val id = "com.duckduckgo.privacytips.fb" + override val launchIntent = NotificationHandlerService.NotificationEvent.START_FB_FLOW + override val cancelIntent = CANCEL + + override suspend fun canShow(): Boolean { + if (notificationDao.exists(id)) { + Timber.v("MARCOS Notification already seen") + return false + } + + if (userStageStore.getUserAppStage() != AppStage.ESTABLISHED) { + Timber.v("MARCOS User not in established state") + return false + } + + return true + } + + override suspend fun buildSpecification(): NotificationSpec { + return FacebookSpecification(context) + } +} + +class FacebookSpecification(context: Context) : NotificationSpec { + override val channel = NotificationRegistrar.ChannelType.TUTORIALS + override val systemId = NotificationRegistrar.NotificationId.Facebook + override val name = "Update auto clear data" + override val icon = R.drawable.notification_logo + override val title: String = "Worried about Facebook tracking you?" + override val description: String = "Here's a simply way to reduce it's reach." + override val launchButton: String? = null + override val closeButton: String? = null + override val pixelSuffix = "fb" + override val autoCancel = true +} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt index c46f825cf44d..3a0b90f67969 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt @@ -31,6 +31,7 @@ data class UserStage( enum class AppStage { NEW, DAX_ONBOARDING, + FLOW_FB, ESTABLISHED; } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 047442293f81..014fe77e337e 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -23,6 +23,7 @@ import javax.inject.Inject interface UserStageStore { suspend fun getUserAppStage(): AppStage suspend fun stageCompleted(appStage: AppStage): AppStage + suspend fun registerInStage(appStage: AppStage) } class AppUserStageStore @Inject constructor( @@ -41,6 +42,7 @@ class AppUserStageStore @Inject constructor( val newAppStage = when (appStage) { AppStage.NEW -> AppStage.DAX_ONBOARDING AppStage.DAX_ONBOARDING -> AppStage.ESTABLISHED + AppStage.FLOW_FB -> AppStage.ESTABLISHED AppStage.ESTABLISHED -> AppStage.ESTABLISHED } @@ -51,6 +53,10 @@ class AppUserStageStore @Inject constructor( return@withContext newAppStage } } + + override suspend fun registerInStage(appStage: AppStage) { + userStageDao.updateUserStage(appStage) + } } suspend fun UserStageStore.isNewUser(): Boolean { @@ -60,3 +66,7 @@ suspend fun UserStageStore.isNewUser(): Boolean { suspend fun UserStageStore.daxOnboardingActive(): Boolean { return this.getUserAppStage() == AppStage.DAX_ONBOARDING } + +suspend fun UserStageStore.fbFlowActive(): Boolean { + return this.getUserAppStage() == AppStage.FLOW_FB +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt index 427123d7cf3a..e182c53fd6b5 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt @@ -48,12 +48,12 @@ interface VariantManager { // Bottom Bar Navigation Experiment Variant( key = "mb", - weight = 1.0, + weight = 0.0, features = emptyList(), filterBy = { noFilter() }), Variant( key = "mk", - weight = 1.0, + weight = 0.0, features = listOf(BottomBarNavigation), filterBy = { noFilter() }) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index be7ff1f014c4..3ad2f869a851 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -19,4 +19,11 @@ Download Manager is disabled Download Manager not available on this device Enable + + + Did you know the Facebook app can make requests for data even when you\'re not using?<br/><br/>Replace the app with a home screen shortcut that opens Facebook in DuckDuckGo. Then delete the Facebook app. + Add Facebook Shortcut + Not Now + + From baa850e7738fcdb2cdbc8f140c0acd3efdec47b1 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 10 Jun 2020 11:35:50 +0100 Subject: [PATCH 02/38] Add db migrations and new icon for shortcut --- .../22.json | 746 ++++++++++++++++++ .../app/browser/BrowserTabViewModel.kt | 6 +- .../app/browser/favicon/FaviconDownloader.kt | 6 + .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 18 +- .../duckduckgo/app/di/DaggerWorkerFactory.kt | 13 +- .../duckduckgo/app/di/NotificationModule.kt | 17 +- .../com/duckduckgo/app/di/WorkerModule.kt | 6 +- .../duckduckgo/app/global/db/AppDatabase.kt | 22 +- .../global/timestamps/db/KeyTimestampDao.kt | 32 + .../timestamps/db/KeyTimestampEntity.kt | 46 ++ .../global/timestamps/db/KeyTimestampStore.kt | 44 ++ .../AndroidNotificationScheduler.kt | 31 +- .../NotificationHandlerService.kt | 10 +- ...tification.kt => UseOurAppNotification.kt} | 26 +- .../app/onboarding/store/UserStage.kt | 3 +- .../app/onboarding/store/UserStageStore.kt | 11 +- app/src/main/res/drawable/ic_fb_favicon.png | Bin 0 -> 1779 bytes 17 files changed, 965 insertions(+), 72 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json create mode 100644 app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt create mode 100644 app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt rename app/src/main/java/com/duckduckgo/app/notification/model/{FacebookNotification.kt => UseOurAppNotification.kt} (68%) create mode 100644 app/src/main/res/drawable/ic_fb_favicon.png diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json new file mode 100644 index 000000000000..54d51b064261 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json @@ -0,0 +1,746 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "ec737cddc966adeb518f31a718199ff1", + "entities": [ + { + "tableName": "tds_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `defaultAction` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `categories` TEXT NOT NULL, `rules` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultAction", + "columnName": "defaultAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `displayName` TEXT NOT NULL, `prevalence` REAL NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevalence", + "columnName": "prevalence", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_domain_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `entityName` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityName", + "columnName": "entityName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporary_tracking_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_bloom_filter_spec", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `errorRate` REAL NOT NULL, `totalEntries` INTEGER NOT NULL, `sha256` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorRate", + "columnName": "errorRate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalEntries", + "columnName": "totalEntries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_whitelisted_domain", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`networkName`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sites_visited", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tabId` TEXT NOT NULL, `url` TEXT, `title` TEXT, `skipHome` INTEGER NOT NULL, `viewed` INTEGER NOT NULL, `position` INTEGER NOT NULL, `tabPreviewFile` TEXT, PRIMARY KEY(`tabId`))", + "fields": [ + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "skipHome", + "columnName": "skipHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewed", + "columnName": "viewed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabPreviewFile", + "columnName": "tabPreviewFile", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tab_selection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tabId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`tabId`) REFERENCES `tabs`(`tabId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tab_selection_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "tabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`surveyId` TEXT NOT NULL, `url` TEXT, `daysInstalled` INTEGER, `status` TEXT NOT NULL, PRIMARY KEY(`surveyId`))", + "fields": [ + { + "fieldPath": "surveyId", + "columnName": "surveyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "daysInstalled", + "columnName": "daysInstalled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "surveyId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dismissed_cta", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ctaId` TEXT NOT NULL, PRIMARY KEY(`ctaId`))", + "fields": [ + { + "fieldPath": "ctaId", + "columnName": "ctaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "ctaId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_days_used", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, PRIMARY KEY(`date`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_enjoyment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventType` INTEGER NOT NULL, `promptCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `primaryKey` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "promptCount", + "columnName": "promptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryKey", + "columnName": "primaryKey", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "primaryKey" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notificationId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "privacy_protection_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `blocked_tracker_count` INTEGER NOT NULL, `upgrade_count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedTrackerCount", + "columnName": "blocked_tracker_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upgradeCount", + "columnName": "upgrade_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UncaughtExceptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `version` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionSource", + "columnName": "exceptionSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tdsMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `eTag` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userStage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `appStage` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appStage", + "columnName": "appStage", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "fireproofWebsites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyTimestamps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ec737cddc966adeb518f31a718199ff1')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 79c067732333..1e3e2bb9133d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1078,15 +1078,15 @@ class BrowserTabViewModel( is HomePanelCta.Survey -> LaunchSurvey(cta.survey) is HomePanelCta.AddWidgetAuto -> LaunchAddWidget is HomePanelCta.AddWidgetInstructions -> LaunchLegacyAddWidget - is DaxFacebookCta -> navigatetoUrlAndAddShorcut(url = "https://www.facebook.com", title = "Facebook") + is DaxFacebookCta -> navigateToUrlAndAddShorcut(url = "https://www.facebook.com", title = "Facebook", icon = R.drawable.ic_fb_favicon) else -> return } } - private fun navigatetoUrlAndAddShorcut(url: String, title: String): AddHomeShortcut { + private fun navigateToUrlAndAddShorcut(url: String, title: String, icon: Int): AddHomeShortcut { val convertedUrl = queryUrlConverter.convertQueryToUrl(url) onUserSubmittedQuery(convertedUrl) - return AddHomeShortcut(title, convertedUrl) + return AddHomeShortcut(title, convertedUrl, faviconDownloader.load(icon)) } fun onUserClickCtaSecondaryButton() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt index eecdec0c9c77..dac5f3f51ab5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt @@ -19,6 +19,9 @@ package com.duckduckgo.app.browser.favicon import android.content.Context import android.graphics.Bitmap import android.net.Uri +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap import com.bumptech.glide.Glide import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.view.toPx @@ -29,6 +32,7 @@ import javax.inject.Inject interface FaviconDownloader { fun download(currentPageUrl: Uri): Single + fun load(@DrawableRes drawableRes: Int): Bitmap? } class GlideFaviconDownloader @Inject constructor(private val context: Context) : FaviconDownloader { @@ -48,6 +52,8 @@ class GlideFaviconDownloader @Inject constructor(private val context: Context) : } } + override fun load(drawableRes: Int): Bitmap? = ContextCompat.getDrawable(context, drawableRes)?.toBitmap() + companion object { private const val DESIRED_IMAGE_SIZE_DP = 24 private const val TIMEOUT_PERIOD_SECONDS: Long = 3 diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index da50656c6d55..08094062c685 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -32,7 +32,7 @@ import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.store.daxOnboardingActive -import com.duckduckgo.app.onboarding.store.fbFlowActive +import com.duckduckgo.app.onboarding.store.useOurAppOnboarding import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.Variant @@ -112,10 +112,10 @@ class CtaViewModel @Inject constructor( } } - private suspend fun completeStageIfUserInFBFlowState() { - if (fbFlowIsActive()) { - Timber.d("Completing FB FLOW") - userStageStore.stageCompleted(AppStage.FLOW_FB) + private suspend fun completeStageIfUserInUseOurAppCompleted() { + if (useOurAppActive()) { + Timber.d("Completing USE OUR APP ONBOARDING") + userStageStore.stageCompleted(AppStage.USE_OUR_APP_ONBOARDING) } } @@ -132,7 +132,7 @@ class CtaViewModel @Inject constructor( dismissedCtaDao.insert(DismissedCta(cta.ctaId)) } - completeStageIfUserInFBFlowState() + completeStageIfUserInUseOurAppCompleted() completeStageIfDaxOnboardingCompleted() } } @@ -197,7 +197,7 @@ class CtaViewModel @Inject constructor( } @WorkerThread - private suspend fun canShowFacebookDialog(): Boolean = fbFlowIsActive() && !fbDialogShown() + private suspend fun canShowFacebookDialog(): Boolean = useOurAppActive() && !fbDialogShown() @WorkerThread private fun canShowWidgetCta(): Boolean { @@ -257,8 +257,6 @@ class CtaViewModel @Inject constructor( } } - private fun variant(): Variant = variantManager.getVariant() - private fun fbDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.FB_FLOW) private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) @@ -275,7 +273,7 @@ class CtaViewModel @Inject constructor( private fun isSerpUrl(url: String): Boolean = url.contains(DaxDialogCta.SERP) - private suspend fun fbFlowIsActive(): Boolean = userStageStore.fbFlowActive() + private suspend fun useOurAppActive(): Boolean = userStageStore.useOurAppOnboarding() private suspend fun daxOnboardingActive(): Boolean = userStageStore.daxOnboardingActive() diff --git a/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt b/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt index a9e227a6459f..26d25cf54b24 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt @@ -26,13 +26,12 @@ import com.duckduckgo.app.global.job.AppConfigurationWorker import com.duckduckgo.app.global.view.ClearDataAction import com.duckduckgo.app.job.ConfigurationDownloader import com.duckduckgo.app.notification.NotificationFactory -import com.duckduckgo.app.notification.NotificationScheduler import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker -import com.duckduckgo.app.notification.NotificationScheduler.FacebookNotificationWorker +import com.duckduckgo.app.notification.NotificationScheduler.UseOurAppNotificationWorker import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.ClearDataNotification -import com.duckduckgo.app.notification.model.FacebookNotification +import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.notification.model.PrivacyProtectionNotification import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.api.OfflinePixelScheduler @@ -49,7 +48,7 @@ class DaggerWorkerFactory( private val notificationFactory: NotificationFactory, private val clearDataNotification: ClearDataNotification, private val privacyProtectionNotification: PrivacyProtectionNotification, - private val facebookNotification: FacebookNotification, + private val useOurAppNotification: UseOurAppNotification, private val configurationDownloader: ConfigurationDownloader, private val pixel: Pixel ) : WorkerFactory() { @@ -67,7 +66,7 @@ class DaggerWorkerFactory( is ClearDataNotificationWorker -> injectClearDataNotificationWorker(instance) is PrivacyNotificationWorker -> injectPrivacyNotificationWorker(instance) is AppConfigurationWorker -> injectAppConfigurationWorker(instance) - is FacebookNotificationWorker -> injectFacebookNotificationWorker(instance) + is UseOurAppNotificationWorker -> injectUseOurAppNotificationWorker(instance) else -> Timber.i("No injection required for worker $workerClassName") } @@ -108,11 +107,11 @@ class DaggerWorkerFactory( worker.notification = privacyProtectionNotification } - private fun injectFacebookNotificationWorker(worker: FacebookNotificationWorker) { + private fun injectUseOurAppNotificationWorker(worker: UseOurAppNotificationWorker) { worker.manager = notificationManager worker.notificationDao = notificationDao worker.factory = notificationFactory worker.pixel = pixel - worker.notification = facebookNotification + worker.notification = useOurAppNotification } } diff --git a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt index 94334d00956f..3f4d03c2d11b 100644 --- a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt @@ -26,9 +26,9 @@ import com.duckduckgo.app.notification.NotificationFactory import com.duckduckgo.app.notification.NotificationScheduler import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.ClearDataNotification -import com.duckduckgo.app.notification.model.FacebookNotification +import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.notification.model.PrivacyProtectionNotification -import com.duckduckgo.app.onboarding.store.AppUserStageStore +import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao import com.duckduckgo.app.settings.db.SettingsDataStore import dagger.Module @@ -59,10 +59,9 @@ class NotificationModule { @Provides fun provideFacebookNotification( context: Context, - notificationDao: NotificationDao, - userStageStore: AppUserStageStore - ): FacebookNotification { - return FacebookNotification(context, notificationDao, userStageStore) + notificationDao: NotificationDao + ): UseOurAppNotification { + return UseOurAppNotification(context, notificationDao) } @Provides @@ -89,13 +88,15 @@ class NotificationModule { workManager: WorkManager, clearDataNotification: ClearDataNotification, privacyProtectionNotification: PrivacyProtectionNotification, - facebookNotification: FacebookNotification + useOurAppNotification: UseOurAppNotification, + stageStore: UserStageStore ): AndroidNotificationScheduler { return NotificationScheduler( workManager, clearDataNotification, privacyProtectionNotification, - facebookNotification + useOurAppNotification, + stageStore ) } diff --git a/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt b/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt index 5bc714784ddb..3bc7bd37a8d5 100644 --- a/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt @@ -26,7 +26,7 @@ import com.duckduckgo.app.job.ConfigurationDownloader import com.duckduckgo.app.notification.NotificationFactory import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.ClearDataNotification -import com.duckduckgo.app.notification.model.FacebookNotification +import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.notification.model.PrivacyProtectionNotification import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.api.OfflinePixelSender @@ -59,7 +59,7 @@ class WorkerModule { notificationFactory: NotificationFactory, clearDataNotification: ClearDataNotification, privacyProtectionNotification: PrivacyProtectionNotification, - facebookNotification: FacebookNotification, + useOurAppNotification: UseOurAppNotification, configurationDownloader: ConfigurationDownloader, pixel: Pixel ): WorkerFactory { @@ -72,7 +72,7 @@ class WorkerModule { notificationFactory, clearDataNotification, privacyProtectionNotification, - facebookNotification, + useOurAppNotification, configurationDownloader, pixel ) diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index cf634d945d9a..7be10e0555bc 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -35,6 +35,9 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.exception.UncaughtExceptionDao import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSourceConverter +import com.duckduckgo.app.global.timestamps.db.KeyTimestampDao +import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity +import com.duckduckgo.app.global.timestamps.db.TimestampKeyTypeConverter import com.duckduckgo.app.httpsupgrade.db.HttpsBloomFilterSpecDao import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao import com.duckduckgo.app.httpsupgrade.model.HttpsBloomFilterSpec @@ -58,7 +61,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 21, entities = [ + exportSchema = true, version = 22, entities = [ TdsTracker::class, TdsEntity::class, TdsDomainEntity::class, @@ -81,7 +84,8 @@ import com.duckduckgo.app.usage.search.SearchCountEntity UncaughtExceptionEntity::class, TdsMetadata::class, UserStage::class, - FireproofWebsiteEntity::class + FireproofWebsiteEntity::class, + KeyTimestampEntity::class ] ) @@ -94,7 +98,8 @@ import com.duckduckgo.app.usage.search.SearchCountEntity RuleTypeConverter::class, CategoriesTypeConverter::class, UncaughtExceptionSourceConverter::class, - StageTypeConverter::class + StageTypeConverter::class, + TimestampKeyTypeConverter::class ) abstract class AppDatabase : RoomDatabase() { @@ -119,6 +124,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun tdsDao(): TdsMetadataDao abstract fun userStageDao(): UserStageDao abstract fun fireproofWebsiteDao(): FireproofWebsiteDao + abstract fun keyTimestampDao(): KeyTimestampDao } @Suppress("PropertyName") @@ -292,6 +298,13 @@ class MigrationsProvider(val context: Context) { } } + val MIGRATION_21_TO_22: Migration = object : Migration(21, 22) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `keyTimestamps` (`id` TEXT NOT NULL PRIMARY KEY, `timestamp` INTEGER NOT NULL)") + database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") + } + } + val ALL_MIGRATIONS: List get() = listOf( MIGRATION_1_TO_2, @@ -313,7 +326,8 @@ class MigrationsProvider(val context: Context) { MIGRATION_17_TO_18, MIGRATION_18_TO_19, MIGRATION_19_TO_20, - MIGRATION_20_TO_21 + MIGRATION_20_TO_21, + MIGRATION_21_TO_22 ) @Deprecated( diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt new file mode 100644 index 000000000000..12a1bd6a87ca --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt @@ -0,0 +1,32 @@ +/* + * 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.global.timestamps.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface KeyTimestampDao { + + @Query("select * from $KEY_TIMESTAMPS_TABLE_NAME where id=:timestampKey") + suspend fun getTimestamp(timestampKey: TimestampKey): KeyTimestampEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(keyTimestampEntity: KeyTimestampEntity) +} diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt new file mode 100644 index 000000000000..5fa1430b58c9 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt @@ -0,0 +1,46 @@ +/* + * 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.global.timestamps.db + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverter + +const val KEY_TIMESTAMPS_TABLE_NAME = "keyTimestamps" + +@Entity(tableName = KEY_TIMESTAMPS_TABLE_NAME) +data class KeyTimestampEntity( + @PrimaryKey val id: TimestampKey, + val timestamp: Int +) + +enum class TimestampKey { + USE_OUR_APP_SHORTCUT_ADDED +} + +class TimestampKeyTypeConverter { + + @TypeConverter + fun toKey(stage: String): TimestampKey { + return TimestampKey.valueOf(stage) + } + + @TypeConverter + fun fromKey(stage: TimestampKey): String { + return stage.name + } +} diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt new file mode 100644 index 000000000000..9add766b3b4e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt @@ -0,0 +1,44 @@ +/* + * 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.global.timestamps.db + +import com.duckduckgo.app.global.DispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface KeyTimestampStore { + suspend fun getTimestamp(timestampKey: TimestampKey): KeyTimestampEntity? + suspend fun registerTimestamp(timestampEntity: KeyTimestampEntity) +} + +class AppKeyTimestampStore @Inject constructor( + private val keyTimestampDao: KeyTimestampDao, + private val dispatcher: DispatcherProvider +) : KeyTimestampStore { + + override suspend fun getTimestamp(timestampKey: TimestampKey): KeyTimestampEntity? { + return withContext(dispatcher.io()) { + keyTimestampDao.getTimestamp(timestampKey) + } + } + + override suspend fun registerTimestamp(timestampEntity: KeyTimestampEntity) { + withContext(dispatcher.io()) { + keyTimestampDao.insert(timestampEntity) + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt index 08ea34f5332b..ba4766f21226 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt @@ -23,6 +23,9 @@ import androidx.work.* import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.Notification import com.duckduckgo.app.notification.model.SchedulableNotification +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.onboarding.store.useOurAppNotification import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.NOTIFICATION_SHOWN import timber.log.Timber @@ -39,7 +42,8 @@ class NotificationScheduler( private val workManager: WorkManager, private val clearDataNotification: SchedulableNotification, private val privacyNotification: SchedulableNotification, - private val facebookNotification: SchedulableNotification + private val useOurAppNotification: SchedulableNotification, + private val userStageStore: UserStageStore ) : AndroidNotificationScheduler { override suspend fun scheduleNextNotification() { @@ -48,9 +52,18 @@ class NotificationScheduler( } private suspend fun scheduleFacebookNotification() { - when { - facebookNotification.canShow() -> { - scheduleUniqueNotification(OneTimeWorkRequestBuilder(), 15, TimeUnit.SECONDS, FACEBOOK_WORK_REQUEST_TAG) + if (userStageStore.useOurAppNotification()) { + val operation = scheduleUniqueNotification( + OneTimeWorkRequestBuilder(), + 15, + TimeUnit.SECONDS, + USE_OUR_APP_WORK_REQUEST_TAG + ) + try { + operation.await() + userStageStore.stageCompleted(AppStage.USE_OUR_APP_NOTIFICATION) + } catch (e: Exception) { + Timber.v("Notification could not be scheduled: $e") } } } @@ -69,14 +82,14 @@ class NotificationScheduler( } } - private fun scheduleUniqueNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String) { - Timber.v("MARCOS Scheduling unique notification") + private fun scheduleUniqueNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String): Operation { + Timber.v("Scheduling unique notification") val request = builder .addTag(tag) .setInitialDelay(duration, unit) .build() - workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request) + return workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request) } private fun scheduleNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String) { @@ -95,7 +108,7 @@ class NotificationScheduler( open class ClearDataNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) class PrivacyNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) - class FacebookNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) + class UseOurAppNotificationWorker(context: Context, params: WorkerParameters) : SchedulableNotificationWorker(context, params) open class SchedulableNotificationWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -126,6 +139,6 @@ class NotificationScheduler( companion object { const val UNUSED_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.schedule" - const val FACEBOOK_WORK_REQUEST_TAG = "com.duckduckgo.notification.facebook" + const val USE_OUR_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.useOurApp" } } diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt index 80c04c7d5223..192f74db4f75 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt @@ -21,7 +21,6 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent -import android.os.Bundle import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat import com.duckduckgo.app.browser.BrowserActivity @@ -29,7 +28,7 @@ import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.APP_LAUNCH import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CLEAR_DATA_LAUNCH -import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.START_FB_FLOW +import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.USE_OUR_APP import com.duckduckgo.app.notification.model.NotificationSpec import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore @@ -40,7 +39,6 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.NOTIFICATION_CANCELL import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.NOTIFICATION_LAUNCHED import dagger.android.AndroidInjection import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.MainCoroutineDispatcher import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -80,9 +78,9 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { APP_LAUNCH -> onAppLaunched(pixelSuffix) CLEAR_DATA_LAUNCH -> onClearDataLaunched(pixelSuffix) CANCEL -> onCancelled(pixelSuffix) - START_FB_FLOW -> { + USE_OUR_APP -> { GlobalScope.launch(dispatcher.io()) { - userStageStore.registerInStage(AppStage.FLOW_FB) + userStageStore.registerInStage(AppStage.USE_OUR_APP_ONBOARDING) } onAppLaunched(pixelSuffix) } @@ -129,7 +127,7 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { const val APP_LAUNCH = "com.duckduckgo.notification.launch.app" const val CLEAR_DATA_LAUNCH = "com.duckduckgo.notification.launch.clearData" const val CANCEL = "com.duckduckgo.notification.cancel" - const val START_FB_FLOW = "com.duckduckgo.notification.flow.fb" + const val USE_OUR_APP = "com.duckduckgo.notification.flow.useOurApp" } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt similarity index 68% rename from app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt rename to app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt index b488c35cfa9a..c0daa5d3f053 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/model/FacebookNotification.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt @@ -19,33 +19,23 @@ package com.duckduckgo.app.notification.model import android.content.Context import com.duckduckgo.app.browser.R import com.duckduckgo.app.notification.NotificationHandlerService -import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.APP_LAUNCH import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL -import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CLEAR_DATA_LAUNCH import com.duckduckgo.app.notification.NotificationRegistrar import com.duckduckgo.app.notification.db.NotificationDao -import com.duckduckgo.app.onboarding.store.AppStage -import com.duckduckgo.app.onboarding.store.UserStageStore import timber.log.Timber -class FacebookNotification( +class UseOurAppNotification( private val context: Context, - private val notificationDao: NotificationDao, - private val userStageStore: UserStageStore + private val notificationDao: NotificationDao ) : SchedulableNotification { override val id = "com.duckduckgo.privacytips.fb" - override val launchIntent = NotificationHandlerService.NotificationEvent.START_FB_FLOW + override val launchIntent = NotificationHandlerService.NotificationEvent.USE_OUR_APP override val cancelIntent = CANCEL override suspend fun canShow(): Boolean { if (notificationDao.exists(id)) { - Timber.v("MARCOS Notification already seen") - return false - } - - if (userStageStore.getUserAppStage() != AppStage.ESTABLISHED) { - Timber.v("MARCOS User not in established state") + Timber.v("Notification already seen") return false } @@ -53,19 +43,19 @@ class FacebookNotification( } override suspend fun buildSpecification(): NotificationSpec { - return FacebookSpecification(context) + return UseOurAppSpecification(context) } } -class FacebookSpecification(context: Context) : NotificationSpec { +class UseOurAppSpecification(context: Context) : NotificationSpec { override val channel = NotificationRegistrar.ChannelType.TUTORIALS override val systemId = NotificationRegistrar.NotificationId.Facebook - override val name = "Update auto clear data" + override val name = "Use our app" override val icon = R.drawable.notification_logo override val title: String = "Worried about Facebook tracking you?" override val description: String = "Here's a simply way to reduce it's reach." override val launchButton: String? = null override val closeButton: String? = null - override val pixelSuffix = "fb" + override val pixelSuffix = "uoa" override val autoCancel = true } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt index 3a0b90f67969..8ebf5c76f568 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStage.kt @@ -31,7 +31,8 @@ data class UserStage( enum class AppStage { NEW, DAX_ONBOARDING, - FLOW_FB, + USE_OUR_APP_NOTIFICATION, + USE_OUR_APP_ONBOARDING, ESTABLISHED; } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 014fe77e337e..26163fc462b5 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -42,7 +42,8 @@ class AppUserStageStore @Inject constructor( val newAppStage = when (appStage) { AppStage.NEW -> AppStage.DAX_ONBOARDING AppStage.DAX_ONBOARDING -> AppStage.ESTABLISHED - AppStage.FLOW_FB -> AppStage.ESTABLISHED + AppStage.USE_OUR_APP_NOTIFICATION -> AppStage.ESTABLISHED + AppStage.USE_OUR_APP_ONBOARDING -> AppStage.ESTABLISHED AppStage.ESTABLISHED -> AppStage.ESTABLISHED } @@ -67,6 +68,10 @@ suspend fun UserStageStore.daxOnboardingActive(): Boolean { return this.getUserAppStage() == AppStage.DAX_ONBOARDING } -suspend fun UserStageStore.fbFlowActive(): Boolean { - return this.getUserAppStage() == AppStage.FLOW_FB +suspend fun UserStageStore.useOurAppOnboarding(): Boolean { + return this.getUserAppStage() == AppStage.USE_OUR_APP_ONBOARDING +} + +suspend fun UserStageStore.useOurAppNotification(): Boolean { + return this.getUserAppStage() == AppStage.USE_OUR_APP_NOTIFICATION } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_fb_favicon.png b/app/src/main/res/drawable/ic_fb_favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..32416f0e52327a82548ddace6dd4543842876578 GIT binary patch literal 1779 zcmaJ?dpJ~S9G(;vtCYwkIgKtX=02E{`wV85vD2U^UFLFRFmuMtDT7G0BAcgDBuq+H z>B5q&p$(hXWmCCSMA?mYCAV1F>~7gJLi@+s^E~H#-|u^V@B6#G-*b*|e7$tFO|&r> zj4q4G;G!!{{Vvi%zekM@C8CQl;t`1W!68T#PX=M!gfJfhSYqC8hzs$A(UA?1BL*|y zLgXKa1hRcV0W8Mx)IK5P%Qu7D=42!?&uifJo?s-9lv(*it$a zDq_aSAio%2e?d&RfF{JcI0KFf5G4>p2oF$*BP4QA;e?&y1<}3w7>@;}T##@l?8l@6 z*&Kil%OHS?quL4xL<(R}!x5=OiUZLGAQ6aUJb{cSl5L43kU#^8RABnSqS0i+5Rl98 zoQ{QBPS{Wck%D-9R8$l$ij0G0yYWOCjfN+X@FbEg>R~I7mLNQZtwe4y!@z*#0+~pP zh+qkzX5{hVJ%|$)&GcglV(BccL_Xanv|)G!Pl_kv2BXR0Xn!43w03aKua(IiWBD%R^x)K&JziY@R?QQQKBi z05%(BN#qDmB7j&7CoD>V6N!YN1KEw@LAGa5?1%&+(Szbfu_w?yC=3#V>gmp~cbma7 zV8I?SBtd4d!cSO_Ik9Rjh^1&`1|$>hg@m3mSPV?n42tH?#eGh`X{>PWT6s%`KI-kPZc$@a`H%#ayA0KJ3<)}jUY8IS!_8mOF_3ldOZ`nNYOBuNAjdT= z^sv-jO3l-`yNI{Wvn{LsPP&OZzq?i$+3!-_lGyy}L8gjQn`phVf_OL8$lQn$=FSu? z(}-i6xArL>YA)B`*t*Ng?5Eq1VPb3F-$TmS%HbS0Fm~|#5j%6zl4`x<8eCT}T%}ZO zDiG!I{dq>~F9+z4<*g}MIr$>#?$6e%jZ1Ax9uCZBR!A$FA}SYbx7S(oN&%r_1bNKqZePCRF@v) zb3UKmibM!?n7v=9CJ{sPN}R>2G0?QM9+!hDSno9v89~@e_8n2ZTQ>9t+ok`qD)z%` zoseEbr4H+6XkkS4uW6CJwb8-lim8`?<;n$R^8o&)3M{itKnPzXQ6JY1&%dyiV1Gxpijr zm{xA(H}3}D+rJ!tmGZ9Hc)YVH#2aWh8~rRhPeL6{Bn>&of8915pNBW(pIlz1qa#-eqECezmQE zGWl(pma;G3CTN3Iq4BZBmhbCs9a8LtQ?`sw`rOG_7mn-;W_FJ^dUx49m7ExuIEw@^ zt0n21?^Rt7>}*oSh6QyOX5Ej=C_Ck2p|tH7>Ws_Xe)3Atws6((n!;dj(thu^!cO;= z9X?lu=PZk!xTbx@sZs5454AjWou4(H-(6Uh#%^YHY%r*)zEk`-i*;a(4OtV6jWhB-$C*8a)Jy3^nd9M=OABmWCMROGPuk}K%SX3X zn8o+Pz|oM#1S<|nLBi+aXz ptUrG3&${HsC472n{8RIJS{R+p4mGUkjvMOV5X-}tQRKes;6IqKzaans literal 0 HcmV?d00001 From 21c10ea28c22bea5dea2171e97f7baef39a913fd Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 10 Jun 2020 12:10:39 +0100 Subject: [PATCH 03/38] Rename methods/constants. Add strings for the new notification. Add new stage to the logic after onboarding depending on shortcut capabilities. --- .../com/duckduckgo/app/cta/model/DismissedCta.kt | 4 ++-- app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt | 2 +- .../java/com/duckduckgo/app/cta/ui/CtaViewModel.kt | 7 +++---- .../app/notification/AndroidNotificationScheduler.kt | 1 + .../app/notification/NotificationRegistrar.kt | 2 +- .../app/notification/model/UseOurAppNotification.kt | 11 +++++++---- .../app/onboarding/store/UserStageStore.kt | 12 ++++++++++-- app/src/main/res/values/string-untranslated.xml | 2 ++ 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt index f93c0879fe96..faf0f82adf40 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt @@ -29,8 +29,8 @@ enum class CtaId { DAX_DIALOG_NETWORK, DAX_DIALOG_OTHER, DAX_END, - FB_FLOW, - DELETE_FB + USE_OUR_APP, + USE_OUR_APP_DELETION } @Entity( diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 4ee955d7d77d..fa98cc2660bc 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -72,7 +72,7 @@ class DaxFacebookCta( @StringRes val text: Int = R.string.facebookDialogText, @StringRes val okButton: Int = R.string.facebookDialogButtonText, @StringRes val cancelButton: Int = R.string.facebookDialogCancelButtonText, - override val ctaId: CtaId = CtaId.FB_FLOW, + override val ctaId: CtaId = CtaId.USE_OUR_APP, override val shownPixel: Pixel.PixelName? = null, override val okPixel: Pixel.PixelName?= null, override val cancelPixel: Pixel.PixelName? = null diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 08094062c685..4893d712fa7c 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -35,7 +35,6 @@ import com.duckduckgo.app.onboarding.store.daxOnboardingActive import com.duckduckgo.app.onboarding.store.useOurAppOnboarding import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.settings.db.SettingsDataStore -import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.survey.db.SurveyDao @@ -165,7 +164,7 @@ class CtaViewModel @Inject constructor( canShowDaxCtaEndOfJourney() -> { DaxBubbleCta.DaxEndCta(onboardingStore, appInstallStore) } - canShowFacebookDialog() -> { + canShowUseOurAppDialog() -> { DaxFacebookCta() } canShowWidgetCta() -> { @@ -197,7 +196,7 @@ class CtaViewModel @Inject constructor( } @WorkerThread - private suspend fun canShowFacebookDialog(): Boolean = useOurAppActive() && !fbDialogShown() + private suspend fun canShowUseOurAppDialog(): Boolean = useOurAppActive() && !useOurAppDialogShown() @WorkerThread private fun canShowWidgetCta(): Boolean { @@ -257,7 +256,7 @@ class CtaViewModel @Inject constructor( } } - private fun fbDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.FB_FLOW) + private fun useOurAppDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.USE_OUR_APP) private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) diff --git a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt index 5072beca28f4..67ea83be28b7 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt @@ -60,6 +60,7 @@ class NotificationScheduler( ) : AndroidNotificationScheduler { override suspend fun scheduleNextNotification() { + scheduleFacebookNotification() scheduleInactiveUserNotifications() } diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt index 7e7a33f03d36..fc959e7d9f6d 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt @@ -50,7 +50,7 @@ class NotificationRegistrar @Inject constructor( const val PrivacyProtection = 101 const val Article = 103 // 102 was used for the search notification hence using 103 moving forward const val AppFeature = 104 - const val Facebook = 105 + const val UseOurApp = 105 } object ChannelType { diff --git a/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt index c0daa5d3f053..6968c409f1a9 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.notification.model import android.content.Context +import android.os.Bundle import com.duckduckgo.app.browser.R import com.duckduckgo.app.notification.NotificationHandlerService import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL @@ -29,7 +30,7 @@ class UseOurAppNotification( private val notificationDao: NotificationDao ) : SchedulableNotification { - override val id = "com.duckduckgo.privacytips.fb" + override val id = "com.duckduckgo.privacytips.useOurApp" override val launchIntent = NotificationHandlerService.NotificationEvent.USE_OUR_APP override val cancelIntent = CANCEL @@ -49,13 +50,15 @@ class UseOurAppNotification( class UseOurAppSpecification(context: Context) : NotificationSpec { override val channel = NotificationRegistrar.ChannelType.TUTORIALS - override val systemId = NotificationRegistrar.NotificationId.Facebook + override val systemId = NotificationRegistrar.NotificationId.UseOurApp override val name = "Use our app" override val icon = R.drawable.notification_logo - override val title: String = "Worried about Facebook tracking you?" - override val description: String = "Here's a simply way to reduce it's reach." + override val title: String = context.getString(R.string.useOurAppNotificationTitle) + override val description: String = context.getString(R.string.useOurAppNotificationDescription) override val launchButton: String? = null override val closeButton: String? = null override val pixelSuffix = "uoa" override val autoCancel = true + override val bundle: Bundle = Bundle() + override val color: Int = R.color.ic_launcher_red_background } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 26163fc462b5..210eb3c045b1 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.onboarding.store +import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.DispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject @@ -28,7 +29,8 @@ interface UserStageStore { class AppUserStageStore @Inject constructor( private val userStageDao: UserStageDao, - private val dispatcher: DispatcherProvider + private val dispatcher: DispatcherProvider, + private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector ) : UserStageStore { override suspend fun getUserAppStage(): AppStage { return withContext(dispatcher.io()) { @@ -41,7 +43,13 @@ class AppUserStageStore @Inject constructor( return withContext(dispatcher.io()) { val newAppStage = when (appStage) { AppStage.NEW -> AppStage.DAX_ONBOARDING - AppStage.DAX_ONBOARDING -> AppStage.ESTABLISHED + AppStage.DAX_ONBOARDING -> { + if (addToHomeCapabilityDetector.isAddToHomeSupported()) { + AppStage.USE_OUR_APP_NOTIFICATION + } else { + AppStage.ESTABLISHED + } + } AppStage.USE_OUR_APP_NOTIFICATION -> AppStage.ESTABLISHED AppStage.USE_OUR_APP_ONBOARDING -> AppStage.ESTABLISHED AppStage.ESTABLISHED -> AppStage.ESTABLISHED diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 79312fe9bb45..29636182046d 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -36,5 +36,7 @@ Did you know the Facebook app can make requests for data even when you\'re not using?<br/><br/>Replace the app with a home screen shortcut that opens Facebook in DuckDuckGo. Then delete the Facebook app. Add Facebook Shortcut Not Now + Worried about Facebook tracking you? + Here\'s a simply way to reduce it\'s reach. From 2f5c8c27ff1572f967de9de10f5a291069229371 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 10 Jun 2020 15:00:33 +0100 Subject: [PATCH 04/38] Move logic and add new tab repo methods --- .../duckduckgo/app/browser/BrowserActivity.kt | 12 +++-- .../app/browser/BrowserTabFragment.kt | 17 +------ .../app/browser/BrowserTabViewModel.kt | 12 ++--- .../app/browser/BrowserViewModel.kt | 4 ++ .../app/browser/favicon/FaviconDownloader.kt | 3 -- .../app/browser/shortcut/ShortcutBuilder.kt | 42 +++++++++++++--- .../app/browser/shortcut/ShortcutReceiver.kt | 50 +++++++++++++++++++ .../java/com/duckduckgo/app/cta/ui/Cta.kt | 2 +- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 2 +- .../java/com/duckduckgo/app/di/DaoModule.kt | 3 ++ .../java/com/duckduckgo/app/di/StoreModule.kt | 8 ++- .../app/global/DuckDuckGoApplication.kt | 13 +++++ .../timestamps/db/KeyTimestampEntity.kt | 2 +- .../com/duckduckgo/app/tabs/db/TabsDao.kt | 3 ++ .../app/tabs/model/TabDataRepository.kt | 13 +++++ .../app/tabs/model/TabRepository.kt | 2 + 16 files changed, 150 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 4850b08715cd..2c1c8e0f7e15 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -32,6 +32,7 @@ import com.duckduckgo.app.browser.BrowserViewModel.Command.Refresh import com.duckduckgo.app.browser.rating.ui.AppEnjoymentDialogFragment import com.duckduckgo.app.browser.rating.ui.GiveFeedbackDialogFragment import com.duckduckgo.app.browser.rating.ui.RateAppDialogFragment +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder import com.duckduckgo.app.feedback.ui.common.FeedbackActivity import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.ApplicationClearDataState @@ -202,9 +203,14 @@ class BrowserActivity : DuckDuckGoActivity(), CoroutineScope by MainScope() { val sharedText = intent.intentText if (sharedText != null) { - Timber.w("opening in new tab requested for $sharedText") - launch { viewModel.onOpenInNewTabRequested(sharedText, true) } - return + if (intent.getBooleanExtra(ShortcutBuilder.SHORTCUT_EXTRA_ARG, false)) { + Timber.d("Shortcut opened with url $sharedText") + launch { viewModel.onOpenShortcut(sharedText) } + } else { + Timber.w("opening in new tab requested for $sharedText") + launch { viewModel.onOpenInNewTabRequested(sharedText, true) } + return + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a76f02c7dc48..2fb1c433648d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -44,7 +44,6 @@ import androidx.annotation.AnyThread import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat -import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.core.view.* @@ -383,8 +382,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } private fun addHomeShortcut(homeShortcut: Command.AddHomeShortcut, context: Context) { - val shortcutInfo = shortcutBuilder.buildPinnedPageShortcut(context, homeShortcut) - ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null) + shortcutBuilder.requestPinShortcut(context, homeShortcut) } private fun configureObservers() { @@ -467,16 +465,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi viewModel.onUserSubmittedQuery(query) } - private fun navigateAndAddShortcut(shortCut: Command.AddHomeShortcut) { - hideKeyboard() - renderer.hideFindInPage() - viewModel.registerDaxBubbleCtaDismissed() - webView?.loadUrl(shortCut.url) -// context?.let { context -> -// addHomeShortcut(shortCut, context) -// } - } - private fun navigate(url: String) { hideKeyboard() renderer.hideFindInPage() @@ -583,9 +571,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi is Command.DaxCommand.HideDaxDialog -> showHideTipsDialog(it.cta) is Command.HideWebContent -> webView?.hide() is Command.ShowWebContent -> webView?.show() - is Command.NavigateAndAddShortCut -> { - navigateAndAddShortcut(it.shortcut) - } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 1e3e2bb9133d..25997862ea94 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -52,6 +52,8 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_TITLE +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao @@ -213,7 +215,6 @@ class BrowserTabViewModel( object LaunchTabSwitcher : Command() object HideWebContent : Command() object ShowWebContent : Command() - class NavigateAndAddShortCut(val shortcut: AddHomeShortcut) : Command() class ShowErrorWithAction(val action: () -> Unit) : Command() sealed class DaxCommand : Command() { @@ -1078,15 +1079,14 @@ class BrowserTabViewModel( is HomePanelCta.Survey -> LaunchSurvey(cta.survey) is HomePanelCta.AddWidgetAuto -> LaunchAddWidget is HomePanelCta.AddWidgetInstructions -> LaunchLegacyAddWidget - is DaxFacebookCta -> navigateToUrlAndAddShorcut(url = "https://www.facebook.com", title = "Facebook", icon = R.drawable.ic_fb_favicon) + is UseOurAppCta -> navigateToUrlAndAddShortcut(url = USE_OUR_APP_SHORTCUT_URL, title = USE_OUR_APP_SHORTCUT_TITLE) else -> return } } - private fun navigateToUrlAndAddShorcut(url: String, title: String, icon: Int): AddHomeShortcut { - val convertedUrl = queryUrlConverter.convertQueryToUrl(url) - onUserSubmittedQuery(convertedUrl) - return AddHomeShortcut(title, convertedUrl, faviconDownloader.load(icon)) + private fun navigateToUrlAndAddShortcut(url: String, title: String): AddHomeShortcut { + onUserSubmittedQuery(url) + return AddHomeShortcut(title, url) } fun onUserClickCtaSecondaryButton() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 6b80b16988a9..194e5ab101b8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -197,4 +197,8 @@ class BrowserViewModel( override fun onUserCancelledGiveFeedbackDialog(promptCount: PromptCount) { onUserDeclinedToGiveFeedback(promptCount) } + + fun onOpenShortcut(url: String) { + tabRepository.selectByUrlOrNewTab(url, queryUrlConverter.convertQueryToUrl(url)) + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt index dac5f3f51ab5..b3da89fefc69 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt @@ -32,7 +32,6 @@ import javax.inject.Inject interface FaviconDownloader { fun download(currentPageUrl: Uri): Single - fun load(@DrawableRes drawableRes: Int): Bitmap? } class GlideFaviconDownloader @Inject constructor(private val context: Context) : FaviconDownloader { @@ -52,8 +51,6 @@ class GlideFaviconDownloader @Inject constructor(private val context: Context) : } } - override fun load(drawableRes: Int): Bitmap? = ContextCompat.getDrawable(context, drawableRes)?.toBitmap() - companion object { private const val DESIRED_IMAGE_SIZE_DP = 24 private const val TIMEOUT_PERIOD_SECONDS: Long = 3 diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt index 6b00cf252833..275c50d66792 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt @@ -16,27 +16,32 @@ package com.duckduckgo.app.browser.shortcut +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.BrowserTabViewModel import com.duckduckgo.app.browser.R -import java.util.* +import timber.log.Timber +import java.util.UUID import javax.inject.Inject class ShortcutBuilder @Inject constructor() { - fun buildPinnedPageShortcut(context: Context, homeShortcut: BrowserTabViewModel.Command.AddHomeShortcut): ShortcutInfoCompat { + private fun buildPinnedPageShortcut(context: Context, homeShortcut: BrowserTabViewModel.Command.AddHomeShortcut): ShortcutInfoCompat { val intent = Intent(context, BrowserActivity::class.java) intent.action = Intent.ACTION_VIEW intent.putExtra(Intent.EXTRA_TEXT, homeShortcut.url) + intent.putExtra(SHORTCUT_EXTRA_ARG, true) - val icon = if (homeShortcut.icon != null) { - IconCompat.createWithBitmap(homeShortcut.icon) - } else { - IconCompat.createWithResource(context, R.drawable.logo_mini) + val icon = when { + homeShortcut.icon != null -> IconCompat.createWithBitmap(homeShortcut.icon) + homeShortcut.url == USE_OUR_APP_SHORTCUT_URL -> IconCompat.createWithResource(context, R.drawable.ic_fb_favicon) + else -> IconCompat.createWithResource(context, R.drawable.logo_mini) } return ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString()) @@ -45,4 +50,29 @@ class ShortcutBuilder @Inject constructor() { .setIcon(icon) .build() } + + private fun buildPendingIntent(context: Context, url: String, title: String): PendingIntent? { + val pinnedShortcutCallbackIntent = Intent(USE_OUR_APP_SHORTCUT_ADDED) + pinnedShortcutCallbackIntent.putExtra(SHORTCUT_URL_ARG, url) + pinnedShortcutCallbackIntent.putExtra(SHORTCUT_TITLE_ARG, title) + return PendingIntent.getBroadcast(context, USE_OUR_APP_SHORTCUT_ADDED_CODE, pinnedShortcutCallbackIntent, FLAG_UPDATE_CURRENT) + } + + fun requestPinShortcut(context: Context, homeShortcut: BrowserTabViewModel.Command.AddHomeShortcut) { + val shortcutInfo = buildPinnedPageShortcut(context, homeShortcut) + val pendingIntent = buildPendingIntent(context, homeShortcut.url, homeShortcut.title) + + ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, pendingIntent?.intentSender) + } + + companion object { + const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" + const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" + const val USE_OUR_APP_SHORTCUT_ADDED: String = "useOurAppShortcutAdded" + const val USE_OUR_APP_SHORTCUT_ADDED_CODE = 9000 + + const val SHORTCUT_EXTRA_ARG = "shortCutAdded" + const val SHORTCUT_URL_ARG = "shortcutUrl" + const val SHORTCUT_TITLE_ARG = "shortcutTitle" + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt new file mode 100644 index 000000000000..a2c4a9036948 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -0,0 +1,50 @@ +/* + * 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.browser.shortcut + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.widget.Toast +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TITLE_ARG +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_URL +import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.timestamps.db.TimestampKey +import com.duckduckgo.app.statistics.pixels.Pixel +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ShortcutReceiver @Inject constructor(private val keyTimestampStore: KeyTimestampStore, val dispatcher: DispatcherProvider, val pixel: Pixel) : + BroadcastReceiver() { + + override fun onReceive(context: Context?, intent: Intent?) { + val originUrl = intent?.getStringExtra(SHORTCUT_URL_ARG) + val title = intent?.getStringExtra(SHORTCUT_TITLE_ARG) + + Toast.makeText(context, "Success! $title has been added to your home screen.", Toast.LENGTH_SHORT).show() + + GlobalScope.launch(dispatcher.io()) { + if (originUrl == USE_OUR_APP_SHORTCUT_URL) { + keyTimestampStore.registerTimestamp(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) + } + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index fa98cc2660bc..f70a6fd0ebfa 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -68,7 +68,7 @@ interface Cta { fun pixelOkParameters(): Map } -class DaxFacebookCta( +class UseOurAppCta( @StringRes val text: Int = R.string.facebookDialogText, @StringRes val okButton: Int = R.string.facebookDialogButtonText, @StringRes val cancelButton: Int = R.string.facebookDialogCancelButtonText, diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 4893d712fa7c..fae9a9dad3d3 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -165,7 +165,7 @@ class CtaViewModel @Inject constructor( DaxBubbleCta.DaxEndCta(onboardingStore, appInstallStore) } canShowUseOurAppDialog() -> { - DaxFacebookCta() + UseOurAppCta() } canShowWidgetCta() -> { if (widgetCapabilities.supportsAutomaticWidgetAdd) AddWidgetAuto else AddWidgetInstructions diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index 134af216b55f..b335e21d6d2f 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -82,4 +82,7 @@ class DaoModule { @Provides fun fireproofWebsiteDao(database: AppDatabase) = database.fireproofWebsiteDao() + + @Provides + fun keyTimestampDao(database: AppDatabase) = database.keyTimestampDao() } diff --git a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt index dd6562647d54..aa1d7d83e806 100644 --- a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt @@ -20,6 +20,9 @@ import com.duckduckgo.app.fire.UnsentForgetAllPixelStore import com.duckduckgo.app.fire.UnsentForgetAllPixelStoreSharedPreferences import com.duckduckgo.app.global.install.AppInstallSharedPreferences import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.global.timestamps.db.AppKeyTimestampStore +import com.duckduckgo.app.global.timestamps.db.KeyTimestampDao +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore import com.duckduckgo.app.onboarding.store.AppUserStageStore import com.duckduckgo.app.onboarding.store.OnboardingSharedPreferences import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -60,5 +63,8 @@ abstract class StoreModule { abstract fun bindOfflinePixelDataStore(store: OfflinePixelCountSharedPreferences): OfflinePixelCountDataStore @Binds - abstract fun bindUserStageStore(userStageDao: AppUserStageStore): UserStageStore + abstract fun bindUserStageStore(userStageStore: AppUserStageStore): UserStageStore + + @Binds + abstract fun bindKeyTimestampStore(keyTimestampStore: AppKeyTimestampStore): KeyTimestampStore } diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt index 8059dfe94fac..638832369028 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.global import android.app.Application +import android.content.IntentFilter import android.os.Build import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver @@ -25,6 +26,8 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.WorkerFactory import com.duckduckgo.app.browser.BuildConfig import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserObserver +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder +import com.duckduckgo.app.browser.shortcut.ShortcutReceiver import com.duckduckgo.app.di.AppComponent import com.duckduckgo.app.di.DaggerAppComponent import com.duckduckgo.app.fire.DataClearer @@ -66,6 +69,7 @@ import timber.log.Timber import javax.inject.Inject import kotlin.concurrent.thread + open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleObserver { @Inject @@ -146,6 +150,9 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO @Inject lateinit var atbInitializer: AtbInitializer + @Inject + lateinit var shortcutReceiver: ShortcutReceiver + @Inject lateinit var variantManager: VariantManager @@ -286,6 +293,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO Timber.i("Suppressing app launch pixel") return } + registerReceiver(shortcutReceiver, IntentFilter(ShortcutBuilder.USE_OUR_APP_SHORTCUT_ADDED)) pixel.fire(APP_LAUNCH) } @@ -298,6 +306,11 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO } } + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onAppStopped() { + unregisterReceiver(shortcutReceiver) + } + companion object { private const val APP_RESTART_CAUSED_BY_FIRE_GRACE_PERIOD: Long = 10_000L } diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt index 5fa1430b58c9..2c197c99c812 100644 --- a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt @@ -25,7 +25,7 @@ const val KEY_TIMESTAMPS_TABLE_NAME = "keyTimestamps" @Entity(tableName = KEY_TIMESTAMPS_TABLE_NAME) data class KeyTimestampEntity( @PrimaryKey val id: TimestampKey, - val timestamp: Int + val timestamp: Long = System.currentTimeMillis() ) enum class TimestampKey { diff --git a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt index e95a3862cc60..53bfb3a2bf08 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt @@ -44,6 +44,9 @@ abstract class TabsDao { @Query("select * from tabs where tabId = :tabId") abstract fun tab(tabId: String): TabEntity? + @Query("select tabId from tabs where url LIKE :url") + abstract fun selectTabByUrl(url: String): String? + @Insert(onConflict = OnConflictStrategy.REPLACE) abstract fun insertTab(tab: TabEntity) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index e70c014fae72..8c493b321746 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -85,6 +85,19 @@ class TabDataRepository @Inject constructor( } } + override fun selectByUrlOrNewTab(url: String, query: String) { + databaseExecutor().scheduleDirect { + val tabId = tabsDao.selectTabByUrl(url) + GlobalScope.launch { + if (tabId != null) { + select(tabId) + } else { + add(query, skipHome = true, isDefaultTab = false) + } + } + } + } + override suspend fun addNewTabAfterExistingTab(url: String?, tabId: String) { databaseExecutor().scheduleDirect { val position = tabsDao.tab(tabId)?.position ?: -1 diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index ad7e2067d4ec..188232309caa 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -49,4 +49,6 @@ interface TabRepository { suspend fun select(tabId: String) fun updateTabPreviewImage(tabId: String, fileName: String?) + + fun selectByUrlOrNewTab(url: String, query: String) } From 6219cfdbc362cb2e4d325e58cabd39535e7b11ca Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 10 Jun 2020 15:18:35 +0100 Subject: [PATCH 05/38] Add deletion cta to flow --- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 23 ++++++++++++++++++ .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 24 +++++++++++++++++++ .../main/res/values/string-untranslated.xml | 1 + 3 files changed, 48 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index f70a6fd0ebfa..e83eb8817924 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -92,7 +92,30 @@ class UseOurAppCta( override fun pixelOkParameters(): Map = emptyMap() override fun pixelShownParameters(): Map = emptyMap() +} + +class UseOurAppDeletionCta( + @StringRes val text: Int = R.string.deletionDialogText, + @StringRes val okButton: Int = R.string.daxDialogGotIt, + override val ctaId: CtaId = CtaId.USE_OUR_APP_DELETION, + override val shownPixel: Pixel.PixelName? = null, + override val okPixel: Pixel.PixelName? = null, + override val cancelPixel: Pixel.PixelName? = null +) : Cta, DialogCta { + + override fun createCta(activity: FragmentActivity): DaxDialog = + TypewriterDaxDialog.newInstance( + daxText = activity.resources.getString(text), + primaryButtonText = activity.resources.getString(okButton), + dismissible = false, + showHideButton = false + ) + override fun pixelCancelParameters(): Map = emptyMap() + + override fun pixelOkParameters(): Map = emptyMap() + + override fun pixelShownParameters(): Map = emptyMap() } sealed class DaxDialogCta( diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index fae9a9dad3d3..f8efdf78b22a 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -27,7 +27,10 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain +import com.duckduckgo.app.global.model.domainMatchesUrl import com.duckduckgo.app.global.model.orderedTrackingEntities +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.timestamps.db.TimestampKey import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -42,6 +45,7 @@ import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.widget.ui.WidgetCapabilities import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.CoroutineContext @@ -58,6 +62,7 @@ class CtaViewModel @Inject constructor( private val settingsDataStore: SettingsDataStore, private val onboardingStore: OnboardingStore, private val userStageStore: UserStageStore, + private val keyTimestampStore: KeyTimestampStore, private val dispatchers: DispatcherProvider ) { val surveyLiveData: LiveData = surveyDao.getLiveScheduled() @@ -179,6 +184,7 @@ class CtaViewModel @Inject constructor( canShowDaxDialogCta() -> { getDaxDialogCta(site) } + canShowUseOurAppDeletionDialog(site) -> UseOurAppDeletionCta() else -> null } } @@ -195,6 +201,22 @@ class CtaViewModel @Inject constructor( return null } + @WorkerThread + private suspend fun canShowUseOurAppDeletionDialog(site: Site?): Boolean = + isOnUseOurAppSite(site) && twoDaysSinceShortcutAdded() && !useOurAppDeletionDialogShown() + + @WorkerThread + private suspend fun twoDaysSinceShortcutAdded(): Boolean { + val timestampKey = keyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) ?: return false + val days = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestampKey.timestamp) + return (days >= 2) + } + + private fun isOnUseOurAppSite(site: Site?): Boolean { + if (site == null) return false + return site.domainMatchesUrl("m.facebook.com") || site.domainMatchesUrl("facebook.com") + } + @WorkerThread private suspend fun canShowUseOurAppDialog(): Boolean = useOurAppActive() && !useOurAppDialogShown() @@ -256,6 +278,8 @@ class CtaViewModel @Inject constructor( } } + private fun useOurAppDeletionDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION) + private fun useOurAppDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.USE_OUR_APP) private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 29636182046d..7d236cc67a6b 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -39,4 +39,5 @@ Worried about Facebook tracking you? Here\'s a simply way to reduce it\'s reach. + Checking your feed in DuckDuckGo is a great alternative to using the Facebook app!<br/><br/>But if the Facebook app is on your phone, it can make requests for data even when you\'re not using it.<br/><br/>Prevent this by deleting it now! From 5c8e12a25a762a5a5462fe0ec8198e0f5fbe8931 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 11 Jun 2020 16:26:05 +0100 Subject: [PATCH 06/38] Hide keyboard when showing dialog --- .../app/browser/BrowserTabFragment.kt | 5 ++- .../app/browser/BrowserTabViewModel.kt | 32 +++++++++++++------ .../NotificationHandlerService.kt | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 2fb1c433648d..6424ffb1de54 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -446,7 +446,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun showHome() { errorSnackbar.dismiss() newTabLayout.show() - showKeyboardImmediately() appBarLayout.setExpanded(true) webView?.onPause() webView?.hide() @@ -940,7 +939,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun hideKeyboardImmediately() { if (!isHidden) { - Timber.v("Keyboard now hiding") + Timber.v("Keyboard now hiding immediately") omnibarTextInput.hideKeyboard() focusDummy.requestFocus() } @@ -956,7 +955,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun showKeyboardImmediately() { if (!isHidden) { - Timber.v("Keyboard now showing") + Timber.v("Keyboard now showing immediately") omnibarTextInput?.showKeyboard() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 25997862ea94..aadff672b991 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -333,7 +333,6 @@ class BrowserTabViewModel( } fun onViewResumed() { - command.value = if (!currentBrowserViewState().browserShowing) ShowKeyboard else HideKeyboard if (currentGlobalLayoutState() is Invalidated && currentBrowserViewState().browserShowing) { showErrorWithAction() } @@ -342,7 +341,10 @@ class BrowserTabViewModel( fun onViewVisible() { // we expect refreshCta to be called when a site is fully loaded if browsingShowing -trackers data available-. if (!currentBrowserViewState().browserShowing) { - refreshCta() + viewModelScope.launch { + val cta = refreshCta() + showOrHideKeyboard(cta) // we hide the keyboard when showing a DialogCta type in the home screen otherwise we show it + } } } @@ -459,6 +461,7 @@ class BrowserTabViewModel( return true } else if (!skipHome) { navigateHome() + command.value = ShowKeyboard return true } @@ -480,7 +483,6 @@ class BrowserTabViewModel( findInPageViewState.value = FindInPageViewState() omnibarViewState.value = currentOmnibarViewState().copy(omnibarText = "", shouldMoveCaretToEnd = false) loadingViewState.value = currentLoadingViewState().copy(isLoading = false) - deleteTabPreview(tabId) } @@ -1038,7 +1040,9 @@ class BrowserTabViewModel( fun onSurveyChanged(survey: Survey?) { val activeSurvey = ctaViewModel.onSurveyChanged(survey) if (activeSurvey != null) { - refreshCta() + viewModelScope.launch { + refreshCta() + } } } @@ -1051,15 +1055,20 @@ class BrowserTabViewModel( ctaViewModel.onCtaShown(cta) } - fun refreshCta() { + suspend fun refreshCta(): Cta? { if (currentGlobalLayoutState() is Browser) { - viewModelScope.launch { - val cta = withContext(dispatchers.io()) { - ctaViewModel.refreshCta(dispatchers.io(), currentBrowserViewState().browserShowing, siteLiveData.value) - } - ctaViewState.value = currentCtaViewState().copy(cta = cta) + val cta = withContext(dispatchers.io()) { + ctaViewModel.refreshCta(dispatchers.io(), currentBrowserViewState().browserShowing, siteLiveData.value) } + ctaViewState.value = currentCtaViewState().copy(cta = cta) + return cta } + return null + } + + private fun showOrHideKeyboard(cta: Cta?) { + val browserState = currentBrowserViewState() + command.value = if (!browserState.browserShowing && cta !is DialogCta) ShowKeyboard else HideKeyboard } fun registerDaxBubbleCtaDismissed() { @@ -1093,6 +1102,9 @@ class BrowserTabViewModel( viewModelScope.launch { val cta = currentCtaViewState().cta ?: return@launch ctaViewModel.onUserDismissedCta(cta) + if (cta is UseOurAppCta) { + command.value = ShowKeyboard + } } } diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt index 3510b5477985..7ae2db73abf3 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt @@ -87,8 +87,8 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { USE_OUR_APP -> { GlobalScope.launch(dispatcher.io()) { userStageStore.registerInStage(AppStage.USE_OUR_APP_ONBOARDING) + onAppLaunched(pixelSuffix) } - onAppLaunched(pixelSuffix) } } From d91c0390e126cf9f0b2998ad9025e7ac5cd11dd9 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 12 Jun 2020 09:03:25 +0100 Subject: [PATCH 07/38] Add copy to shortcut success toast --- .../app/browser/shortcut/ShortcutReceiver.kt | 12 +++++++++++- app/src/main/res/values/string-untranslated.xml | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index a2c4a9036948..24670996d5bd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -19,7 +19,9 @@ package com.duckduckgo.app.browser.shortcut import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Build import android.widget.Toast +import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TITLE_ARG import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_URL @@ -39,7 +41,11 @@ class ShortcutReceiver @Inject constructor(private val keyTimestampStore: KeyTim val originUrl = intent?.getStringExtra(SHORTCUT_URL_ARG) val title = intent?.getStringExtra(SHORTCUT_TITLE_ARG) - Toast.makeText(context, "Success! $title has been added to your home screen.", Toast.LENGTH_SHORT).show() + if (!IGNORE_MANUFACTURERS_LIST.contains(Build.MANUFACTURER)) { + context?.let { + Toast.makeText(it, it.getString(R.string.useOurAppShortcutAddedText, title), Toast.LENGTH_SHORT).show() + } + } GlobalScope.launch(dispatcher.io()) { if (originUrl == USE_OUR_APP_SHORTCUT_URL) { @@ -47,4 +53,8 @@ class ShortcutReceiver @Inject constructor(private val keyTimestampStore: KeyTim } } } + + companion object { + val IGNORE_MANUFACTURERS_LIST = listOf("samsung", "huawei") + } } diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 7d236cc67a6b..0ac11889055d 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -38,6 +38,7 @@ Not Now Worried about Facebook tracking you? Here\'s a simply way to reduce it\'s reach. - + Success! %s has been added to your home screen. Checking your feed in DuckDuckGo is a great alternative to using the Facebook app!<br/><br/>But if the Facebook app is on your phone, it can make requests for data even when you\'re not using it.<br/><br/>Prevent this by deleting it now! + From 623419002128e250e5dc1d0c7e810c12ae3cdb8a Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 12 Jun 2020 12:51:01 +0100 Subject: [PATCH 08/38] Fix tests --- .../app/browser/BrowserTabViewModelTest.kt | 42 +++++++++++++---- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 13 ++--- .../app/global/db/AppDatabaseTest.kt | 47 ++++++++++++++++--- .../AndroidNotificationSchedulerTest.kt | 7 ++- .../onboarding/store/AppUserStageStoreTest.kt | 6 ++- .../systemsearch/SystemSearchViewModelTest.kt | 4 +- .../app/browser/BrowserTabFragment.kt | 1 - .../app/browser/BrowserTabViewModel.kt | 5 +- .../app/browser/favicon/FaviconDownloader.kt | 3 -- .../app/browser/shortcut/ShortcutBuilder.kt | 1 - .../java/com/duckduckgo/app/cta/ui/Cta.kt | 2 +- .../java/com/duckduckgo/app/di/StoreModule.kt | 1 - .../app/global/DuckDuckGoApplication.kt | 1 - .../app/onboarding/store/UserStageStore.kt | 2 +- 14 files changed, 98 insertions(+), 37 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index ad65bd089067..baba3143df41 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -55,6 +55,8 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao @@ -180,6 +182,9 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockUserWhitelistDao: UserWhitelistDao + @Mock + private lateinit var mockKeyTimestampStore: KeyTimestampStore + private lateinit var mockAutoCompleteApi: AutoCompleteApi private lateinit var ctaViewModel: CtaViewModel @@ -217,6 +222,7 @@ class BrowserTabViewModelTest { mockSettingsStore, mockOnboardingStore, mockUserStageStore, + mockKeyTimestampStore, coroutineRule.testDispatcherProvider ) @@ -300,19 +306,31 @@ class BrowserTabViewModelTest { } @Test - fun whenViewIsResumedAndBrowserShowingThenKeyboardHidden() { - setBrowserShowing(true) - testee.onViewResumed() + fun whenViewBecomesVisibleAndHomeShowingAndUserIsInUseOurAppOnboardingStageThenKeyboardShown() = coroutineRule.runBlocking { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) + setBrowserShowing(false) + + testee.onViewVisible() verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) - assertTrue(commandCaptor.allValues.contains(Command.HideKeyboard)) + assertTrue(commandCaptor.allValues.contains(Command.ShowKeyboard)) } @Test - fun whenViewIsResumedAndHomeShowingThenKeyboardShown() { + fun whenViewBecomesVisibleAndHomeShowingAndUserIsNotInUseOurAppOnboardingStageThenKeyboardHidden() = coroutineRule.runBlocking { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_ONBOARDING) setBrowserShowing(false) - testee.onViewResumed() + + testee.onViewVisible() verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) - assertTrue(commandCaptor.allValues.contains(Command.ShowKeyboard)) + assertTrue(commandCaptor.allValues.contains(Command.HideKeyboard)) + } + + @Test + fun whenViewBecomesVisibleAndBrowserShowingThenKeyboardHidden() { + setBrowserShowing(true) + testee.onViewVisible() + verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertTrue(commandCaptor.allValues.contains(Command.HideKeyboard)) } @Test @@ -1490,7 +1508,15 @@ class BrowserTabViewModelTest { fun whenUserPressesBackAndNotSkippingHomeThenWebViewPreviewNotGenerated() { setupNavigation(isBrowsing = true, canGoBack = false, skipHome = false) testee.onUserPressedBack() - verify(mockCommandObserver, never()).onChanged(commandCaptor.capture()) + assertFalse(commandCaptor.allValues.contains(Command.GenerateWebViewPreviewImage)) + } + + @Test + fun whenUserPressesBackAndGoesToHomeThenKeyboardShown() { + setupNavigation(isBrowsing = true, canGoBack = false, skipHome = false) + testee.onUserPressedBack() + verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertTrue(commandCaptor.allValues.contains(Command.ShowKeyboard)) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 2eb7e21ad725..474de9cf6e4b 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -28,6 +28,7 @@ import com.duckduckgo.app.cta.model.DismissedCta import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -106,6 +107,9 @@ class CtaViewModelTest { @Mock private lateinit var mockUserStageStore: UserStageStore + @Mock + private lateinit var mockKeyTimestampStore: KeyTimestampStore + private val requiredDaxOnboardingCtas: List = listOf( CtaId.DAX_INTRO, CtaId.DAX_DIALOG_SERP, @@ -139,6 +143,7 @@ class CtaViewModelTest { mockSettingsDataStore, mockOnboardingStore, mockUserStageStore, + mockKeyTimestampStore, coroutineRule.testDispatcherProvider ) } @@ -186,14 +191,6 @@ class CtaViewModelTest { verify(mockPixel).fire(eq(SURVEY_CTA_LAUNCHED), any(), any()) } - @Test - fun whenCtaSecondaryButtonClickedPixelIsFired() { - val secondaryButtonCta = mock() - whenever(secondaryButtonCta.secondaryButtonPixel).thenReturn(ONBOARDING_DAX_ALL_CTA_HIDDEN) - testee.onUserClickCtaSecondaryButton(secondaryButtonCta) - verify(mockPixel).fire(eq(ONBOARDING_DAX_ALL_CTA_HIDDEN), any(), any()) - } - @Test fun whenCtaDismissedPixelIsFired() = coroutineRule.runBlocking { testee.onUserDismissedCta(HomePanelCta.Survey(Survey("abc", "http://example.com", 1, SCHEDULED))) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index e82bc18113dd..75bdcddcc196 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -16,12 +16,15 @@ package com.duckduckgo.app.global.db +import android.content.ContentValues import android.content.Context import android.content.SharedPreferences +import android.database.sqlite.SQLiteDatabase import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.room.migration.Migration import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.duckduckgo.app.CoroutineTestRule @@ -192,10 +195,11 @@ class AppDatabaseTest { } @Test - fun whenMigratingFromVersion17To18IfUserSeeOnboardingThenMigrateToEstablished() = coroutineRule.runBlocking { + fun whenMigratingFromVersion17To18IfUserSeeOnboardingThenMigrateToEstablished() { givenUserSawOnboarding() - createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) - assertEquals(AppStage.ESTABLISHED, database().userStageDao().currentUserAppStage()?.appStage) + val database = createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) + val stage = getUserStage(database) + assertEquals(AppStage.ESTABLISHED.name, stage) } @Test @@ -216,19 +220,48 @@ class AppDatabaseTest { createDatabaseAndMigrate(20, 21, migrationsProvider.MIGRATION_20_TO_21) } + @Test + fun whenMigratingFromVersion21To22ThenValidationSucceeds() { + createDatabaseAndMigrate(21, 22, migrationsProvider.MIGRATION_21_TO_22) + } + + @Test + fun whenMigratingFromVersion21To22IfUserIsEstablishedThenMigrateToNotification() { + val values: ContentValues = ContentValues().apply { + put("key", 1) + put("appStage", AppStage.ESTABLISHED.name) + } + testHelper.createDatabase(TEST_DB_NAME, 21).use { + it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) + val stage = getUserStage(it) + assertEquals(AppStage.USE_OUR_APP_NOTIFICATION.name, stage) + } + } + + private fun getUserStage(database: SupportSQLiteDatabase): String { + var stage = "" + database.query("SELECT appStage from userStage limit 1").use { + it.moveToFirst() + stage = it.getString(0) + } + return stage + } + private fun createDatabase(version: Int) { testHelper.createDatabase(TEST_DB_NAME, version).close() } - private fun runMigrations(newVersion: Int, vararg migrations: Migration) { - testHelper.runMigrationsAndValidate(TEST_DB_NAME, newVersion, true, *migrations) + private fun runMigrations(newVersion: Int, vararg migrations: Migration): SupportSQLiteDatabase { + return testHelper.runMigrationsAndValidate(TEST_DB_NAME, newVersion, true, *migrations) } - private fun createDatabaseAndMigrate(originalVersion: Int, newVersion: Int, vararg migrations: Migration) { + private fun createDatabaseAndMigrate(originalVersion: Int, newVersion: Int, vararg migrations: Migration): SupportSQLiteDatabase { createDatabase(originalVersion) - runMigrations(newVersion, *migrations) + return runMigrations(newVersion, *migrations) } + @Deprecated("Don't use anymore, instead execute a query directly to the database, see getUserStage as an example") private fun database(): AppDatabase { val database = Room .databaseBuilder(getInstrumentation().targetContext, AppDatabase::class.java, TEST_DB_NAME) diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt index b1c4fe1ae70c..52508c9d47a3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt @@ -33,6 +33,7 @@ import com.duckduckgo.app.notification.NotificationScheduler.DripA2NotificationW import com.duckduckgo.app.notification.NotificationScheduler.DripB1NotificationWorker import com.duckduckgo.app.notification.NotificationScheduler.DripB2NotificationWorker import com.duckduckgo.app.notification.model.SchedulableNotification +import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.VariantManager.VariantFeature.DripNotification @@ -61,12 +62,14 @@ class AndroidNotificationSchedulerTest { var coroutinesTestRule = CoroutineTestRule() private val variantManager: VariantManager = mock() + private val userStageStore: UserStageStore = mock() private val clearNotification: SchedulableNotification = mock() private val privacyNotification: SchedulableNotification = mock() private val dripA1Notification: SchedulableNotification = mock() private val dripA2Notification: SchedulableNotification = mock() private val dripB1Notification: SchedulableNotification = mock() private val dripB2Notification: SchedulableNotification = mock() + private val ourAppNotification: SchedulableNotification = mock() private val context = InstrumentationRegistry.getInstrumentation().targetContext private lateinit var workManager: WorkManager @@ -84,7 +87,9 @@ class AndroidNotificationSchedulerTest { dripA2Notification, dripB1Notification, dripB2Notification, - variantManager + ourAppNotification, + variantManager, + userStageStore ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt index 4ddec2a19abb..a8ac1146d14f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt @@ -17,21 +17,25 @@ package com.duckduckgo.app.onboarding.store import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.runBlocking import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test +@ExperimentalCoroutinesApi class AppUserStageStoreTest { @get:Rule var coroutineRule = CoroutineTestRule() private val userStageDao = mock() + private val addToHomeCapabilityDetector = mock() - private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider) + private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, addToHomeCapabilityDetector) @Test fun whenGetUserAppStageThenRetunCurrentStage() = coroutineRule.runBlocking { diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 9cf35607a05b..f92b679e7949 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -24,6 +24,7 @@ import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel @@ -57,6 +58,7 @@ class SystemSearchViewModelTest { private val mockDeviceAppLookup: DeviceAppLookup = mock() private val mockAutoComplete: AutoComplete = mock() private val mockPixel: Pixel = mock() + private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector = mock() private val commandObserver: Observer = mock() private val commandCaptor = argumentCaptor() @@ -273,7 +275,7 @@ class SystemSearchViewModelTest { override suspend fun currentUserAppStage() = UserStage(appStage = AppStage.NEW) override fun insert(userStage: UserStage) {} } - return AppUserStageStore(emptyUserStageDao, coroutineRule.testDispatcherProvider) + return AppUserStageStore(emptyUserStageDao, coroutineRule.testDispatcherProvider, addToHomeCapabilityDetector) } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 6424ffb1de54..8b11747341d7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1202,7 +1202,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi viewModel.onUserClickCtaSecondaryButton() } - private fun launchHideTipsDialog(context: Context, cta: Cta) { AlertDialog.Builder(context) .setTitle(R.string.hideTipsTitle) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index aadff672b991..c3aba36dfa65 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -345,6 +345,8 @@ class BrowserTabViewModel( val cta = refreshCta() showOrHideKeyboard(cta) // we hide the keyboard when showing a DialogCta type in the home screen otherwise we show it } + } else { + command.value = HideKeyboard } } @@ -1067,8 +1069,7 @@ class BrowserTabViewModel( } private fun showOrHideKeyboard(cta: Cta?) { - val browserState = currentBrowserViewState() - command.value = if (!browserState.browserShowing && cta !is DialogCta) ShowKeyboard else HideKeyboard + command.value = if (cta is DialogCta) HideKeyboard else ShowKeyboard } fun registerDaxBubbleCtaDismissed() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt index b3da89fefc69..eecdec0c9c77 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt @@ -19,9 +19,6 @@ package com.duckduckgo.app.browser.favicon import android.content.Context import android.graphics.Bitmap import android.net.Uri -import androidx.annotation.DrawableRes -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap import com.bumptech.glide.Glide import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.view.toPx diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt index 275c50d66792..641ebb4363f8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt @@ -26,7 +26,6 @@ import androidx.core.graphics.drawable.IconCompat import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.BrowserTabViewModel import com.duckduckgo.app.browser.R -import timber.log.Timber import java.util.UUID import javax.inject.Inject diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index e83eb8817924..4dcde794c84e 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -74,7 +74,7 @@ class UseOurAppCta( @StringRes val cancelButton: Int = R.string.facebookDialogCancelButtonText, override val ctaId: CtaId = CtaId.USE_OUR_APP, override val shownPixel: Pixel.PixelName? = null, - override val okPixel: Pixel.PixelName?= null, + override val okPixel: Pixel.PixelName? = null, override val cancelPixel: Pixel.PixelName? = null ) : Cta, DialogCta { diff --git a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt index aa1d7d83e806..1e1597097c1e 100644 --- a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt @@ -21,7 +21,6 @@ import com.duckduckgo.app.fire.UnsentForgetAllPixelStoreSharedPreferences import com.duckduckgo.app.global.install.AppInstallSharedPreferences import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.timestamps.db.AppKeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.KeyTimestampDao import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore import com.duckduckgo.app.onboarding.store.AppUserStageStore import com.duckduckgo.app.onboarding.store.OnboardingSharedPreferences diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt index 638832369028..d3e3b35be4bc 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt @@ -69,7 +69,6 @@ import timber.log.Timber import javax.inject.Inject import kotlin.concurrent.thread - open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleObserver { @Inject diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 210eb3c045b1..cc41c9e0e0e4 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -82,4 +82,4 @@ suspend fun UserStageStore.useOurAppOnboarding(): Boolean { suspend fun UserStageStore.useOurAppNotification(): Boolean { return this.getUserAppStage() == AppStage.USE_OUR_APP_NOTIFICATION -} \ No newline at end of file +} From 09ffd5263a7e2e27297ee152ddbf788a1c224e52 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Sat, 20 Jun 2020 09:44:11 +0100 Subject: [PATCH 09/38] Add pixels --- .../com/duckduckgo/app/browser/BrowserViewModel.kt | 10 +++++++++- .../app/browser/shortcut/ShortcutReceiver.kt | 1 + app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt | 8 ++++---- .../com/duckduckgo/app/global/ViewModelFactory.kt | 3 ++- .../app/notification/model/UseOurAppNotification.kt | 3 ++- .../com/duckduckgo/app/statistics/pixels/Pixel.kt | 12 +++++++++++- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 194e5ab101b8..b609bd80b3d5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -27,6 +27,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.rating.ui.AppEnjoymentDialogFragment import com.duckduckgo.app.browser.rating.ui.GiveFeedbackDialogFragment import com.duckduckgo.app.browser.rating.ui.RateAppDialogFragment +import com.duckduckgo.app.browser.shortcut.ShortcutBuilder import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.ApplicationClearDataState import com.duckduckgo.app.global.SingleLiveEvent @@ -35,6 +36,7 @@ import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity.Companion.RELOAD_RESULT_CODE +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import kotlinx.coroutines.CoroutineScope @@ -48,7 +50,8 @@ class BrowserViewModel( private val queryUrlConverter: OmnibarEntryConverter, private val dataClearer: DataClearer, private val appEnjoymentPromptEmitter: AppEnjoymentPromptEmitter, - private val appEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder + private val appEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder, + private val pixel: Pixel ) : AppEnjoymentDialogFragment.Listener, RateAppDialogFragment.Listener, GiveFeedbackDialogFragment.Listener, @@ -200,5 +203,10 @@ class BrowserViewModel( fun onOpenShortcut(url: String) { tabRepository.selectByUrlOrNewTab(url, queryUrlConverter.convertQueryToUrl(url)) + if (url == ShortcutBuilder.USE_OUR_APP_SHORTCUT_URL) { + pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) + } else { + pixel.fire(Pixel.PixelName.SHORTCUT_OPENED) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index 24670996d5bd..40284b0deaad 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -50,6 +50,7 @@ class ShortcutReceiver @Inject constructor(private val keyTimestampStore: KeyTim GlobalScope.launch(dispatcher.io()) { if (originUrl == USE_OUR_APP_SHORTCUT_URL) { keyTimestampStore.registerTimestamp(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) + pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) } } } diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 4dcde794c84e..6727f771d6ce 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -73,9 +73,9 @@ class UseOurAppCta( @StringRes val okButton: Int = R.string.facebookDialogButtonText, @StringRes val cancelButton: Int = R.string.facebookDialogCancelButtonText, override val ctaId: CtaId = CtaId.USE_OUR_APP, - override val shownPixel: Pixel.PixelName? = null, - override val okPixel: Pixel.PixelName? = null, - override val cancelPixel: Pixel.PixelName? = null + override val shownPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_SHOWN, + override val okPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_OK, + override val cancelPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_CANCELLED ) : Cta, DialogCta { override fun createCta(activity: FragmentActivity): DaxDialog = @@ -98,7 +98,7 @@ class UseOurAppDeletionCta( @StringRes val text: Int = R.string.deletionDialogText, @StringRes val okButton: Int = R.string.daxDialogGotIt, override val ctaId: CtaId = CtaId.USE_OUR_APP_DELETION, - override val shownPixel: Pixel.PixelName? = null, + override val shownPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_DELETE_SHOWN, override val okPixel: Pixel.PixelName? = null, override val cancelPixel: Pixel.PixelName? = null ) : Cta, DialogCta { diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index 3d9b7a5b540c..aacb91d315fc 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -172,7 +172,8 @@ class ViewModelFactory @Inject constructor( queryUrlConverter, dataClearer, appEnjoymentPromptEmitter, - appEnjoymentUserEventRecorder + appEnjoymentUserEventRecorder, + pixel ) } diff --git a/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt index 6968c409f1a9..6e110c6998ee 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt @@ -23,6 +23,7 @@ import com.duckduckgo.app.notification.NotificationHandlerService import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL import com.duckduckgo.app.notification.NotificationRegistrar import com.duckduckgo.app.notification.db.NotificationDao +import com.duckduckgo.app.statistics.pixels.Pixel import timber.log.Timber class UseOurAppNotification( @@ -57,7 +58,7 @@ class UseOurAppSpecification(context: Context) : NotificationSpec { override val description: String = context.getString(R.string.useOurAppNotificationDescription) override val launchButton: String? = null override val closeButton: String? = null - override val pixelSuffix = "uoa" + override val pixelSuffix = Pixel.PixelName.USE_OUR_APP_NOTIFICATION_SUFFIX.pixelName override val autoCancel = true override val bundle: Bundle = Bundle() override val color: Int = R.color.ic_launcher_red_background diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 0debb00b3cc7..137eee0a64af 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -185,7 +185,17 @@ interface Pixel { COOKIE_DATABASE_EXCEPTION_DELETE_ERROR("m_cdb_e_de"), FIREPROOF_WEBSITE_ADDED("m_fw_a"), FIREPROOF_WEBSITE_DELETED("m_fw_d"), - FIREPROOF_WEBSITE_UNDO("m_fw_u") + FIREPROOF_WEBSITE_UNDO("m_fw_u"), + + USE_OUR_APP_NOTIFICATION_SUFFIX("uoa"), + USE_OUR_APP_DIALOG_SHOWN("m_uoa_d"), + USE_OUR_APP_DIALOG_OK("m_uoa_d_ok"), + USE_OUR_APP_DIALOG_CANCELLED("m_uoa_d_c"), + USE_OUR_APP_SHORTCUT_ADDED("m_uoa_s_a"), + USE_OUR_APP_DIALOG_DELETE_SHOWN("m_uoa_dd"), + USE_OUR_APP_SHORTCUT_OPENED("m_sho_uoa_o"), + + SHORTCUT_OPENED("m_sho_o"), } object PixelParameter { From 2241e2d2e825a36d754afbdd7e30dcdeb1c45d53 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Sat, 20 Jun 2020 11:56:59 +0100 Subject: [PATCH 10/38] Add more pixels --- .../app/browser/BrowserTabViewModelTest.kt | 6 +++ .../app/browser/BrowserViewModelTest.kt | 7 +++- .../app/browser/BrowserTabViewModel.kt | 40 ++++++++++++++++++- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 9 +---- .../duckduckgo/app/global/ViewModelFactory.kt | 7 +++- .../com/duckduckgo/app/global/model/Site.kt | 5 +++ .../model/UseOurAppNotification.kt | 6 ++- .../duckduckgo/app/statistics/pixels/Pixel.kt | 4 +- 8 files changed, 71 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index afd858eaf921..454da823adec 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -60,6 +60,7 @@ import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -201,6 +202,9 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockKeyTimestampStore: KeyTimestampStore + @Mock + private lateinit var mockNotificationDao: NotificationDao + private lateinit var mockAutoCompleteApi: AutoCompleteApi private lateinit var ctaViewModel: CtaViewModel @@ -273,6 +277,8 @@ class BrowserTabViewModelTest { pixel = mockPixel, dispatchers = coroutineRule.testDispatcherProvider, fireproofWebsiteDao = fireproofWebsiteDao, + keyTimestampStore = mockKeyTimestampStore, + notificationDao = mockNotificationDao, variantManager = mockVariantManager ) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 4e7b0d35b7e7..6e91800b0991 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -28,6 +28,7 @@ import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.nhaarman.mockitokotlin2.* @@ -71,6 +72,9 @@ class BrowserViewModelTest { @Mock private lateinit var mockAppEnjoymentPromptEmitter: AppEnjoymentPromptEmitter + @Mock + private lateinit var mockPixel: Pixel + private lateinit var testee: BrowserViewModel @Before @@ -84,7 +88,8 @@ class BrowserViewModelTest { queryUrlConverter = mockOmnibarEntryConverter, dataClearer = mockAutomaticDataClearer, appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter, - appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder + appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder, + pixel = mockPixel ) testee.command.observeForever(mockCommandObserver) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 487b41c7bd42..ef5a560746f1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -65,9 +65,10 @@ import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthen import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxDialogCta +import com.duckduckgo.app.cta.ui.DialogCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.HomeTopPanelCta -import com.duckduckgo.app.cta.ui.SecondaryButtonCta +import com.duckduckgo.app.cta.ui.UseOurAppCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.AppUrl @@ -80,7 +81,12 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.domainMatchesUrl +import com.duckduckgo.app.global.model.isUseOurAppDomain +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.timestamps.db.TimestampKey import com.duckduckgo.app.global.toDesktopUri +import com.duckduckgo.app.notification.db.NotificationDao +import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.PrivacyGrade @@ -129,6 +135,8 @@ class BrowserTabViewModel( private val searchCountDao: SearchCountDao, private val pixel: Pixel, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), + private val keyTimestampStore: KeyTimestampStore, + private val notificationDao: NotificationDao, private val variantManager: VariantManager ) : WebViewClientListener, EditBookmarkListener, HttpAuthenticationListener, ViewModel() { @@ -295,6 +303,7 @@ class BrowserTabViewModel( fun onViewReady() { url?.let { + isDomainSameAsUseOurAppSiteDomain() onUserSubmittedQuery(it) } } @@ -575,7 +584,15 @@ class BrowserTabViewModel( private fun pageChanged(url: String, title: String?) { Timber.v("Page changed: $url") - buildSiteFactory(url, title) + + val oldSite = site + + buildSiteFactory(url, title) // Immediately updates site with the new URL + + // Navigating from different website to use our app website + if (!oldSite.isUseOurAppDomain()) { + isDomainSameAsUseOurAppSiteDomain() + } val currentOmnibarViewState = currentOmnibarViewState() omnibarViewState.value = currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false) @@ -611,6 +628,25 @@ class BrowserTabViewModel( registerSiteVisit() } + private fun isDomainSameAsUseOurAppSiteDomain() { + if (site.isUseOurAppDomain()) { + viewModelScope.launch { sendPixelIfUseOurAppSiteVisited() } + } + } + + private suspend fun sendPixelIfUseOurAppSiteVisited() { + withContext(dispatchers.io()) { + val isShortcutAdded = keyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + val isUseOurAppNotificationSeen = notificationDao.exists(UseOurAppNotification.ID) + + if (isShortcutAdded != null) { + pixel.fire(PixelName.UOA_VISITED_AFTER_SHORTCUT) + } else if (isUseOurAppNotificationSeen) { + pixel.fire(PixelName.UOA_VISITED_AFTER_NOTIFICATION) + } + } + } + private fun shouldShowDaxIcon(currentUrl: String?, showPrivacyGrade: Boolean): Boolean { if (!variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SerpHeaderRemoval)) { return false diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index f8efdf78b22a..b8b47aa023ce 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -27,7 +27,7 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain -import com.duckduckgo.app.global.model.domainMatchesUrl +import com.duckduckgo.app.global.model.isUseOurAppDomain import com.duckduckgo.app.global.model.orderedTrackingEntities import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore import com.duckduckgo.app.global.timestamps.db.TimestampKey @@ -203,7 +203,7 @@ class CtaViewModel @Inject constructor( @WorkerThread private suspend fun canShowUseOurAppDeletionDialog(site: Site?): Boolean = - isOnUseOurAppSite(site) && twoDaysSinceShortcutAdded() && !useOurAppDeletionDialogShown() + !useOurAppDeletionDialogShown() && site.isUseOurAppDomain() && twoDaysSinceShortcutAdded() @WorkerThread private suspend fun twoDaysSinceShortcutAdded(): Boolean { @@ -212,11 +212,6 @@ class CtaViewModel @Inject constructor( return (days >= 2) } - private fun isOnUseOurAppSite(site: Site?): Boolean { - if (site == null) return false - return site.domainMatchesUrl("m.facebook.com") || site.domainMatchesUrl("facebook.com") - } - @WorkerThread private suspend fun canShowUseOurAppDialog(): Boolean = useOurAppActive() && !useOurAppDialogShown() diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index aacb91d315fc..5bb93876c465 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -43,10 +43,12 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore import com.duckduckgo.app.icon.api.IconModifier import com.duckduckgo.app.icon.ui.ChangeIconViewModel import com.duckduckgo.app.launch.LaunchViewModel import com.duckduckgo.app.notification.AndroidNotificationScheduler +import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.OnboardingPageManager import com.duckduckgo.app.onboarding.ui.OnboardingViewModel @@ -109,7 +111,8 @@ class ViewModelFactory @Inject constructor( private val onboardingPageManager: OnboardingPageManager, private val appInstallationReferrerStateListener: AppInstallationReferrerStateListener, private val appIconModifier: IconModifier, - private val notificationScheduler: AndroidNotificationScheduler, + private val keyTimestampStore: KeyTimestampStore, + private val notificationDao: NotificationDao, private val dispatcherProvider: DispatcherProvider ) : ViewModelProvider.NewInstanceFactory() { @@ -197,6 +200,8 @@ class ViewModelFactory @Inject constructor( ctaViewModel = ctaViewModel, searchCountDao = searchCountDao, pixel = pixel, + keyTimestampStore = keyTimestampStore, + notificationDao = notificationDao, variantManager = variantManager ) diff --git a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt index 7f7ee6e63ed8..51af8c0cae1f 100644 --- a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt +++ b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt @@ -72,4 +72,9 @@ fun Site.domainMatchesUrl(matchingUrl: String): Boolean { return uri?.baseHost == matchingUrl.toUri().baseHost } +fun Site?.isUseOurAppDomain(): Boolean { + if (this == null) return false + return domainMatchesUrl("m.facebook.com") || domainMatchesUrl("facebook.com") +} + val Site.domain get() = uri?.host diff --git a/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt index 6e110c6998ee..97b2e4eef052 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/model/UseOurAppNotification.kt @@ -31,7 +31,7 @@ class UseOurAppNotification( private val notificationDao: NotificationDao ) : SchedulableNotification { - override val id = "com.duckduckgo.privacytips.useOurApp" + override val id = ID override val launchIntent = NotificationHandlerService.NotificationEvent.USE_OUR_APP override val cancelIntent = CANCEL @@ -47,6 +47,10 @@ class UseOurAppNotification( override suspend fun buildSpecification(): NotificationSpec { return UseOurAppSpecification(context) } + + companion object { + const val ID = "com.duckduckgo.privacytips.useOurApp" + } } class UseOurAppSpecification(context: Context) : NotificationSpec { diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 137eee0a64af..d0a5eb1bade9 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -193,8 +193,10 @@ interface Pixel { USE_OUR_APP_DIALOG_CANCELLED("m_uoa_d_c"), USE_OUR_APP_SHORTCUT_ADDED("m_uoa_s_a"), USE_OUR_APP_DIALOG_DELETE_SHOWN("m_uoa_dd"), - USE_OUR_APP_SHORTCUT_OPENED("m_sho_uoa_o"), + UOA_VISITED_AFTER_SHORTCUT("m_uoa_vas"), + UOA_VISITED_AFTER_NOTIFICATION("m_uoa_van"), + USE_OUR_APP_SHORTCUT_OPENED("m_sho_uoa_o"), SHORTCUT_OPENED("m_sho_o"), } From 713ef52c4d288829da46bf16d45fe7292fc51591 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Sat, 20 Jun 2020 11:57:56 +0100 Subject: [PATCH 11/38] Fix ktlint --- app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index 5bb93876c465..c632c5d62b7d 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -47,7 +47,6 @@ import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore import com.duckduckgo.app.icon.api.IconModifier import com.duckduckgo.app.icon.ui.ChangeIconViewModel import com.duckduckgo.app.launch.LaunchViewModel -import com.duckduckgo.app.notification.AndroidNotificationScheduler import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.OnboardingPageManager From 65a980cc54599520240ef84f547f1c59f45b0b33 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Jun 2020 12:36:21 +0100 Subject: [PATCH 12/38] Add pixel --- .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 9 +++++---- .../main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt | 2 +- .../java/com/duckduckgo/app/statistics/pixels/Pixel.kt | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index ef5a560746f1..3731e77f2c6f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -638,11 +638,12 @@ class BrowserTabViewModel( withContext(dispatchers.io()) { val isShortcutAdded = keyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) val isUseOurAppNotificationSeen = notificationDao.exists(UseOurAppNotification.ID) + val deleteCtaShown = ctaViewModel.useOurAppDeletionDialogShown() - if (isShortcutAdded != null) { - pixel.fire(PixelName.UOA_VISITED_AFTER_SHORTCUT) - } else if (isUseOurAppNotificationSeen) { - pixel.fire(PixelName.UOA_VISITED_AFTER_NOTIFICATION) + when { + deleteCtaShown -> pixel.fire(PixelName.UOA_VISITED_AFTER_DELETE_CTA) + isShortcutAdded != null -> pixel.fire(PixelName.UOA_VISITED_AFTER_SHORTCUT) + isUseOurAppNotificationSeen -> pixel.fire(PixelName.UOA_VISITED_AFTER_NOTIFICATION) } } } diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index b8b47aa023ce..66046748b000 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -273,7 +273,7 @@ class CtaViewModel @Inject constructor( } } - private fun useOurAppDeletionDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION) + fun useOurAppDeletionDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION) private fun useOurAppDialogShown(): Boolean = dismissedCtaDao.exists(CtaId.USE_OUR_APP) diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index d0a5eb1bade9..7fd42f8119da 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -195,6 +195,7 @@ interface Pixel { USE_OUR_APP_DIALOG_DELETE_SHOWN("m_uoa_dd"), UOA_VISITED_AFTER_SHORTCUT("m_uoa_vas"), UOA_VISITED_AFTER_NOTIFICATION("m_uoa_van"), + UOA_VISITED_AFTER_DELETE_CTA("m_uoa_vad"), USE_OUR_APP_SHORTCUT_OPENED("m_sho_uoa_o"), SHORTCUT_OPENED("m_sho_o"), From e2c503c94898ee4c4ab40600d5f0472332aed011 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Jun 2020 13:59:25 +0100 Subject: [PATCH 13/38] Create tests and move use our app domains to the cta companion object --- .../app/browser/BrowserTabViewModelTest.kt | 112 ++++++++++++++++++ .../app/browser/BrowserTabFragment.kt | 4 +- .../app/browser/BrowserTabViewModel.kt | 4 +- .../app/browser/BrowserViewModel.kt | 4 +- .../app/browser/shortcut/ShortcutBuilder.kt | 3 +- .../app/browser/shortcut/ShortcutReceiver.kt | 2 +- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 15 ++- .../duckduckgo/app/di/NotificationModule.kt | 2 +- .../com/duckduckgo/app/global/model/Site.kt | 4 +- .../main/res/values/string-untranslated.xml | 8 +- 10 files changed, 139 insertions(+), 19 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 454da823adec..b75a3d0a366e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -56,11 +56,18 @@ import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta import com.duckduckgo.app.cta.ui.DaxDialogCta import com.duckduckgo.app.cta.ui.HomePanelCta +import com.duckduckgo.app.cta.ui.UseOurAppCta +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_DOMAIN +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory +import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.timestamps.db.TimestampKey.* import com.duckduckgo.app.notification.db.NotificationDao +import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -1686,6 +1693,16 @@ class BrowserTabViewModelTest { assertCommandIssued() } + @Test + fun whenUserClickedUseOurAppCtaOkButtonThenLaunchAddHomeShortcutAndNavigateCommand() { + whenever(mockOmnibarConverter.convertQueryToUrl(USE_OUR_APP_SHORTCUT_URL, null)).thenReturn(USE_OUR_APP_SHORTCUT_URL) + val cta = UseOurAppCta() + setCta(cta) + testee.onUserClickCtaOkButton() + assertCommandIssued() + assertCommandIssued() + } + @Test fun whenSurveyCtaDismissedAndNoOtherCtaPossibleCtaIsNull() = coroutineRule.runBlocking { givenShownCtas(CtaId.DAX_INTRO, CtaId.DAX_END) @@ -1763,6 +1780,22 @@ class BrowserTabViewModelTest { verify(mockSurveyDao).cancelScheduledSurveys() } + @Test + fun whenUserClickedSecondaryCtaButtonThenFirePixel() { + val cta = UseOurAppCta() + setCta(cta) + testee.onUserClickCtaSecondaryButton() + verify(mockPixel).fire(cta.cancelPixel!!, cta.pixelCancelParameters()) + } + + @Test + fun whenUserClickedSecondaryCtaButtonInUseOurAppCtaThenLaunchShowKeyboardCommand() { + val cta = UseOurAppCta() + setCta(cta) + testee.onUserClickCtaSecondaryButton() + assertCommandIssued() + } + @Test fun whenSurrogateDetectedThenSiteUpdated() { givenOneActiveTabSelected() @@ -2022,6 +2055,75 @@ class BrowserTabViewModelTest { assertFalse(browserViewState().showDaxIcon) } + @Test + fun whenViewReadyIfDomainSameAsUseOurAppAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking { + givenUseOurAppSiteSelected() + whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) + testee.onViewReady() + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) + } + + @Test + fun whenViewReadyIfDomainSameAsUseOurAppAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { + givenUseOurAppSiteSelected() + whenever(mockKeyTimestampStore.getTimestamp(USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(USE_OUR_APP_SHORTCUT_ADDED)) + testee.onViewReady() + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) + } + + @Test + fun whenViewReadyIfDomainSameAsUseOurAppAfterDeleteCtaShownThenPixelSent() = coroutineRule.runBlocking { + givenUseOurAppSiteSelected() + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) + testee.onViewReady() + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) + } + + @Test + fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking { + whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) + } + + @Test + fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { + whenever(mockKeyTimestampStore.getTimestamp(USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(USE_OUR_APP_SHORTCUT_ADDED)) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) + } + + @Test + fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterDeleteCtaShownThenPixelSent() = coroutineRule.runBlocking { + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) + } + + @Test + fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking { + givenUseOurAppSiteSelected() + whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) + } + + @Test + fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { + givenUseOurAppSiteSelected() + whenever(mockKeyTimestampStore.getTimestamp(USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(USE_OUR_APP_SHORTCUT_ADDED)) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) + } + + @Test + fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteThenAfterDeleteCtaShownPixelNotSent() = coroutineRule.runBlocking { + givenUseOurAppSiteSelected() + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) + } + private inline fun assertCommandIssued(instanceAssertions: T.() -> Unit = {}) { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) val issuedCommand = commandCaptor.allValues.find { it is T } @@ -2061,6 +2163,16 @@ class BrowserTabViewModelTest { testee.loadData("TAB_ID", "https://example.com", false) } + private fun givenUseOurAppSiteSelected() { + whenever(mockOmnibarConverter.convertQueryToUrl(USE_OUR_APP_DOMAIN, null)).thenReturn(USE_OUR_APP_DOMAIN) + val site: Site = mock() + whenever(site.url).thenReturn(USE_OUR_APP_DOMAIN) + val siteLiveData = MutableLiveData() + siteLiveData.value = site + whenever(mockTabsRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + testee.loadData("TAB_ID", USE_OUR_APP_DOMAIN, false) + } + private fun givenFireproofWebsiteDomain(vararg fireproofWebsitesDomain: String) { fireproofWebsitesDomain.forEach { fireproofWebsiteDao.insert(FireproofWebsiteEntity(domain = it)) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index e877274f7d0d..066466f246a9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -939,7 +939,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun hideKeyboardImmediately() { if (!isHidden) { - Timber.v("Keyboard now hiding immediately") + Timber.v("Keyboard now hiding") omnibarTextInput.hideKeyboard() focusDummy.requestFocus() } @@ -955,7 +955,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun showKeyboardImmediately() { if (!isHidden) { - Timber.v("Keyboard now showing immediately") + Timber.v("Keyboard now showing") omnibarTextInput?.showKeyboard() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 3731e77f2c6f..efdc0c58b1f9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -59,8 +59,6 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage -import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_TITLE -import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.CtaViewModel @@ -69,6 +67,8 @@ import com.duckduckgo.app.cta.ui.DialogCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.HomeTopPanelCta import com.duckduckgo.app.cta.ui.UseOurAppCta +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_TITLE +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.AppUrl diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index b609bd80b3d5..0f4c6cd468d0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -27,7 +27,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.rating.ui.AppEnjoymentDialogFragment import com.duckduckgo.app.browser.rating.ui.GiveFeedbackDialogFragment import com.duckduckgo.app.browser.rating.ui.RateAppDialogFragment -import com.duckduckgo.app.browser.shortcut.ShortcutBuilder +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.ApplicationClearDataState import com.duckduckgo.app.global.SingleLiveEvent @@ -203,7 +203,7 @@ class BrowserViewModel( fun onOpenShortcut(url: String) { tabRepository.selectByUrlOrNewTab(url, queryUrlConverter.convertQueryToUrl(url)) - if (url == ShortcutBuilder.USE_OUR_APP_SHORTCUT_URL) { + if (url == USE_OUR_APP_SHORTCUT_URL) { pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) } else { pixel.fire(Pixel.PixelName.SHORTCUT_OPENED) diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt index 641ebb4363f8..bc85ce775b5b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt @@ -26,6 +26,7 @@ import androidx.core.graphics.drawable.IconCompat import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.BrowserTabViewModel import com.duckduckgo.app.browser.R +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import java.util.UUID import javax.inject.Inject @@ -65,8 +66,6 @@ class ShortcutBuilder @Inject constructor() { } companion object { - const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" - const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" const val USE_OUR_APP_SHORTCUT_ADDED: String = "useOurAppShortcutAdded" const val USE_OUR_APP_SHORTCUT_ADDED_CODE = 9000 diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index 40284b0deaad..632e422fd0ee 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -24,7 +24,7 @@ import android.widget.Toast import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TITLE_ARG import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG -import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.USE_OUR_APP_SHORTCUT_URL +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 6727f771d6ce..55c87b40d6e5 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -69,9 +69,9 @@ interface Cta { } class UseOurAppCta( - @StringRes val text: Int = R.string.facebookDialogText, - @StringRes val okButton: Int = R.string.facebookDialogButtonText, - @StringRes val cancelButton: Int = R.string.facebookDialogCancelButtonText, + @StringRes val text: Int = R.string.useOurAppDialogText, + @StringRes val okButton: Int = R.string.useOurAppDialogButtonText, + @StringRes val cancelButton: Int = R.string.useOurAppDialogCancelButtonText, override val ctaId: CtaId = CtaId.USE_OUR_APP, override val shownPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_SHOWN, override val okPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_OK, @@ -92,10 +92,17 @@ class UseOurAppCta( override fun pixelOkParameters(): Map = emptyMap() override fun pixelShownParameters(): Map = emptyMap() + + companion object { + const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" + const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" + const val USE_OUR_APP_DOMAIN = "facebook.com" + const val USE_OUR_APP_MOBILE_DOMAIN = "m.facebook.com" + } } class UseOurAppDeletionCta( - @StringRes val text: Int = R.string.deletionDialogText, + @StringRes val text: Int = R.string.useOurAppDeletionDialogText, @StringRes val okButton: Int = R.string.daxDialogGotIt, override val ctaId: CtaId = CtaId.USE_OUR_APP_DELETION, override val shownPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_DELETE_SHOWN, diff --git a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt index cf63d2511637..b3acb5ac5a6d 100644 --- a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt @@ -62,7 +62,7 @@ class NotificationModule { } @Provides - fun provideFacebookNotification( + fun provideUseOurAppNotification( context: Context, notificationDao: NotificationDao ): UseOurAppNotification { diff --git a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt index 51af8c0cae1f..c0ffa4341d9e 100644 --- a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt +++ b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt @@ -18,6 +18,8 @@ package com.duckduckgo.app.global.model import android.net.Uri import androidx.core.net.toUri +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_DOMAIN +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_MOBILE_DOMAIN import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.model.SiteFactory.SitePrivacyData import com.duckduckgo.app.privacy.model.HttpsStatus @@ -74,7 +76,7 @@ fun Site.domainMatchesUrl(matchingUrl: String): Boolean { fun Site?.isUseOurAppDomain(): Boolean { if (this == null) return false - return domainMatchesUrl("m.facebook.com") || domainMatchesUrl("facebook.com") + return domainMatchesUrl(USE_OUR_APP_DOMAIN) || domainMatchesUrl(USE_OUR_APP_MOBILE_DOMAIN) } val Site.domain get() = uri?.host diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 0ac11889055d..07f60fb392d2 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -33,12 +33,12 @@ Try It Out - Did you know the Facebook app can make requests for data even when you\'re not using?<br/><br/>Replace the app with a home screen shortcut that opens Facebook in DuckDuckGo. Then delete the Facebook app. - Add Facebook Shortcut - Not Now + Did you know the Facebook app can make requests for data even when you\'re not using?<br/><br/>Replace the app with a home screen shortcut that opens Facebook in DuckDuckGo. Then delete the Facebook app. + Add Facebook Shortcut + Not Now Worried about Facebook tracking you? Here\'s a simply way to reduce it\'s reach. Success! %s has been added to your home screen. - Checking your feed in DuckDuckGo is a great alternative to using the Facebook app!<br/><br/>But if the Facebook app is on your phone, it can make requests for data even when you\'re not using it.<br/><br/>Prevent this by deleting it now! + Checking your feed in DuckDuckGo is a great alternative to using the Facebook app!<br/><br/>But if the Facebook app is on your phone, it can make requests for data even when you\'re not using it.<br/><br/>Prevent this by deleting it now! From 6eee789ec8d3da36f05e37468eef9591b9519ef8 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Jun 2020 15:44:42 +0100 Subject: [PATCH 14/38] Add more tests --- .../app/browser/BrowserViewModelTest.kt | 28 +++++- .../java/com/duckduckgo/app/cta/ui/CtaTest.kt | 36 ++++++++ .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 76 ++++++++++++++++- .../timestamps/db/KeyTimestampDaoTest.kt | 85 +++++++++++++++++++ .../NotificationHandlerServiceTest.kt | 31 +++++++ .../model/UseOurAppNotificationTest.kt | 52 ++++++++++++ .../onboarding/store/AppUserStageStoreTest.kt | 40 ++++++++- 7 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt create mode 100644 app/src/androidTest/java/com/duckduckgo/app/notification/model/UseOurAppNotificationTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 6e91800b0991..f9ebb7a13453 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.Observer import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.BrowserViewModel.Command.DisplayMessage import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter +import com.duckduckgo.app.cta.ui.UseOurAppCta import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions @@ -43,7 +44,6 @@ import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.MockitoAnnotations -import java.util.Arrays.asList class BrowserViewModelTest { @@ -127,7 +127,7 @@ class BrowserViewModelTest { @Test fun whenTabsUpdatedWithTabsThenNewTabNotLaunched() = runBlocking { - testee.onTabsUpdated(asList(TabEntity(TAB_ID, "", "", false, true, 0))) + testee.onTabsUpdated(listOf(TabEntity(TAB_ID, "", "", skipHome = false, viewed = true, position = 0))) verify(mockCommandObserver, never()).onChanged(any()) } @@ -170,6 +170,30 @@ class BrowserViewModelTest { assertTrue(testee.viewState.value!!.hideWebContent) } + @Test + fun whenOpenShortcutThenSelectByUrlOrNewTab() { + val url = "example.com" + whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) + testee.onOpenShortcut(url) + verify(mockTabRepository).selectByUrlOrNewTab(url, url) + } + + @Test + fun whenOpenShortcutIfUrlIsUSeOurAppUrlThenFirePixel() { + val url = UseOurAppCta.USE_OUR_APP_SHORTCUT_URL + whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) + testee.onOpenShortcut(url) + verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) + } + + @Test + fun whenOpenShortcutIfUrlIsNotUSeOurAppUrlThenFirePixel() { + val url = "example.com" + whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) + testee.onOpenShortcut(url) + verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_OPENED) + } + companion object { const val TAB_ID = "TAB_ID" } diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt index 0c4c79f40ab0..77fc4aa3b265 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt @@ -338,6 +338,42 @@ class CtaTest { assertEquals("FacebookwithZero", value) } + @Test + fun whenCtaIsUseOurAppReturnEmptyOkParameters() { + val testee = UseOurAppCta() + assertTrue(testee.pixelOkParameters().isEmpty()) + } + + @Test + fun whenCtaIsUseOurAppReturnEmptyCancelParameters() { + val testee = UseOurAppCta() + assertTrue(testee.pixelCancelParameters().isEmpty()) + } + + @Test + fun whenCtaIsUseOurAppReturnEmptyShownParameters() { + val testee = UseOurAppCta() + assertTrue(testee.pixelShownParameters().isEmpty()) + } + + @Test + fun whenCtaIsUseOurAppDeletionReturnEmptyOkParameters() { + val testee = UseOurAppDeletionCta() + assertTrue(testee.pixelOkParameters().isEmpty()) + } + + @Test + fun whenCtaIsUseOurAppDeletionReturnEmptyCancelParameters() { + val testee = UseOurAppDeletionCta() + assertTrue(testee.pixelCancelParameters().isEmpty()) + } + + @Test + fun whenCtaIsUseOurAppDeletionReturnEmptyShownParameters() { + val testee = UseOurAppDeletionCta() + assertTrue(testee.pixelShownParameters().isEmpty()) + } + private fun site( url: String = "http://www.test.com", uri: Uri? = Uri.parse(url), diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 474de9cf6e4b..117ab2b82718 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -25,10 +25,13 @@ import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta +import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.timestamps.db.TimestampKey import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -417,17 +420,82 @@ class CtaViewModelTest { @Test fun whenRefreshCtaWhileBrowsingWithDaxOnboardingCompletedButNotAllCtasWereShownThenReturnNull() = runBlockingTest { givenShownDaxOnboardingCtas(listOf(CtaId.DAX_INTRO)) - givenDaxOnboardingCompleted() + givenUserIsEstablished() val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true) assertNull(value) } + @Test + fun whenRefreshCtaOnHomeTabAndUseOurAppOnboardingActiveThenUseOurAppCtaShown() = runBlockingTest { + givenUseOurAppActive() + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) + assertTrue(value is UseOurAppCta) + } + + @Test + fun whenRefreshCtaOnHomeTabAndUseOurAppOnboardingActiveAndCtaShownThenReturnNull() = runBlockingTest { + givenUseOurAppActive() + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP)).thenReturn(true) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) + assertNull(value) + } + + @Test + fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenShowUseOurAppDeletionCta() = runBlockingTest { + givenUserIsEstablished() + val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) + assertTrue(value is UseOurAppDeletionCta) + } + + @Test + fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndCtaShownThenReturnNull() = runBlockingTest { + givenUserIsEstablished() + val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) + assertNull(value) + } + + @Test + fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { + givenUserIsEstablished() + val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) + whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) + assertNull(value) + } + + @Test + fun whenRefreshCtaWhileBrowsingAndSiteIsNotUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { + givenUserIsEstablished() + val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = "test")) + assertNull(value) + } + + @Test + fun whenUseOurAppCtaDismissedThenStageCompleted() = coroutineRule.runBlocking { + givenUseOurAppActive() + testee.onUserDismissedCta(UseOurAppCta()) + verify(mockUserStageStore).stageCompleted(AppStage.USE_OUR_APP_ONBOARDING) + } + private suspend fun givenDaxOnboardingActive() { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) } - private suspend fun givenDaxOnboardingCompleted() { + private suspend fun givenUserIsEstablished() { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) } @@ -445,6 +513,10 @@ class CtaViewModelTest { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) } + private suspend fun givenUseOurAppActive() { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_ONBOARDING) + } + private fun site( url: String = "http://www.test.com", uri: Uri? = Uri.parse(url), diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt new file mode 100644 index 000000000000..eaf1410ba9a9 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt @@ -0,0 +1,85 @@ +/* + * 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.global.timestamps.db + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.After +import org.junit.Test + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import java.util.concurrent.TimeUnit + +@ExperimentalCoroutinesApi +class KeyTimestampDaoTest { + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + var coroutineRule = CoroutineTestRule() + + private lateinit var db: AppDatabase + + private lateinit var dao: KeyTimestampDao + + private lateinit var testee: AppKeyTimestampStore + + @Before + fun before() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java).build() + dao = db.keyTimestampDao() + testee = AppKeyTimestampStore(dao, coroutineRule.testDispatcherProvider) + } + + @After + fun after() { + db.close() + } + + @Test + fun whenGetTimestampAndDatabaseEmptyThenReturnNull() = coroutineRule.runBlocking { + val value = testee.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + assertNull(value) + } + + @Test + fun whenInsertingTimestampThenReturnSameTimestamp() = coroutineRule.runBlocking { + val entity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + testee.registerTimestamp(entity) + + assertEquals(entity.timestamp, testee.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + } + + @Test + fun whenInsertingSameTimestampThenReplaceOldTimestampWithTheNew() = coroutineRule.runBlocking { + val entity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + val newEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(1)) + + testee.registerTimestamp(entity) + testee.registerTimestamp(newEntity) + + assertEquals(newEntity.timestamp, testee.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + } +} diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt index 6f1227ef632f..e927c3ed304f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt @@ -19,20 +19,32 @@ package com.duckduckgo.app.notification import android.content.Intent import androidx.core.app.NotificationManagerCompat import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.notification.NotificationHandlerService.Companion.PIXEL_SUFFIX_EXTRA import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CANCEL import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.CLEAR_DATA_LAUNCH +import com.duckduckgo.app.notification.NotificationHandlerService.NotificationEvent.USE_OUR_APP +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before +import org.junit.Rule import org.junit.Test +@ExperimentalCoroutinesApi class NotificationHandlerServiceTest { + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + private var mockPixel: Pixel = mock() + private var mockUserStageStore: UserStageStore = mock() private var testee = NotificationHandlerService() private val context = InstrumentationRegistry.getInstrumentation().targetContext @@ -41,6 +53,8 @@ class NotificationHandlerServiceTest { testee.pixel = mockPixel testee.context = context testee.notificationManager = NotificationManagerCompat.from(context) + testee.userStageStore = mockUserStageStore + testee.dispatcher = coroutinesTestRule.testDispatcherProvider } @Test @@ -60,4 +74,21 @@ class NotificationHandlerServiceTest { testee.onHandleIntent(intent) verify(mockPixel).fire(eq("mnot_c_abc"), any(), any()) } + + @Test + fun whenIntentIsUseOurAppThenCorrespondingPixelIsFired() { + val intent = Intent(context, NotificationHandlerService::class.java) + intent.type = USE_OUR_APP + intent.putExtra(PIXEL_SUFFIX_EXTRA, "abc") + testee.onHandleIntent(intent) + verify(mockPixel).fire(eq("mnot_l_abc"), any(), any()) + } + + @Test + fun whenIntentIsUseOurAppThenRegisterInUseOurAppOnboardingStage() = coroutinesTestRule.runBlocking { + val intent = Intent(context, NotificationHandlerService::class.java) + intent.type = USE_OUR_APP + testee.onHandleIntent(intent) + verify(mockUserStageStore).registerInStage(AppStage.USE_OUR_APP_ONBOARDING) + } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/model/UseOurAppNotificationTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/model/UseOurAppNotificationTest.kt new file mode 100644 index 000000000000..88e55d3f2aa9 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/model/UseOurAppNotificationTest.kt @@ -0,0 +1,52 @@ +/* + * 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.notification.model + +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.notification.db.NotificationDao +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.runBlocking +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class UseOurAppNotificationTest { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val notificationsDao: NotificationDao = mock() + + private lateinit var testee: UseOurAppNotification + + @Before + fun before() { + testee = UseOurAppNotification(context, notificationsDao) + } + + @Test + fun whenNotificationNotSeenThenCanShowIsTrue() = runBlocking { + whenever(notificationsDao.exists(any())).thenReturn(false) + assertTrue(testee.canShow()) + } + + @Test + fun whenNotificationAlreadySeenThenCanShowIsFalse() = runBlocking { + whenever(notificationsDao.exists(any())).thenReturn(true) + assertFalse(testee.canShow()) + } +} diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt index a8ac1146d14f..c392d7a40563 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt @@ -20,6 +20,7 @@ import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.runBlocking import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertEquals @@ -33,9 +34,9 @@ class AppUserStageStoreTest { var coroutineRule = CoroutineTestRule() private val userStageDao = mock() - private val addToHomeCapabilityDetector = mock() + private val mockAddToHomeCapabilityDetector = mock() - private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, addToHomeCapabilityDetector) + private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, mockAddToHomeCapabilityDetector) @Test fun whenGetUserAppStageThenRetunCurrentStage() = coroutineRule.runBlocking { @@ -56,7 +57,7 @@ class AppUserStageStoreTest { } @Test - fun whenStageDaxOnboardingCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { + fun whenStageDaxOnboardingCompletedAndNotAbleToAddShortcutsThenStageEstablishedReturned() = coroutineRule.runBlocking { givenCurrentStage(AppStage.DAX_ONBOARDING) val nextStage = testee.stageCompleted(AppStage.DAX_ONBOARDING) @@ -64,6 +65,33 @@ class AppUserStageStoreTest { assertEquals(AppStage.ESTABLISHED, nextStage) } + @Test + fun whenStageDaxOnboardingCompletedAndAbleToAddShortcutsThenStageUseOurAppNotificationReturned() = coroutineRule.runBlocking { + givenCurrentStage(AppStage.DAX_ONBOARDING) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) + val nextStage = testee.stageCompleted(AppStage.DAX_ONBOARDING) + + assertEquals(AppStage.USE_OUR_APP_NOTIFICATION, nextStage) + } + + @Test + fun whenStageUseOurAppNotificationCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { + givenCurrentStage(AppStage.USE_OUR_APP_NOTIFICATION) + + val nextStage = testee.stageCompleted(AppStage.USE_OUR_APP_NOTIFICATION) + + assertEquals(AppStage.ESTABLISHED, nextStage) + } + + @Test + fun whenStageUseOurAppOnboardingCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { + givenCurrentStage(AppStage.USE_OUR_APP_ONBOARDING) + + val nextStage = testee.stageCompleted(AppStage.USE_OUR_APP_ONBOARDING) + + assertEquals(AppStage.ESTABLISHED, nextStage) + } + @Test fun whenStageEstablishedCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { givenCurrentStage(AppStage.ESTABLISHED) @@ -73,6 +101,12 @@ class AppUserStageStoreTest { assertEquals(AppStage.ESTABLISHED, nextStage) } + @Test + fun whenRegisterInStageThenUpdateUserStageInDao() = coroutineRule.runBlocking { + testee.registerInStage(AppStage.USE_OUR_APP_ONBOARDING) + verify(userStageDao).updateUserStage(AppStage.USE_OUR_APP_ONBOARDING) + } + private suspend fun givenCurrentStage(appStage: AppStage) { whenever(userStageDao.currentUserAppStage()).thenReturn(UserStage(appStage = appStage)) } From 8b14b1031df1141f5caf6dfae322510c29bb6981 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 11:28:08 +0100 Subject: [PATCH 15/38] Add tests --- .../app/browser/BrowserViewModelTest.kt | 11 +- .../AndroidNotificationSchedulerTest.kt | 117 +++++++++++++----- .../NotificationHandlerServiceTest.kt | 1 + .../app/tabs/model/TabDataRepositoryTest.kt | 21 ++++ .../app/browser/BrowserViewModel.kt | 2 +- .../duckduckgo/app/di/NotificationModule.kt | 2 - .../AndroidNotificationScheduler.kt | 5 +- .../com/duckduckgo/app/tabs/db/TabsDao.kt | 2 +- .../app/tabs/model/TabDataRepository.kt | 16 +-- .../app/tabs/model/TabRepository.kt | 2 +- 10 files changed, 125 insertions(+), 54 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index f9ebb7a13453..ad4ba19a79a9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.browser import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer +import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.BrowserViewModel.Command.DisplayMessage import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter @@ -29,10 +30,12 @@ import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity +import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals @@ -45,12 +48,16 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.MockitoAnnotations +@ExperimentalCoroutinesApi class BrowserViewModelTest { @get:Rule @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + @Mock private lateinit var mockCommandObserver: Observer @@ -171,11 +178,11 @@ class BrowserViewModelTest { } @Test - fun whenOpenShortcutThenSelectByUrlOrNewTab() { + fun whenOpenShortcutThenSelectByUrlOrNewTab() = coroutinesTestRule.runBlocking { val url = "example.com" whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) testee.onOpenShortcut(url) - verify(mockTabRepository).selectByUrlOrNewTab(url, url) + verify(mockTabRepository).selectByUrlOrNewTab(url) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt index 52508c9d47a3..f5b4a1c0b61f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt @@ -32,7 +32,9 @@ import com.duckduckgo.app.notification.NotificationScheduler.DripA1NotificationW import com.duckduckgo.app.notification.NotificationScheduler.DripA2NotificationWorker import com.duckduckgo.app.notification.NotificationScheduler.DripB1NotificationWorker import com.duckduckgo.app.notification.NotificationScheduler.DripB2NotificationWorker +import com.duckduckgo.app.notification.NotificationScheduler.UseOurAppNotificationWorker import com.duckduckgo.app.notification.model.SchedulableNotification +import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager @@ -46,6 +48,7 @@ import com.duckduckgo.app.statistics.VariantManager.VariantFeature.Day3ClearData import com.duckduckgo.app.statistics.VariantManager.Companion.DEFAULT_VARIANT import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking @@ -62,14 +65,13 @@ class AndroidNotificationSchedulerTest { var coroutinesTestRule = CoroutineTestRule() private val variantManager: VariantManager = mock() - private val userStageStore: UserStageStore = mock() + private val mockUserStageStore: UserStageStore = mock() private val clearNotification: SchedulableNotification = mock() private val privacyNotification: SchedulableNotification = mock() private val dripA1Notification: SchedulableNotification = mock() private val dripA2Notification: SchedulableNotification = mock() private val dripB1Notification: SchedulableNotification = mock() private val dripB2Notification: SchedulableNotification = mock() - private val ourAppNotification: SchedulableNotification = mock() private val context = InstrumentationRegistry.getInstrumentation().targetContext private lateinit var workManager: WorkManager @@ -87,9 +89,8 @@ class AndroidNotificationSchedulerTest { dripA2Notification, dripB1Notification, dripB2Notification, - ourAppNotification, variantManager, - userStageStore + mockUserStageStore ) } @@ -111,7 +112,7 @@ class AndroidNotificationSchedulerTest { whenever(clearNotification.canShow()).thenReturn(true) testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(PrivacyNotificationWorker::class.jvmName) + assertNotificationScheduled(PrivacyNotificationWorker::class.jvmName) } @Test @@ -121,7 +122,7 @@ class AndroidNotificationSchedulerTest { whenever(clearNotification.canShow()).thenReturn(false) testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(PrivacyNotificationWorker::class.jvmName) + assertNotificationScheduled(PrivacyNotificationWorker::class.jvmName) } @Test @@ -131,7 +132,7 @@ class AndroidNotificationSchedulerTest { whenever(clearNotification.canShow()).thenReturn(true) testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(ClearDataNotificationWorker::class.jvmName) + assertNotificationScheduled(ClearDataNotificationWorker::class.jvmName) } @Test @@ -141,7 +142,7 @@ class AndroidNotificationSchedulerTest { whenever(clearNotification.canShow()).thenReturn(false) testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } // Drip A1 @@ -153,7 +154,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripA1NotificationWorker::class.jvmName) + assertNotificationScheduled(DripA1NotificationWorker::class.jvmName) } @Test @@ -164,7 +165,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(ClearDataNotificationWorker::class.jvmName) + assertNotificationScheduled(ClearDataNotificationWorker::class.jvmName) } @Test @@ -175,7 +176,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } @Test @@ -186,7 +187,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripA1NotificationWorker::class.jvmName) + assertNotificationScheduled(DripA1NotificationWorker::class.jvmName) } // Drip A2 @@ -198,7 +199,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripA2NotificationWorker::class.jvmName) + assertNotificationScheduled(DripA2NotificationWorker::class.jvmName) } @Test @@ -209,7 +210,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(ClearDataNotificationWorker::class.jvmName) + assertNotificationScheduled(ClearDataNotificationWorker::class.jvmName) } @Test @@ -220,7 +221,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } @Test @@ -231,7 +232,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripA2NotificationWorker::class.jvmName) + assertNotificationScheduled(DripA2NotificationWorker::class.jvmName) } // Drip B1 @@ -244,7 +245,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripB1NotificationWorker::class.jvmName) + assertNotificationScheduled(DripB1NotificationWorker::class.jvmName) } @Test @@ -255,7 +256,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(ClearDataNotificationWorker::class.jvmName) + assertNotificationScheduled(ClearDataNotificationWorker::class.jvmName) } @Test @@ -266,7 +267,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } @Test @@ -277,7 +278,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripB1NotificationWorker::class.jvmName) + assertNotificationScheduled(DripB1NotificationWorker::class.jvmName) } // Drip B2 @@ -290,7 +291,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripB2NotificationWorker::class.jvmName) + assertNotificationScheduled(DripB2NotificationWorker::class.jvmName) } @Test @@ -301,7 +302,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(ClearDataNotificationWorker::class.jvmName) + assertNotificationScheduled(ClearDataNotificationWorker::class.jvmName) } @Test @@ -312,7 +313,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } @Test @@ -323,7 +324,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(DripB2NotificationWorker::class.jvmName) + assertNotificationScheduled(DripB2NotificationWorker::class.jvmName) } // Control @@ -335,7 +336,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(PrivacyNotificationWorker::class.jvmName) + assertNotificationScheduled(PrivacyNotificationWorker::class.jvmName) } @Test @@ -346,7 +347,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(PrivacyNotificationWorker::class.jvmName) + assertNotificationScheduled(PrivacyNotificationWorker::class.jvmName) } @Test @@ -357,7 +358,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertUnusedAppNotificationScheduled(ClearDataNotificationWorker::class.jvmName) + assertNotificationScheduled(ClearDataNotificationWorker::class.jvmName) } @Test @@ -368,7 +369,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } // Null variant @@ -384,7 +385,7 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() } @Test @@ -399,7 +400,43 @@ class AndroidNotificationSchedulerTest { testee.scheduleNextNotification() - assertNoUnusedAppNotificationScheduled() + assertNoNotificationScheduled() + } + + @Test + fun whenStageIsUseOurAppNotificationThenNotificationScheduled() = runBlocking { + givenStageIsUserOurAppNotification() + + testee.scheduleNextNotification() + + assertNotificationScheduled(UseOurAppNotificationWorker::class.jvmName, NotificationScheduler.USE_OUR_APP_WORK_REQUEST_TAG) + } + + @Test + fun whenStageIsUseOurAppNotificationAndNotificationScheduledThenStageCompleted() = runBlocking { + givenStageIsUserOurAppNotification() + + testee.scheduleNextNotification() + + verify(mockUserStageStore).stageCompleted(AppStage.USE_OUR_APP_NOTIFICATION) + } + + @Test + fun whenStageIsUseOurAppNotificationThenNoNotificationScheduled() = runBlocking { + givenStageIsEstablished() + + testee.scheduleNextNotification() + + assertNoNotificationScheduled(NotificationScheduler.USE_OUR_APP_WORK_REQUEST_TAG) + } + + @Test + fun whenStageIsNotUseOurAppNotificationThenNoNotificationScheduled() = runBlocking { + givenStageIsEstablished() + + testee.scheduleNextNotification() + + assertNoNotificationScheduled(NotificationScheduler.USE_OUR_APP_WORK_REQUEST_TAG) } private fun setDripA1Variant() { @@ -438,12 +475,24 @@ class AndroidNotificationSchedulerTest { ) } - private fun assertUnusedAppNotificationScheduled(workerName: String) { - assertTrue(getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG).any { it.tags.contains(workerName) }) + private suspend fun givenStageIsUserOurAppNotification() { + whenever(privacyNotification.canShow()).thenReturn(false) + whenever(clearNotification.canShow()).thenReturn(false) + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_NOTIFICATION) + } + + private suspend fun givenStageIsEstablished() { + whenever(privacyNotification.canShow()).thenReturn(false) + whenever(clearNotification.canShow()).thenReturn(false) + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) + } + + private fun assertNotificationScheduled(workerName: String, tag: String = NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG) { + assertTrue(getScheduledWorkers(tag).any { it.tags.contains(workerName) }) } - private fun assertNoUnusedAppNotificationScheduled() { - assertTrue(getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG).isEmpty()) + private fun assertNoNotificationScheduled(tag: String = NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG) { + assertTrue(getScheduledWorkers(tag).isEmpty()) } private fun getScheduledWorkers(tag: String): List { diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt index e927c3ed304f..11388275dcc3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt @@ -88,6 +88,7 @@ class NotificationHandlerServiceTest { fun whenIntentIsUseOurAppThenRegisterInUseOurAppOnboardingStage() = coroutinesTestRule.runBlocking { val intent = Intent(context, NotificationHandlerService::class.java) intent.type = USE_OUR_APP + intent.putExtra(PIXEL_SUFFIX_EXTRA, "abc") testee.onHandleIntent(intent) verify(mockUserStageStore).registerInStage(AppStage.USE_OUR_APP_ONBOARDING) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt index 7eb604824a7c..4889c0c309b9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt @@ -229,6 +229,27 @@ class TabDataRepositoryTest { assertTrue(captor.firstValue.position == 1) } + @Test + fun whenSelectByUrlOrNewTabIfUrlAlreadyExistedInATabThenSelectTheTab() = runBlocking { + val url = "http://www.example.com" + + whenever(mockDao.selectTabByUrl(url)).thenReturn("tabid") + + testee.selectByUrlOrNewTab(url) + + verify(mockDao).insertTabSelection(TabSelectionEntity(tabId = "tabid")) + } + + @Test + fun whenSelectByUrlOrNewTabIfUrlNotExistedInATabThenAddNewTab() = runBlocking { + whenever(mockDao.tabs()).thenReturn(emptyList()) + + testee.selectByUrlOrNewTab("http://www.example.com") + + val captor = argumentCaptor() + verify(mockDao).addAndSelectTab(captor.capture()) + } + companion object { const val TAB_ID = "abcd" } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 0f4c6cd468d0..1d86a3848c84 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -202,7 +202,7 @@ class BrowserViewModel( } fun onOpenShortcut(url: String) { - tabRepository.selectByUrlOrNewTab(url, queryUrlConverter.convertQueryToUrl(url)) + launch { tabRepository.selectByUrlOrNewTab(queryUrlConverter.convertQueryToUrl(url)) } if (url == USE_OUR_APP_SHORTCUT_URL) { pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) } else { diff --git a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt index b3acb5ac5a6d..e67f7f83776b 100644 --- a/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt @@ -160,7 +160,6 @@ class NotificationModule { @Named("dripA2Notification") dripA2Notification: WebsiteNotification, @Named("dripB1Notification") dripB1Notification: AppFeatureNotification, @Named("dripB2Notification") dripB2Notification: AppFeatureNotification, - useOurAppNotification: UseOurAppNotification, variantManager: VariantManager, stageStore: UserStageStore ): AndroidNotificationScheduler { @@ -172,7 +171,6 @@ class NotificationModule { dripA2Notification, dripB1Notification, dripB2Notification, - useOurAppNotification, variantManager, stageStore ) diff --git a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt index 67ea83be28b7..08aff9b2b18d 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt @@ -54,17 +54,16 @@ class NotificationScheduler( private val dripA2Notification: SchedulableNotification, private val dripB1Notification: SchedulableNotification, private val dripB2Notification: SchedulableNotification, - private val useOurAppNotification: SchedulableNotification, private val variantManager: VariantManager, private val userStageStore: UserStageStore ) : AndroidNotificationScheduler { override suspend fun scheduleNextNotification() { - scheduleFacebookNotification() + scheduleUseOurAppNotification() scheduleInactiveUserNotifications() } - private suspend fun scheduleFacebookNotification() { + private suspend fun scheduleUseOurAppNotification() { if (userStageStore.useOurAppNotification()) { val operation = scheduleUniqueNotification( OneTimeWorkRequestBuilder(), diff --git a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt index 53bfb3a2bf08..04a8bc29d419 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt @@ -45,7 +45,7 @@ abstract class TabsDao { abstract fun tab(tabId: String): TabEntity? @Query("select tabId from tabs where url LIKE :url") - abstract fun selectTabByUrl(url: String): String? + abstract suspend fun selectTabByUrl(url: String): String? @Insert(onConflict = OnConflictStrategy.REPLACE) abstract fun insertTab(tab: TabEntity) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index 8c493b321746..ae37751c3454 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -85,16 +85,12 @@ class TabDataRepository @Inject constructor( } } - override fun selectByUrlOrNewTab(url: String, query: String) { - databaseExecutor().scheduleDirect { - val tabId = tabsDao.selectTabByUrl(url) - GlobalScope.launch { - if (tabId != null) { - select(tabId) - } else { - add(query, skipHome = true, isDefaultTab = false) - } - } + override suspend fun selectByUrlOrNewTab(url: String) { + val tabId = tabsDao.selectTabByUrl(url) + if (tabId != null) { + select(tabId) + } else { + add(url, skipHome = true, isDefaultTab = false) } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index 188232309caa..f0c1ee09bfaa 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -50,5 +50,5 @@ interface TabRepository { fun updateTabPreviewImage(tabId: String, fileName: String?) - fun selectByUrlOrNewTab(url: String, query: String) + suspend fun selectByUrlOrNewTab(url: String) } From c4b1a34129bf9e244ff575b4e2ef402b036697ed Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 12:43:36 +0100 Subject: [PATCH 16/38] Add ShortcutReceiver tests --- .../browser/shortcut/ShortcutReceiverTest.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt new file mode 100644 index 000000000000..4f5b66f415d1 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -0,0 +1,91 @@ +/* + * 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.browser.shortcut + +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.cta.ui.UseOurAppCta +import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.runBlocking +import com.duckduckgo.app.statistics.pixels.Pixel +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class ShortcutReceiverTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private val mockKeyTimestampStore: KeyTimestampStore = mock() + private val mockPixel: Pixel = mock() + private lateinit var testee: ShortcutReceiver + private val context = InstrumentationRegistry.getInstrumentation().targetContext + + @Before + fun before() { + + testee = ShortcutReceiver(mockKeyTimestampStore, coroutinesTestRule.testDispatcherProvider, mockPixel) + } + + @Test + fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenRegisterTimestamp() = coroutinesTestRule.runBlocking { + val intent = Intent() + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, UseOurAppCta.USE_OUR_APP_SHORTCUT_URL) + intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") + testee.onReceive(context, intent) + + verify(mockKeyTimestampStore).registerTimestamp(any()) + } + + @Test + fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenFirePixel() { + val intent = Intent() + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, UseOurAppCta.USE_OUR_APP_SHORTCUT_URL) + intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") + testee.onReceive(context, intent) + + verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) + } + + @Test + fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotRegisterTimestamp() = coroutinesTestRule.runBlocking { + val intent = Intent() + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") + intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") + testee.onReceive(context, intent) + + verify(mockKeyTimestampStore, never()).registerTimestamp(any()) + } + + @Test + fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotFirePixel() { + val intent = Intent() + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") + intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") + testee.onReceive(context, intent) + + verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) + } +} From 2fef4dc892cb27b944c074ca365cffd066b3703d Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 13:18:04 +0100 Subject: [PATCH 17/38] Amend notification schedule --- .../duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt | 4 ++-- .../app/notification/AndroidNotificationScheduler.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index 4f5b66f415d1..dd58e0d245d8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.browser.shortcut +import android.content.Context import android.content.Intent import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule @@ -41,11 +42,10 @@ class ShortcutReceiverTest { private val mockKeyTimestampStore: KeyTimestampStore = mock() private val mockPixel: Pixel = mock() private lateinit var testee: ShortcutReceiver - private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val context: Context = mock() @Before fun before() { - testee = ShortcutReceiver(mockKeyTimestampStore, coroutinesTestRule.testDispatcherProvider, mockPixel) } diff --git a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt index 08aff9b2b18d..8fef0e7a2d23 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt @@ -67,8 +67,8 @@ class NotificationScheduler( if (userStageStore.useOurAppNotification()) { val operation = scheduleUniqueNotification( OneTimeWorkRequestBuilder(), - 15, - TimeUnit.SECONDS, + 1, + TimeUnit.DAYS, USE_OUR_APP_WORK_REQUEST_TAG ) try { From 10b05c5cbe0ae1a4f92332a8ab6f7148088ea444 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 13:52:41 +0100 Subject: [PATCH 18/38] Fix ktlint --- .../com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index dd58e0d245d8..460ec182a2bd 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.browser.shortcut import android.content.Context import android.content.Intent -import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.cta.ui.UseOurAppCta import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore From accdf65cff4fb3e3064881640462e1baa5db57f3 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 14:24:03 +0100 Subject: [PATCH 19/38] Fix tests --- .../app/browser/shortcut/ShortcutReceiverTest.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index 460ec182a2bd..e73703ec9d5c 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -16,7 +16,6 @@ package com.duckduckgo.app.browser.shortcut -import android.content.Context import android.content.Intent import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.cta.ui.UseOurAppCta @@ -41,7 +40,6 @@ class ShortcutReceiverTest { private val mockKeyTimestampStore: KeyTimestampStore = mock() private val mockPixel: Pixel = mock() private lateinit var testee: ShortcutReceiver - private val context: Context = mock() @Before fun before() { @@ -53,7 +51,7 @@ class ShortcutReceiverTest { val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, UseOurAppCta.USE_OUR_APP_SHORTCUT_URL) intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") - testee.onReceive(context, intent) + testee.onReceive(null, intent) verify(mockKeyTimestampStore).registerTimestamp(any()) } @@ -63,7 +61,7 @@ class ShortcutReceiverTest { val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, UseOurAppCta.USE_OUR_APP_SHORTCUT_URL) intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") - testee.onReceive(context, intent) + testee.onReceive(null, intent) verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) } @@ -73,7 +71,7 @@ class ShortcutReceiverTest { val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") - testee.onReceive(context, intent) + testee.onReceive(null, intent) verify(mockKeyTimestampStore, never()).registerTimestamp(any()) } @@ -83,7 +81,7 @@ class ShortcutReceiverTest { val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") - testee.onReceive(context, intent) + testee.onReceive(null, intent) verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) } From e0def0cfe7a7a46bf2162c767e63e7a993431b9b Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 16:34:11 +0100 Subject: [PATCH 20/38] Add new logic for users with hide tips enable and tests --- .../app/browser/BrowserTabViewModelTest.kt | 19 ++++----- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 20 +++++++++ .../app/global/db/AppDatabaseTest.kt | 42 ++++++++++++++++++- .../onboarding/store/AppUserStageStoreTest.kt | 14 ++++++- .../systemsearch/SystemSearchViewModelTest.kt | 4 +- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 4 +- .../com/duckduckgo/app/di/DatabaseModule.kt | 10 ++++- .../duckduckgo/app/global/db/AppDatabase.kt | 13 +++++- .../app/onboarding/store/UserStageStore.kt | 6 ++- 9 files changed, 109 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 3aef2cd459f7..a8469efbf0ce 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -57,21 +57,17 @@ import com.duckduckgo.app.cta.ui.DaxBubbleCta import com.duckduckgo.app.cta.ui.DaxDialogCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.UseOurAppCta -import com.duckduckgo.app.cta.ui.UseOurAppCta import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_DOMAIN import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory -import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity import com.duckduckgo.app.global.timestamps.db.TimestampKey import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.TimestampKey.* import com.duckduckgo.app.notification.db.NotificationDao -import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -2092,21 +2088,21 @@ class BrowserTabViewModelTest { @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking { whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) - testee.pageRefreshed(UseOurAppCta.USE_OUR_APP_DOMAIN) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) } @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) - testee.pageRefreshed(UseOurAppCta.USE_OUR_APP_DOMAIN) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterDeleteCtaShownThenPixelSent() = coroutineRule.runBlocking { whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) - testee.pageRefreshed(UseOurAppCta.USE_OUR_APP_DOMAIN) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) } @@ -2114,15 +2110,16 @@ class BrowserTabViewModelTest { fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) - testee.pageRefreshed(UseOurAppCta.USE_OUR_APP_DOMAIN) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) } @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) - testee.pageRefreshed(UseOurAppCta.USE_OUR_APP_DOMAIN) + val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @@ -2130,7 +2127,7 @@ class BrowserTabViewModelTest { fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteThenAfterDeleteCtaShownPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) - testee.pageRefreshed(UseOurAppCta.USE_OUR_APP_DOMAIN) + testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 117ab2b82718..d3efa0372baa 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -434,6 +434,15 @@ class CtaViewModelTest { assertTrue(value is UseOurAppCta) } + @Test + fun whenRefreshCtaOnHomeTabAndUseOurAppOnboardingActiveAndHideTipsIsTrueThenReturnNull() = runBlockingTest { + givenUseOurAppActive() + whenever(mockSettingsDataStore.hideTips).thenReturn(true) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false) + assertNull(value) + } + @Test fun whenRefreshCtaOnHomeTabAndUseOurAppOnboardingActiveAndCtaShownThenReturnNull() = runBlockingTest { givenUseOurAppActive() @@ -453,6 +462,17 @@ class CtaViewModelTest { assertTrue(value is UseOurAppDeletionCta) } + @Test + fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndHideTipsIsTrueThenReturnNull() = runBlockingTest { + givenUserIsEstablished() + val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockSettingsDataStore.hideTips).thenReturn(true) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) + assertNull(value) + } + @Test fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndCtaShownThenReturnNull() = runBlockingTest { givenUserIsEstablished() diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 75bdcddcc196..58b30014dac2 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -29,10 +29,12 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.blockingObserve +import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSource import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.runBlocking +import com.duckduckgo.app.settings.db.SettingsDataStore import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock @@ -58,8 +60,10 @@ class AppDatabaseTest { val testHelper = MigrationTestHelper(getInstrumentation(), AppDatabase::class.qualifiedName, FrameworkSQLiteOpenHelperFactory()) private val context = mock() + private val mockSettingsDataStore = mock() + private val mockAddToHomeCapabilityDetector = mock() - private val migrationsProvider: MigrationsProvider = MigrationsProvider(context) + private val migrationsProvider: MigrationsProvider = MigrationsProvider(context, mockSettingsDataStore, mockAddToHomeCapabilityDetector) @Before fun setup() { @@ -226,7 +230,9 @@ class AppDatabaseTest { } @Test - fun whenMigratingFromVersion21To22IfUserIsEstablishedThenMigrateToNotification() { + fun whenMigratingFromVersion21To22IfUserIsEstablishedAndConditionsAreMetThenMigrateToNotification() { + whenever(mockSettingsDataStore.hideTips).thenReturn(false) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) val values: ContentValues = ContentValues().apply { put("key", 1) put("appStage", AppStage.ESTABLISHED.name) @@ -239,6 +245,38 @@ class AppDatabaseTest { } } + @Test + fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHideTipsIsTrueThenDoNotMigrateToNotification() { + whenever(mockSettingsDataStore.hideTips).thenReturn(true) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) + val values: ContentValues = ContentValues().apply { + put("key", 1) + put("appStage", AppStage.ESTABLISHED.name) + } + testHelper.createDatabase(TEST_DB_NAME, 21).use { + it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) + val stage = getUserStage(it) + assertEquals(AppStage.ESTABLISHED.name, stage) + } + } + + @Test + fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHomeShortcutNotSupportedThenDoNotMigrateToNotification() { + whenever(mockSettingsDataStore.hideTips).thenReturn(false) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(false) + val values: ContentValues = ContentValues().apply { + put("key", 1) + put("appStage", AppStage.ESTABLISHED.name) + } + testHelper.createDatabase(TEST_DB_NAME, 21).use { + it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) + val stage = getUserStage(it) + assertEquals(AppStage.ESTABLISHED.name, stage) + } + } + private fun getUserStage(database: SupportSQLiteDatabase): String { var stage = "" database.query("SELECT appStage from userStage limit 1").use { diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt index c392d7a40563..5efeb5e623e4 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.onboarding.store import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.runBlocking +import com.duckduckgo.app.settings.db.SettingsDataStore import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever @@ -35,8 +36,9 @@ class AppUserStageStoreTest { private val userStageDao = mock() private val mockAddToHomeCapabilityDetector = mock() + private val mockSettingsDataStore = mock() - private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, mockAddToHomeCapabilityDetector) + private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, mockAddToHomeCapabilityDetector, mockSettingsDataStore) @Test fun whenGetUserAppStageThenRetunCurrentStage() = coroutineRule.runBlocking { @@ -74,6 +76,16 @@ class AppUserStageStoreTest { assertEquals(AppStage.USE_OUR_APP_NOTIFICATION, nextStage) } + @Test + fun whenStageDaxOnboardingCompletedAndAbleToAddShortcutsButHideTipsIsTrueThenStageEstablishedReturned() = coroutineRule.runBlocking { + givenCurrentStage(AppStage.DAX_ONBOARDING) + whenever(mockSettingsDataStore.hideTips).thenReturn(true) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) + val nextStage = testee.stageCompleted(AppStage.DAX_ONBOARDING) + + assertEquals(AppStage.ESTABLISHED, nextStage) + } + @Test fun whenStageUseOurAppNotificationCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { givenCurrentStage(AppStage.USE_OUR_APP_NOTIFICATION) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index f92b679e7949..822e58d21ecd 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -27,6 +27,7 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.A import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.runBlocking +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command @@ -59,6 +60,7 @@ class SystemSearchViewModelTest { private val mockAutoComplete: AutoComplete = mock() private val mockPixel: Pixel = mock() private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector = mock() + private val settingsDataStore: SettingsDataStore = mock() private val commandObserver: Observer = mock() private val commandCaptor = argumentCaptor() @@ -275,7 +277,7 @@ class SystemSearchViewModelTest { override suspend fun currentUserAppStage() = UserStage(appStage = AppStage.NEW) override fun insert(userStage: UserStage) {} } - return AppUserStageStore(emptyUserStageDao, coroutineRule.testDispatcherProvider, addToHomeCapabilityDetector) + return AppUserStageStore(emptyUserStageDao, coroutineRule.testDispatcherProvider, addToHomeCapabilityDetector, settingsDataStore) } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 66046748b000..29bab4426971 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -203,7 +203,7 @@ class CtaViewModel @Inject constructor( @WorkerThread private suspend fun canShowUseOurAppDeletionDialog(site: Site?): Boolean = - !useOurAppDeletionDialogShown() && site.isUseOurAppDomain() && twoDaysSinceShortcutAdded() + !settingsDataStore.hideTips && !useOurAppDeletionDialogShown() && site.isUseOurAppDomain() && twoDaysSinceShortcutAdded() @WorkerThread private suspend fun twoDaysSinceShortcutAdded(): Boolean { @@ -213,7 +213,7 @@ class CtaViewModel @Inject constructor( } @WorkerThread - private suspend fun canShowUseOurAppDialog(): Boolean = useOurAppActive() && !useOurAppDialogShown() + private suspend fun canShowUseOurAppDialog(): Boolean = !settingsDataStore.hideTips && useOurAppActive() && !useOurAppDialogShown() @WorkerThread private fun canShowWidgetCta(): Boolean { diff --git a/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt b/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt index 02947e17e4aa..836d1867c5a7 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt @@ -18,8 +18,10 @@ package com.duckduckgo.app.di import android.content.Context import androidx.room.Room +import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.db.MigrationsProvider +import com.duckduckgo.app.settings.db.SettingsDataStore import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -36,7 +38,11 @@ class DatabaseModule { } @Provides - fun provideDatabaseMigrations(context: Context): MigrationsProvider { - return MigrationsProvider(context) + fun provideDatabaseMigrations( + context: Context, + settingsDataStore: SettingsDataStore, + addToHomeCapabilityDetector: AddToHomeCapabilityDetector + ): MigrationsProvider { + return MigrationsProvider(context, settingsDataStore, addToHomeCapabilityDetector) } } diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 7be10e0555bc..1135d33787e1 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -24,6 +24,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.rating.db.AppEnjoymentDao import com.duckduckgo.app.browser.rating.db.AppEnjoymentEntity import com.duckduckgo.app.browser.rating.db.AppEnjoymentTypeConverter @@ -48,6 +49,7 @@ import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.privacy.db.* import com.duckduckgo.app.privacy.model.PrivacyProtectionCountsEntity import com.duckduckgo.app.privacy.model.UserWhitelistedDomain +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.survey.db.SurveyDao import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.tabs.db.TabsDao @@ -128,7 +130,11 @@ abstract class AppDatabase : RoomDatabase() { } @Suppress("PropertyName") -class MigrationsProvider(val context: Context) { +class MigrationsProvider( + val context: Context, + val settingsDataStore: SettingsDataStore, + val addToHomeCapabilityDetector: AddToHomeCapabilityDetector +) { val MIGRATION_1_TO_2: Migration = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { @@ -301,7 +307,10 @@ class MigrationsProvider(val context: Context) { val MIGRATION_21_TO_22: Migration = object : Migration(21, 22) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `keyTimestamps` (`id` TEXT NOT NULL PRIMARY KEY, `timestamp` INTEGER NOT NULL)") - database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") + + if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { + database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") + } } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index cc41c9e0e0e4..76869177bf90 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.onboarding.store import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.settings.db.SettingsDataStore import kotlinx.coroutines.withContext import javax.inject.Inject @@ -30,7 +31,8 @@ interface UserStageStore { class AppUserStageStore @Inject constructor( private val userStageDao: UserStageDao, private val dispatcher: DispatcherProvider, - private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector + private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector, + private val settingsDataStore: SettingsDataStore ) : UserStageStore { override suspend fun getUserAppStage(): AppStage { return withContext(dispatcher.io()) { @@ -44,7 +46,7 @@ class AppUserStageStore @Inject constructor( val newAppStage = when (appStage) { AppStage.NEW -> AppStage.DAX_ONBOARDING AppStage.DAX_ONBOARDING -> { - if (addToHomeCapabilityDetector.isAddToHomeSupported()) { + if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { AppStage.USE_OUR_APP_NOTIFICATION } else { AppStage.ESTABLISHED From b0ff683dffdc3ac90c1a4378636e3931a8162871 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 25 Jun 2020 17:35:47 +0100 Subject: [PATCH 21/38] Remove unneded pixel --- .../com/duckduckgo/app/browser/BrowserTabViewModelTest.kt | 8 -------- app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt | 2 +- .../java/com/duckduckgo/app/statistics/pixels/Pixel.kt | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index a8469efbf0ce..b6f9f4cca9eb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1780,14 +1780,6 @@ class BrowserTabViewModelTest { verify(mockSurveyDao).cancelScheduledSurveys() } - @Test - fun whenUserClickedSecondaryCtaButtonThenFirePixel() { - val cta = UseOurAppCta() - setCta(cta) - testee.onUserClickCtaSecondaryButton() - verify(mockPixel).fire(cta.cancelPixel!!, cta.pixelCancelParameters()) - } - @Test fun whenUserClickedSecondaryCtaButtonInUseOurAppCtaThenLaunchShowKeyboardCommand() { val cta = UseOurAppCta() diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 55c87b40d6e5..53cf0235b470 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -75,7 +75,7 @@ class UseOurAppCta( override val ctaId: CtaId = CtaId.USE_OUR_APP, override val shownPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_SHOWN, override val okPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_OK, - override val cancelPixel: Pixel.PixelName? = Pixel.PixelName.USE_OUR_APP_DIALOG_CANCELLED + override val cancelPixel: Pixel.PixelName? = null ) : Cta, DialogCta { override fun createCta(activity: FragmentActivity): DaxDialog = diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 7fd42f8119da..ec71eedf5222 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -190,7 +190,6 @@ interface Pixel { USE_OUR_APP_NOTIFICATION_SUFFIX("uoa"), USE_OUR_APP_DIALOG_SHOWN("m_uoa_d"), USE_OUR_APP_DIALOG_OK("m_uoa_d_ok"), - USE_OUR_APP_DIALOG_CANCELLED("m_uoa_d_c"), USE_OUR_APP_SHORTCUT_ADDED("m_uoa_s_a"), USE_OUR_APP_DIALOG_DELETE_SHOWN("m_uoa_dd"), UOA_VISITED_AFTER_SHORTCUT("m_uoa_vas"), From 23b28eb8f5b719a093fed651485b277b09e6f07b Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Jun 2020 09:52:24 +0100 Subject: [PATCH 22/38] Rename KeyTimestamp to UserEvent --- .../22.json | 6 ++-- .../app/browser/BrowserTabViewModelTest.kt | 20 ++++++------- .../browser/shortcut/ShortcutReceiverTest.kt | 10 +++---- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 30 +++++++++---------- .../db/UserEventsDaoTest.kt} | 24 +++++++-------- .../AndroidNotificationSchedulerTest.kt | 6 ++-- .../app/browser/BrowserTabViewModel.kt | 8 ++--- .../app/browser/shortcut/ShortcutReceiver.kt | 10 +++---- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 8 ++--- .../java/com/duckduckgo/app/di/DaoModule.kt | 2 +- .../java/com/duckduckgo/app/di/StoreModule.kt | 6 ++-- .../duckduckgo/app/global/ViewModelFactory.kt | 6 ++-- .../duckduckgo/app/global/db/AppDatabase.kt | 14 ++++----- .../db/UserEventEntity.kt} | 20 ++++++------- .../db/UserEventsDao.kt} | 10 +++---- .../db/UserEventsStore.kt} | 22 +++++++------- 16 files changed, 101 insertions(+), 101 deletions(-) rename app/src/androidTest/java/com/duckduckgo/app/global/{timestamps/db/KeyTimestampDaoTest.kt => events/db/UserEventsDaoTest.kt} (69%) rename app/src/main/java/com/duckduckgo/app/global/{timestamps/db/KeyTimestampEntity.kt => events/db/UserEventEntity.kt} (67%) rename app/src/main/java/com/duckduckgo/app/global/{timestamps/db/KeyTimestampDao.kt => events/db/UserEventsDao.kt} (73%) rename app/src/main/java/com/duckduckgo/app/global/{timestamps/db/KeyTimestampStore.kt => events/db/UserEventsStore.kt} (58%) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json index 54d51b064261..20faa52ad3b6 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 22, - "identityHash": "ec737cddc966adeb518f31a718199ff1", + "identityHash": "d6e385bcc19ae0df396817590763b709", "entities": [ { "tableName": "tds_tracker", @@ -711,7 +711,7 @@ "foreignKeys": [] }, { - "tableName": "keyTimestamps", + "tableName": "user_events", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { @@ -740,7 +740,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ec737cddc966adeb518f31a718199ff1')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e385bcc19ae0df396817590763b709')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index b6f9f4cca9eb..e17f180d685a 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -63,10 +63,10 @@ import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory -import com.duckduckgo.app.global.timestamps.db.TimestampKey +import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.notification.model.UseOurAppNotification -import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -207,7 +207,7 @@ class BrowserTabViewModelTest { private lateinit var mockUserWhitelistDao: UserWhitelistDao @Mock - private lateinit var mockKeyTimestampStore: KeyTimestampStore + private lateinit var mockUserEventsStore: UserEventsStore @Mock private lateinit var mockNotificationDao: NotificationDao @@ -249,7 +249,7 @@ class BrowserTabViewModelTest { mockSettingsStore, mockOnboardingStore, mockUserStageStore, - mockKeyTimestampStore, + mockUserEventsStore, coroutineRule.testDispatcherProvider ) @@ -284,7 +284,7 @@ class BrowserTabViewModelTest { pixel = mockPixel, dispatchers = coroutineRule.testDispatcherProvider, fireproofWebsiteDao = fireproofWebsiteDao, - keyTimestampStore = mockKeyTimestampStore, + userEventsStore = mockUserEventsStore, notificationDao = mockNotificationDao, variantManager = mockVariantManager ) @@ -2064,7 +2064,7 @@ class BrowserTabViewModelTest { @Test fun whenViewReadyIfDomainSameAsUseOurAppAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) testee.onViewReady() verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @@ -2086,7 +2086,7 @@ class BrowserTabViewModelTest { @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @@ -2109,8 +2109,8 @@ class BrowserTabViewModelTest { @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() - val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index e73703ec9d5c..32628728a526 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -19,7 +19,7 @@ package com.duckduckgo.app.browser.shortcut import android.content.Intent import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.cta.ui.UseOurAppCta -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.any @@ -37,13 +37,13 @@ class ShortcutReceiverTest { @get:Rule var coroutinesTestRule = CoroutineTestRule() - private val mockKeyTimestampStore: KeyTimestampStore = mock() + private val mockUserEventsStore: UserEventsStore = mock() private val mockPixel: Pixel = mock() private lateinit var testee: ShortcutReceiver @Before fun before() { - testee = ShortcutReceiver(mockKeyTimestampStore, coroutinesTestRule.testDispatcherProvider, mockPixel) + testee = ShortcutReceiver(mockUserEventsStore, coroutinesTestRule.testDispatcherProvider, mockPixel) } @Test @@ -53,7 +53,7 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockKeyTimestampStore).registerTimestamp(any()) + verify(mockUserEventsStore).registerTimestamp(any()) } @Test @@ -73,7 +73,7 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockKeyTimestampStore, never()).registerTimestamp(any()) + verify(mockUserEventsStore, never()).registerTimestamp(any()) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index d3efa0372baa..4105074a7b57 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -29,9 +29,9 @@ import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site -import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.TimestampKey +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -111,7 +111,7 @@ class CtaViewModelTest { private lateinit var mockUserStageStore: UserStageStore @Mock - private lateinit var mockKeyTimestampStore: KeyTimestampStore + private lateinit var mockUserEventsStore: UserEventsStore private val requiredDaxOnboardingCtas: List = listOf( CtaId.DAX_INTRO, @@ -146,7 +146,7 @@ class CtaViewModelTest { mockSettingsDataStore, mockOnboardingStore, mockUserStageStore, - mockKeyTimestampStore, + mockUserEventsStore, coroutineRule.testDispatcherProvider ) } @@ -455,8 +455,8 @@ class CtaViewModelTest { @Test fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenShowUseOurAppDeletionCta() = runBlockingTest { givenUserIsEstablished() - val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) assertTrue(value is UseOurAppDeletionCta) @@ -465,8 +465,8 @@ class CtaViewModelTest { @Test fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndHideTipsIsTrueThenReturnNull() = runBlockingTest { givenUserIsEstablished() - val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) whenever(mockSettingsDataStore.hideTips).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) @@ -476,8 +476,8 @@ class CtaViewModelTest { @Test fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndCtaShownThenReturnNull() = runBlockingTest { givenUserIsEstablished() - val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) @@ -487,8 +487,8 @@ class CtaViewModelTest { @Test fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { givenUserIsEstablished() - val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) assertNull(value) @@ -497,8 +497,8 @@ class CtaViewModelTest { @Test fun whenRefreshCtaWhileBrowsingAndSiteIsNotUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { givenUserIsEstablished() - val timestampEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockKeyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) + whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = "test")) assertNull(value) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt similarity index 69% rename from app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt rename to app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt index eaf1410ba9a9..7b6774d6b1a1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.global.timestamps.db +package com.duckduckgo.app.global.events.db import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room @@ -32,7 +32,7 @@ import org.junit.Rule import java.util.concurrent.TimeUnit @ExperimentalCoroutinesApi -class KeyTimestampDaoTest { +class UserEventsDaoTest { @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @@ -42,15 +42,15 @@ class KeyTimestampDaoTest { private lateinit var db: AppDatabase - private lateinit var dao: KeyTimestampDao + private lateinit var dao: UserEventsDao - private lateinit var testee: AppKeyTimestampStore + private lateinit var testee: AppUserEventsStore @Before fun before() { db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java).build() - dao = db.keyTimestampDao() - testee = AppKeyTimestampStore(dao, coroutineRule.testDispatcherProvider) + dao = db.userEventsDao() + testee = AppUserEventsStore(dao, coroutineRule.testDispatcherProvider) } @After @@ -60,26 +60,26 @@ class KeyTimestampDaoTest { @Test fun whenGetTimestampAndDatabaseEmptyThenReturnNull() = coroutineRule.runBlocking { - val value = testee.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + val value = testee.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) assertNull(value) } @Test fun whenInsertingTimestampThenReturnSameTimestamp() = coroutineRule.runBlocking { - val entity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) testee.registerTimestamp(entity) - assertEquals(entity.timestamp, testee.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + assertEquals(entity.timestamp, testee.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) } @Test fun whenInsertingSameTimestampThenReplaceOldTimestampWithTheNew() = coroutineRule.runBlocking { - val entity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) - val newEntity = KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(1)) + val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) + val newEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(1)) testee.registerTimestamp(entity) testee.registerTimestamp(newEntity) - assertEquals(newEntity.timestamp, testee.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + assertEquals(newEntity.timestamp, testee.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt index f5b4a1c0b61f..eba96fe906c3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt @@ -405,7 +405,7 @@ class AndroidNotificationSchedulerTest { @Test fun whenStageIsUseOurAppNotificationThenNotificationScheduled() = runBlocking { - givenStageIsUserOurAppNotification() + givenStageIsUseOurAppNotification() testee.scheduleNextNotification() @@ -414,7 +414,7 @@ class AndroidNotificationSchedulerTest { @Test fun whenStageIsUseOurAppNotificationAndNotificationScheduledThenStageCompleted() = runBlocking { - givenStageIsUserOurAppNotification() + givenStageIsUseOurAppNotification() testee.scheduleNextNotification() @@ -475,7 +475,7 @@ class AndroidNotificationSchedulerTest { ) } - private suspend fun givenStageIsUserOurAppNotification() { + private suspend fun givenStageIsUseOurAppNotification() { whenever(privacyNotification.canShow()).thenReturn(false) whenever(clearNotification.canShow()).thenReturn(false) whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_NOTIFICATION) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 750d19c29192..7a7c6067e4d3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -82,8 +82,8 @@ import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.domainMatchesUrl import com.duckduckgo.app.global.model.isUseOurAppDomain -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.TimestampKey +import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.toDesktopUri import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.UseOurAppNotification @@ -136,7 +136,7 @@ class BrowserTabViewModel( private val searchCountDao: SearchCountDao, private val pixel: Pixel, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), - private val keyTimestampStore: KeyTimestampStore, + private val userEventsStore: UserEventsStore, private val notificationDao: NotificationDao, private val variantManager: VariantManager ) : WebViewClientListener, EditBookmarkListener, HttpAuthenticationListener, ViewModel() { @@ -652,7 +652,7 @@ class BrowserTabViewModel( private suspend fun sendPixelIfUseOurAppSiteVisited() { withContext(dispatchers.io()) { - val isShortcutAdded = keyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) + val isShortcutAdded = userEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) val isUseOurAppNotificationSeen = notificationDao.exists(UseOurAppNotification.ID) val deleteCtaShown = ctaViewModel.useOurAppDeletionDialogShown() diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index 632e422fd0ee..e3c5688158c7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -26,15 +26,15 @@ import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TI import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.DispatcherProvider -import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.TimestampKey +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.statistics.pixels.Pixel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import javax.inject.Inject -class ShortcutReceiver @Inject constructor(private val keyTimestampStore: KeyTimestampStore, val dispatcher: DispatcherProvider, val pixel: Pixel) : +class ShortcutReceiver @Inject constructor(private val userEventsStore: UserEventsStore, val dispatcher: DispatcherProvider, val pixel: Pixel) : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -49,7 +49,7 @@ class ShortcutReceiver @Inject constructor(private val keyTimestampStore: KeyTim GlobalScope.launch(dispatcher.io()) { if (originUrl == USE_OUR_APP_SHORTCUT_URL) { - keyTimestampStore.registerTimestamp(KeyTimestampEntity(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED)) + userEventsStore.registerTimestamp(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) } } diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 29bab4426971..2fabe852072e 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -29,8 +29,8 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.isUseOurAppDomain import com.duckduckgo.app.global.model.orderedTrackingEntities -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.TimestampKey +import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -62,7 +62,7 @@ class CtaViewModel @Inject constructor( private val settingsDataStore: SettingsDataStore, private val onboardingStore: OnboardingStore, private val userStageStore: UserStageStore, - private val keyTimestampStore: KeyTimestampStore, + private val userEventsStore: UserEventsStore, private val dispatchers: DispatcherProvider ) { val surveyLiveData: LiveData = surveyDao.getLiveScheduled() @@ -207,7 +207,7 @@ class CtaViewModel @Inject constructor( @WorkerThread private suspend fun twoDaysSinceShortcutAdded(): Boolean { - val timestampKey = keyTimestampStore.getTimestamp(TimestampKey.USE_OUR_APP_SHORTCUT_ADDED) ?: return false + val timestampKey = userEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) ?: return false val days = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestampKey.timestamp) return (days >= 2) } diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index b335e21d6d2f..383b9a8f5357 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -84,5 +84,5 @@ class DaoModule { fun fireproofWebsiteDao(database: AppDatabase) = database.fireproofWebsiteDao() @Provides - fun keyTimestampDao(database: AppDatabase) = database.keyTimestampDao() + fun userEventsDao(database: AppDatabase) = database.userEventsDao() } diff --git a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt index 1e1597097c1e..d3b31a68de4e 100644 --- a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt @@ -20,8 +20,8 @@ import com.duckduckgo.app.fire.UnsentForgetAllPixelStore import com.duckduckgo.app.fire.UnsentForgetAllPixelStoreSharedPreferences import com.duckduckgo.app.global.install.AppInstallSharedPreferences import com.duckduckgo.app.global.install.AppInstallStore -import com.duckduckgo.app.global.timestamps.db.AppKeyTimestampStore -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.events.db.AppUserEventsStore +import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.onboarding.store.AppUserStageStore import com.duckduckgo.app.onboarding.store.OnboardingSharedPreferences import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -65,5 +65,5 @@ abstract class StoreModule { abstract fun bindUserStageStore(userStageStore: AppUserStageStore): UserStageStore @Binds - abstract fun bindKeyTimestampStore(keyTimestampStore: AppKeyTimestampStore): KeyTimestampStore + abstract fun bindUserEventsStore(userEventsStore: AppUserEventsStore): UserEventsStore } diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index c632c5d62b7d..c52835832ffb 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -43,7 +43,7 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder -import com.duckduckgo.app.global.timestamps.db.KeyTimestampStore +import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.icon.api.IconModifier import com.duckduckgo.app.icon.ui.ChangeIconViewModel import com.duckduckgo.app.launch.LaunchViewModel @@ -110,7 +110,7 @@ class ViewModelFactory @Inject constructor( private val onboardingPageManager: OnboardingPageManager, private val appInstallationReferrerStateListener: AppInstallationReferrerStateListener, private val appIconModifier: IconModifier, - private val keyTimestampStore: KeyTimestampStore, + private val userEventsStore: UserEventsStore, private val notificationDao: NotificationDao, private val dispatcherProvider: DispatcherProvider ) : ViewModelProvider.NewInstanceFactory() { @@ -199,7 +199,7 @@ class ViewModelFactory @Inject constructor( ctaViewModel = ctaViewModel, searchCountDao = searchCountDao, pixel = pixel, - keyTimestampStore = keyTimestampStore, + userEventsStore = userEventsStore, notificationDao = notificationDao, variantManager = variantManager ) diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 1135d33787e1..9fdb9024a3b3 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -36,9 +36,9 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.exception.UncaughtExceptionDao import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSourceConverter -import com.duckduckgo.app.global.timestamps.db.KeyTimestampDao -import com.duckduckgo.app.global.timestamps.db.KeyTimestampEntity -import com.duckduckgo.app.global.timestamps.db.TimestampKeyTypeConverter +import com.duckduckgo.app.global.events.db.UserEventsDao +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventTypeConverter import com.duckduckgo.app.httpsupgrade.db.HttpsBloomFilterSpecDao import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao import com.duckduckgo.app.httpsupgrade.model.HttpsBloomFilterSpec @@ -87,7 +87,7 @@ import com.duckduckgo.app.usage.search.SearchCountEntity TdsMetadata::class, UserStage::class, FireproofWebsiteEntity::class, - KeyTimestampEntity::class + UserEventEntity::class ] ) @@ -101,7 +101,7 @@ import com.duckduckgo.app.usage.search.SearchCountEntity CategoriesTypeConverter::class, UncaughtExceptionSourceConverter::class, StageTypeConverter::class, - TimestampKeyTypeConverter::class + UserEventTypeConverter::class ) abstract class AppDatabase : RoomDatabase() { @@ -126,7 +126,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun tdsDao(): TdsMetadataDao abstract fun userStageDao(): UserStageDao abstract fun fireproofWebsiteDao(): FireproofWebsiteDao - abstract fun keyTimestampDao(): KeyTimestampDao + abstract fun userEventsDao(): UserEventsDao } @Suppress("PropertyName") @@ -306,7 +306,7 @@ class MigrationsProvider( val MIGRATION_21_TO_22: Migration = object : Migration(21, 22) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `keyTimestamps` (`id` TEXT NOT NULL PRIMARY KEY, `timestamp` INTEGER NOT NULL)") + database.execSQL("CREATE TABLE IF NOT EXISTS `user_events` (`id` TEXT NOT NULL PRIMARY KEY, `timestamp` INTEGER NOT NULL)") if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt similarity index 67% rename from app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt rename to app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt index 2c197c99c812..6e3074700f4a 100644 --- a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt @@ -14,33 +14,33 @@ * limitations under the License. */ -package com.duckduckgo.app.global.timestamps.db +package com.duckduckgo.app.global.events.db import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverter -const val KEY_TIMESTAMPS_TABLE_NAME = "keyTimestamps" +const val USER_EVENTS_TABLE_NAME = "user_events" -@Entity(tableName = KEY_TIMESTAMPS_TABLE_NAME) -data class KeyTimestampEntity( - @PrimaryKey val id: TimestampKey, +@Entity(tableName = USER_EVENTS_TABLE_NAME) +data class UserEventEntity( + @PrimaryKey val id: UserEventKey, val timestamp: Long = System.currentTimeMillis() ) -enum class TimestampKey { +enum class UserEventKey { USE_OUR_APP_SHORTCUT_ADDED } -class TimestampKeyTypeConverter { +class UserEventTypeConverter { @TypeConverter - fun toKey(stage: String): TimestampKey { - return TimestampKey.valueOf(stage) + fun toKey(stage: String): UserEventKey { + return UserEventKey.valueOf(stage) } @TypeConverter - fun fromKey(stage: TimestampKey): String { + fun fromKey(stage: UserEventKey): String { return stage.name } } diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt similarity index 73% rename from app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt rename to app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt index 12a1bd6a87ca..38265802106d 100644 --- a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampDao.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.global.timestamps.db +package com.duckduckgo.app.global.events.db import androidx.room.Dao import androidx.room.Insert @@ -22,11 +22,11 @@ import androidx.room.OnConflictStrategy import androidx.room.Query @Dao -interface KeyTimestampDao { +interface UserEventsDao { - @Query("select * from $KEY_TIMESTAMPS_TABLE_NAME where id=:timestampKey") - suspend fun getTimestamp(timestampKey: TimestampKey): KeyTimestampEntity? + @Query("select * from $USER_EVENTS_TABLE_NAME where id=:userEventKey") + suspend fun getTimestamp(userEventKey: UserEventKey): UserEventEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(keyTimestampEntity: KeyTimestampEntity) + fun insert(userEventEntity: UserEventEntity) } diff --git a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt similarity index 58% rename from app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt rename to app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt index 9add766b3b4e..e57f84ed5c6f 100644 --- a/app/src/main/java/com/duckduckgo/app/global/timestamps/db/KeyTimestampStore.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt @@ -14,31 +14,31 @@ * limitations under the License. */ -package com.duckduckgo.app.global.timestamps.db +package com.duckduckgo.app.global.events.db import com.duckduckgo.app.global.DispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject -interface KeyTimestampStore { - suspend fun getTimestamp(timestampKey: TimestampKey): KeyTimestampEntity? - suspend fun registerTimestamp(timestampEntity: KeyTimestampEntity) +interface UserEventsStore { + suspend fun getTimestamp(userEventKey: UserEventKey): UserEventEntity? + suspend fun registerTimestamp(timestampEntity: UserEventEntity) } -class AppKeyTimestampStore @Inject constructor( - private val keyTimestampDao: KeyTimestampDao, +class AppUserEventsStore @Inject constructor( + private val userEventsDao: UserEventsDao, private val dispatcher: DispatcherProvider -) : KeyTimestampStore { +) : UserEventsStore { - override suspend fun getTimestamp(timestampKey: TimestampKey): KeyTimestampEntity? { + override suspend fun getTimestamp(userEventKey: UserEventKey): UserEventEntity? { return withContext(dispatcher.io()) { - keyTimestampDao.getTimestamp(timestampKey) + userEventsDao.getTimestamp(userEventKey) } } - override suspend fun registerTimestamp(timestampEntity: KeyTimestampEntity) { + override suspend fun registerTimestamp(timestampEntity: UserEventEntity) { withContext(dispatcher.io()) { - keyTimestampDao.insert(timestampEntity) + userEventsDao.insert(timestampEntity) } } } From db616c3041b264aeb5cd3fe75a589e336bc93042 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Jun 2020 10:22:28 +0100 Subject: [PATCH 23/38] Create use our app detector --- .../app/browser/BrowserTabViewModelTest.kt | 7 ++- .../app/browser/BrowserViewModelTest.kt | 4 +- .../browser/shortcut/ShortcutReceiverTest.kt | 6 +- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 4 +- .../global/useourapp/UseOurAppDetectorTest.kt | 57 +++++++++++++++++++ .../app/browser/BrowserTabViewModel.kt | 22 +++---- .../app/browser/BrowserViewModel.kt | 2 +- .../app/browser/shortcut/ShortcutBuilder.kt | 2 +- .../app/browser/shortcut/ShortcutReceiver.kt | 2 +- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 7 --- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 5 +- .../duckduckgo/app/global/ViewModelFactory.kt | 3 + .../com/duckduckgo/app/global/model/Site.kt | 7 --- .../app/global/useourapp/UseOurAppDetector.kt | 45 +++++++++++++++ 14 files changed, 135 insertions(+), 38 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index e17f180d685a..f68394f39f3d 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -57,8 +57,8 @@ import com.duckduckgo.app.cta.ui.DaxBubbleCta import com.duckduckgo.app.cta.ui.DaxDialogCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.UseOurAppCta -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_DOMAIN -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_DOMAIN +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site @@ -67,6 +67,7 @@ import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.global.events.db.UserEventEntity import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -250,6 +251,7 @@ class BrowserTabViewModelTest { mockOnboardingStore, mockUserStageStore, mockUserEventsStore, + UseOurAppDetector(), coroutineRule.testDispatcherProvider ) @@ -286,6 +288,7 @@ class BrowserTabViewModelTest { fireproofWebsiteDao = fireproofWebsiteDao, userEventsStore = mockUserEventsStore, notificationDao = mockNotificationDao, + useOurAppDetector = UseOurAppDetector(), variantManager = mockVariantManager ) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index ad4ba19a79a9..e8a521eef1bb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,12 +23,12 @@ import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.BrowserViewModel.Command.DisplayMessage import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter -import com.duckduckgo.app.cta.ui.UseOurAppCta import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel @@ -187,7 +187,7 @@ class BrowserViewModelTest { @Test fun whenOpenShortcutIfUrlIsUSeOurAppUrlThenFirePixel() { - val url = UseOurAppCta.USE_OUR_APP_SHORTCUT_URL + val url = USE_OUR_APP_SHORTCUT_URL whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) testee.onOpenShortcut(url) verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index 32628728a526..e1f3aa428ebb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -18,8 +18,8 @@ package com.duckduckgo.app.browser.shortcut import android.content.Intent import com.duckduckgo.app.CoroutineTestRule -import com.duckduckgo.app.cta.ui.UseOurAppCta import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.any @@ -49,7 +49,7 @@ class ShortcutReceiverTest { @Test fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenRegisterTimestamp() = coroutinesTestRule.runBlocking { val intent = Intent() - intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, UseOurAppCta.USE_OUR_APP_SHORTCUT_URL) + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL) intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) @@ -59,7 +59,7 @@ class ShortcutReceiverTest { @Test fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenFirePixel() { val intent = Intent() - intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, UseOurAppCta.USE_OUR_APP_SHORTCUT_URL) + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL) intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 4105074a7b57..eadfe1818882 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -25,13 +25,14 @@ import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.events.db.UserEventEntity import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.events.db.UserEventKey +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -147,6 +148,7 @@ class CtaViewModelTest { mockOnboardingStore, mockUserStageStore, mockUserEventsStore, + UseOurAppDetector(), coroutineRule.testDispatcherProvider ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt new file mode 100644 index 000000000000..ed24a386172b --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt @@ -0,0 +1,57 @@ +/* + * 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.global.useourapp + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class UseOurAppDetectorTest { + + private lateinit var testee: UseOurAppDetector + + @Before + fun setup() { + testee = UseOurAppDetector() + } + + @Test + fun whenCheckingIfUrlIsFromUseOurAppDomainThenReturnTrue() { + assertTrue(testee.isUseOurAppUrl("http://www.facebook.com")) + } + + @Test + fun whenCheckingIfMobileUrlIsFromUseOurAppDomainThenReturnTrue() { + assertTrue(testee.isUseOurAppUrl("http://m.facebook.com")) + } + + @Test + fun whenCheckingIfMobileOnlyDomainIsFromUseOurAppDomainThenReturnTrue() { + assertTrue(testee.isUseOurAppUrl("m.facebook.com")) + } + + @Test + fun whenCheckingIfOnlyDomainUrlIsFromUseOurAppDomainThenReturnTrue() { + assertTrue(testee.isUseOurAppUrl("facebook.com")) + } + + @Test + fun whenCheckingIfUrlIsFromUseOurAppDomainThenReturnFalse() { + assertFalse(testee.isUseOurAppUrl("http://example.com")) + } + +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 7a7c6067e4d3..4ca4220d3f87 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -67,8 +67,6 @@ import com.duckduckgo.app.cta.ui.DialogCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.HomeTopPanelCta import com.duckduckgo.app.cta.ui.UseOurAppCta -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_TITLE -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.global.AppUrl @@ -81,10 +79,12 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.domainMatchesUrl -import com.duckduckgo.app.global.model.isUseOurAppDomain import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.toDesktopUri +import com.duckduckgo.app.global.useourapp.UseOurAppDetector +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_TITLE +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.UseOurAppNotification import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao @@ -138,6 +138,7 @@ class BrowserTabViewModel( private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), private val userEventsStore: UserEventsStore, private val notificationDao: NotificationDao, + private val useOurAppDetector: UseOurAppDetector, private val variantManager: VariantManager ) : WebViewClientListener, EditBookmarkListener, HttpAuthenticationListener, ViewModel() { @@ -305,7 +306,7 @@ class BrowserTabViewModel( fun onViewReady() { url?.let { - isDomainSameAsUseOurAppSiteDomain() + isDomainSameAsUseOurAppDomain(it) onUserSubmittedQuery(it) } } @@ -599,14 +600,13 @@ class BrowserTabViewModel( private fun pageChanged(url: String, title: String?) { Timber.v("Page changed: $url") + val oldUrl = site?.url - val oldSite = site - - buildSiteFactory(url, title) // Immediately updates site with the new URL + buildSiteFactory(url, title) // Navigating from different website to use our app website - if (!oldSite.isUseOurAppDomain()) { - isDomainSameAsUseOurAppSiteDomain() + if (!useOurAppDetector.isUseOurAppUrl(oldUrl)) { + isDomainSameAsUseOurAppDomain(url) } command.value = RefreshUserAgent(site?.uri?.host, currentBrowserViewState().isDesktopBrowsingMode) @@ -644,8 +644,8 @@ class BrowserTabViewModel( registerSiteVisit() } - private fun isDomainSameAsUseOurAppSiteDomain() { - if (site.isUseOurAppDomain()) { + private fun isDomainSameAsUseOurAppDomain(url: String) { + if (useOurAppDetector.isUseOurAppUrl(url)) { viewModelScope.launch { sendPixelIfUseOurAppSiteVisited() } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 1d86a3848c84..d81e7ce6a5b5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -27,7 +27,6 @@ import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.rating.ui.AppEnjoymentDialogFragment import com.duckduckgo.app.browser.rating.ui.GiveFeedbackDialogFragment import com.duckduckgo.app.browser.rating.ui.RateAppDialogFragment -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.ApplicationClearDataState import com.duckduckgo.app.global.SingleLiveEvent @@ -35,6 +34,7 @@ import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity.Companion.RELOAD_RESULT_CODE import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.tabs.model.TabEntity diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt index bc85ce775b5b..a1c4bd97e166 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt @@ -26,7 +26,7 @@ import androidx.core.graphics.drawable.IconCompat import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.BrowserTabViewModel import com.duckduckgo.app.browser.R -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import java.util.UUID import javax.inject.Inject diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index e3c5688158c7..c864875def34 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -24,11 +24,11 @@ import android.widget.Toast import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TITLE_ARG import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.events.db.UserEventEntity import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.events.db.UserEventKey +import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.statistics.pixels.Pixel import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 53cf0235b470..c48d26969995 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -92,13 +92,6 @@ class UseOurAppCta( override fun pixelOkParameters(): Map = emptyMap() override fun pixelShownParameters(): Map = emptyMap() - - companion object { - const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" - const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" - const val USE_OUR_APP_DOMAIN = "facebook.com" - const val USE_OUR_APP_MOBILE_DOMAIN = "m.facebook.com" - } } class UseOurAppDeletionCta( diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 2fabe852072e..c3f76b4bfe6c 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -27,10 +27,10 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain -import com.duckduckgo.app.global.model.isUseOurAppDomain import com.duckduckgo.app.global.model.orderedTrackingEntities import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.events.db.UserEventKey +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore @@ -63,6 +63,7 @@ class CtaViewModel @Inject constructor( private val onboardingStore: OnboardingStore, private val userStageStore: UserStageStore, private val userEventsStore: UserEventsStore, + private val useOurAppDetector: UseOurAppDetector, private val dispatchers: DispatcherProvider ) { val surveyLiveData: LiveData = surveyDao.getLiveScheduled() @@ -203,7 +204,7 @@ class CtaViewModel @Inject constructor( @WorkerThread private suspend fun canShowUseOurAppDeletionDialog(site: Site?): Boolean = - !settingsDataStore.hideTips && !useOurAppDeletionDialogShown() && site.isUseOurAppDomain() && twoDaysSinceShortcutAdded() + !settingsDataStore.hideTips && !useOurAppDeletionDialogShown() && useOurAppDetector.isUseOurAppUrl(site?.url) && twoDaysSinceShortcutAdded() @WorkerThread private suspend fun twoDaysSinceShortcutAdded(): Boolean { diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index c52835832ffb..5c49cf43ac75 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -44,6 +44,7 @@ import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.icon.api.IconModifier import com.duckduckgo.app.icon.ui.ChangeIconViewModel import com.duckduckgo.app.launch.LaunchViewModel @@ -112,6 +113,7 @@ class ViewModelFactory @Inject constructor( private val appIconModifier: IconModifier, private val userEventsStore: UserEventsStore, private val notificationDao: NotificationDao, + private val userOurAppDetector: UseOurAppDetector, private val dispatcherProvider: DispatcherProvider ) : ViewModelProvider.NewInstanceFactory() { @@ -201,6 +203,7 @@ class ViewModelFactory @Inject constructor( pixel = pixel, userEventsStore = userEventsStore, notificationDao = notificationDao, + useOurAppDetector = userOurAppDetector, variantManager = variantManager ) diff --git a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt index c0ffa4341d9e..7f7ee6e63ed8 100644 --- a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt +++ b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt @@ -18,8 +18,6 @@ package com.duckduckgo.app.global.model import android.net.Uri import androidx.core.net.toUri -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_DOMAIN -import com.duckduckgo.app.cta.ui.UseOurAppCta.Companion.USE_OUR_APP_MOBILE_DOMAIN import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.model.SiteFactory.SitePrivacyData import com.duckduckgo.app.privacy.model.HttpsStatus @@ -74,9 +72,4 @@ fun Site.domainMatchesUrl(matchingUrl: String): Boolean { return uri?.baseHost == matchingUrl.toUri().baseHost } -fun Site?.isUseOurAppDomain(): Boolean { - if (this == null) return false - return domainMatchesUrl(USE_OUR_APP_DOMAIN) || domainMatchesUrl(USE_OUR_APP_MOBILE_DOMAIN) -} - val Site.domain get() = uri?.host diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt new file mode 100644 index 000000000000..527ae9328d89 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt @@ -0,0 +1,45 @@ +/* + * 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.global.useourapp + +import android.net.Uri +import androidx.core.net.toUri +import com.duckduckgo.app.global.baseHost +import javax.inject.Inject + +class UseOurAppDetector @Inject constructor() { + + fun isUseOurAppUrl(url: String?): Boolean { + if (url == null) return false + return isUseOurAppUrl(url.toUri()) + } + + private fun isUseOurAppUrl(uri: Uri): Boolean { + return domainMatchesUrl(uri, USE_OUR_APP_DOMAIN) || domainMatchesUrl(uri, USE_OUR_APP_MOBILE_DOMAIN) + } + + private fun domainMatchesUrl(uri: Uri, matchingUrl: String): Boolean { + return uri.baseHost == matchingUrl.toUri().baseHost + } + + companion object { + const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" + const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" + const val USE_OUR_APP_DOMAIN = "facebook.com" + const val USE_OUR_APP_MOBILE_DOMAIN = "m.facebook.com" + } +} From df8f285eb983412f80717a50563d760aa1db7770 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Jun 2020 10:29:44 +0100 Subject: [PATCH 24/38] Rename methods --- .../app/browser/BrowserTabViewModelTest.kt | 6 +++--- .../app/browser/shortcut/ShortcutReceiverTest.kt | 4 ++-- .../com/duckduckgo/app/cta/ui/CtaViewModelTest.kt | 10 +++++----- .../app/global/events/db/UserEventsDaoTest.kt | 12 ++++++------ .../duckduckgo/app/browser/BrowserTabViewModel.kt | 2 +- .../app/browser/shortcut/ShortcutReceiver.kt | 2 +- .../java/com/duckduckgo/app/cta/ui/CtaViewModel.kt | 2 +- .../app/global/events/db/UserEventEntity.kt | 3 ++- .../duckduckgo/app/global/events/db/UserEventsDao.kt | 2 +- .../app/global/events/db/UserEventsStore.kt | 10 +++++----- 10 files changed, 27 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index f68394f39f3d..9d5d4a3502a8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -2067,7 +2067,7 @@ class BrowserTabViewModelTest { @Test fun whenViewReadyIfDomainSameAsUseOurAppAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) testee.onViewReady() verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @@ -2089,7 +2089,7 @@ class BrowserTabViewModelTest { @Test fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @@ -2113,7 +2113,7 @@ class BrowserTabViewModelTest { fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) testee.pageRefreshed(USE_OUR_APP_DOMAIN) verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index e1f3aa428ebb..94d6e1735c9f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -53,7 +53,7 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockUserEventsStore).registerTimestamp(any()) + verify(mockUserEventsStore).registerUserEvent(any()) } @Test @@ -73,7 +73,7 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockUserEventsStore, never()).registerTimestamp(any()) + verify(mockUserEventsStore, never()).registerUserEvent(any()) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index eadfe1818882..44531cfdedc1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -458,7 +458,7 @@ class CtaViewModelTest { fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenShowUseOurAppDeletionCta() = runBlockingTest { givenUserIsEstablished() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) assertTrue(value is UseOurAppDeletionCta) @@ -468,7 +468,7 @@ class CtaViewModelTest { fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndHideTipsIsTrueThenReturnNull() = runBlockingTest { givenUserIsEstablished() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) whenever(mockSettingsDataStore.hideTips).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) @@ -479,7 +479,7 @@ class CtaViewModelTest { fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedAndCtaShownThenReturnNull() = runBlockingTest { givenUserIsEstablished() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) @@ -490,7 +490,7 @@ class CtaViewModelTest { fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { givenUserIsEstablished() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = USE_OUR_APP_SHORTCUT_URL)) assertNull(value) @@ -500,7 +500,7 @@ class CtaViewModelTest { fun whenRefreshCtaWhileBrowsingAndSiteIsNotUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { givenUserIsEstablished() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)) - whenever(mockUserEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site(url = "test")) assertNull(value) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt index 7b6774d6b1a1..0155d139c470 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt @@ -60,16 +60,16 @@ class UserEventsDaoTest { @Test fun whenGetTimestampAndDatabaseEmptyThenReturnNull() = coroutineRule.runBlocking { - val value = testee.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) + val value = testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) assertNull(value) } @Test fun whenInsertingTimestampThenReturnSameTimestamp() = coroutineRule.runBlocking { val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - testee.registerTimestamp(entity) + testee.registerUserEvent(entity) - assertEquals(entity.timestamp, testee.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + assertEquals(entity.timestamp, testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) } @Test @@ -77,9 +77,9 @@ class UserEventsDaoTest { val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) val newEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(1)) - testee.registerTimestamp(entity) - testee.registerTimestamp(newEntity) + testee.registerUserEvent(entity) + testee.registerUserEvent(newEntity) - assertEquals(newEntity.timestamp, testee.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + assertEquals(newEntity.timestamp, testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 4ca4220d3f87..88aecf53a43a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -652,7 +652,7 @@ class BrowserTabViewModel( private suspend fun sendPixelIfUseOurAppSiteVisited() { withContext(dispatchers.io()) { - val isShortcutAdded = userEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) + val isShortcutAdded = userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) val isUseOurAppNotificationSeen = notificationDao.exists(UseOurAppNotification.ID) val deleteCtaShown = ctaViewModel.useOurAppDeletionDialogShown() diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index c864875def34..0d39364d1a6f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -49,7 +49,7 @@ class ShortcutReceiver @Inject constructor(private val userEventsStore: UserEven GlobalScope.launch(dispatcher.io()) { if (originUrl == USE_OUR_APP_SHORTCUT_URL) { - userEventsStore.registerTimestamp(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + userEventsStore.registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) } } diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index c3f76b4bfe6c..529ed2ea4fcf 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -208,7 +208,7 @@ class CtaViewModel @Inject constructor( @WorkerThread private suspend fun twoDaysSinceShortcutAdded(): Boolean { - val timestampKey = userEventsStore.getTimestamp(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) ?: return false + val timestampKey = userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) ?: return false val days = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestampKey.timestamp) return (days >= 2) } diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt index 6e3074700f4a..ceac3b81e4aa 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt @@ -29,7 +29,8 @@ data class UserEventEntity( ) enum class UserEventKey { - USE_OUR_APP_SHORTCUT_ADDED + USE_OUR_APP_SHORTCUT_ADDED, + USE_OUR_APP_FIREPROOF_SEEN } class UserEventTypeConverter { diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt index 38265802106d..d611cc8ab73c 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt @@ -25,7 +25,7 @@ import androidx.room.Query interface UserEventsDao { @Query("select * from $USER_EVENTS_TABLE_NAME where id=:userEventKey") - suspend fun getTimestamp(userEventKey: UserEventKey): UserEventEntity? + suspend fun getUserEvent(userEventKey: UserEventKey): UserEventEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(userEventEntity: UserEventEntity) diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt index e57f84ed5c6f..0545c0e3bb80 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.withContext import javax.inject.Inject interface UserEventsStore { - suspend fun getTimestamp(userEventKey: UserEventKey): UserEventEntity? - suspend fun registerTimestamp(timestampEntity: UserEventEntity) + suspend fun getUserEvent(userEventKey: UserEventKey): UserEventEntity? + suspend fun registerUserEvent(timestampEntity: UserEventEntity) } class AppUserEventsStore @Inject constructor( @@ -30,13 +30,13 @@ class AppUserEventsStore @Inject constructor( private val dispatcher: DispatcherProvider ) : UserEventsStore { - override suspend fun getTimestamp(userEventKey: UserEventKey): UserEventEntity? { + override suspend fun getUserEvent(userEventKey: UserEventKey): UserEventEntity? { return withContext(dispatcher.io()) { - userEventsDao.getTimestamp(userEventKey) + userEventsDao.getUserEvent(userEventKey) } } - override suspend fun registerTimestamp(timestampEntity: UserEventEntity) { + override suspend fun registerUserEvent(timestampEntity: UserEventEntity) { withContext(dispatcher.io()) { userEventsDao.insert(timestampEntity) } From c359ea5310c8cd3841c67ec972e2740923a93e48 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Jun 2020 12:27:40 +0100 Subject: [PATCH 25/38] Amend copy --- app/src/main/res/values/string-untranslated.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 07f60fb392d2..8dbb392b22a3 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -33,11 +33,11 @@ Try It Out - Did you know the Facebook app can make requests for data even when you\'re not using?<br/><br/>Replace the app with a home screen shortcut that opens Facebook in DuckDuckGo. Then delete the Facebook app. + Did you know the Facebook app can make requests for data even when you\'re not using it?<br/><br/>Replace the app with a home screen shortcut that opens Facebook in DuckDuckGo. Then delete the Facebook app. Add Facebook Shortcut Not Now Worried about Facebook tracking you? - Here\'s a simply way to reduce it\'s reach. + Here\'s a simply way to reduce its reach. Success! %s has been added to your home screen. Checking your feed in DuckDuckGo is a great alternative to using the Facebook app!<br/><br/>But if the Facebook app is on your phone, it can make requests for data even when you\'re not using it.<br/><br/>Prevent this by deleting it now! From deb919ac8cfcf4b5fb7de97d6ac4111145b4e138 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Sat, 27 Jun 2020 11:20:00 +0100 Subject: [PATCH 26/38] Amend copy! --- app/src/main/res/values/string-untranslated.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 8dbb392b22a3..f3e655ba7ed6 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -37,7 +37,7 @@ Add Facebook Shortcut Not Now Worried about Facebook tracking you? - Here\'s a simply way to reduce its reach. + Here\'s a simple way to reduce its reach. Success! %s has been added to your home screen. Checking your feed in DuckDuckGo is a great alternative to using the Facebook app!<br/><br/>But if the Facebook app is on your phone, it can make requests for data even when you\'re not using it.<br/><br/>Prevent this by deleting it now! From 3ec696ba1827ddefd480664a076880dab23f93a1 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Tue, 30 Jun 2020 17:03:03 +0100 Subject: [PATCH 27/38] Integrate fireproof dialog --- .../app/browser/BrowserTabViewModelTest.kt | 29 ++++- .../logindetection/JsLoginDetectorTest.kt | 5 +- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 2 +- .../global/useourapp/UseOurAppDetectorTest.kt | 105 +++++++++++++++++- .../app/browser/BrowserTabViewModel.kt | 2 + .../app/browser/di/BrowserModule.kt | 5 +- .../logindetection/DOMLoginDetector.kt | 6 +- .../app/global/events/db/UserEventsStore.kt | 6 +- .../app/global/useourapp/UseOurAppDetector.kt | 32 +++++- 9 files changed, 178 insertions(+), 14 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 73359d4fb3b1..2a84d4cb7d42 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -52,7 +52,6 @@ import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta -import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository @@ -250,7 +249,7 @@ class BrowserTabViewModelTest { mockOnboardingStore, mockUserStageStore, mockUserEventsStore, - UseOurAppDetector(), + UseOurAppDetector(mockUserEventsStore), coroutineRule.testDispatcherProvider ) @@ -289,7 +288,7 @@ class BrowserTabViewModelTest { navigationAwareLoginDetector = mockNavigationAwareLoginDetector, userEventsStore = mockUserEventsStore, notificationDao = mockNotificationDao, - useOurAppDetector = UseOurAppDetector(), + useOurAppDetector = UseOurAppDetector(mockUserEventsStore), variantManager = mockVariantManager ) @@ -1980,6 +1979,30 @@ class BrowserTabViewModelTest { } } + @Test + fun whenLoginDetectedAndUrlIsUseOurAppThenRegisterUserEvent() = coroutineRule.runBlocking { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + loginEventLiveData.value = givenLoginDetected(USE_OUR_APP_SHORTCUT_URL) + + verify(mockUserEventsStore).registerUserEvent(any()) + } + + @Test + fun whenLoginDetectedAndUrlIsNotUseOurAppThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + loginEventLiveData.value = givenLoginDetected("example.com") + + verify(mockUserEventsStore, never()).registerUserEvent(any()) + } + + @Test + fun whenLoginDetectedAndDialogAlreadySeenThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + loginEventLiveData.value = givenLoginDetected(USE_OUR_APP_SHORTCUT_URL) + + verify(mockUserEventsStore, never()).registerUserEvent(any()) + } + @Test fun whenUserBrowsingPressesBackThenCannotAddBookmark() { setupNavigation(skipHome = false, isBrowsing = true, canGoBack = false) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/JsLoginDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/JsLoginDetectorTest.kt index 8b6ee33ae6b0..348df5a773e7 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/JsLoginDetectorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/JsLoginDetectorTest.kt @@ -23,6 +23,8 @@ import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.logindetection.LoginDetectionJavascriptInterface.Companion.JAVASCRIPT_INTERFACE_NAME +import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.runBlocking import com.duckduckgo.app.settings.db.SettingsDataStore import com.nhaarman.mockitokotlin2.* @@ -38,8 +40,9 @@ class JsLoginDetectorTest { var coroutinesTestRule = CoroutineTestRule() private val settingsDataStore: SettingsDataStore = mock() + private val userEventsStore: UserEventsStore = mock() - private val testee = JsLoginDetector(settingsDataStore) + private val testee = JsLoginDetector(settingsDataStore, UseOurAppDetector(userEventsStore)) @UiThreadTest @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 44531cfdedc1..5c8f31de6174 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -148,7 +148,7 @@ class CtaViewModelTest { mockOnboardingStore, mockUserStageStore, mockUserEventsStore, - UseOurAppDetector(), + UseOurAppDetector(mockUserEventsStore), coroutineRule.testDispatcherProvider ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt index ed24a386172b..9be5192567aa 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt @@ -16,17 +16,36 @@ package com.duckduckgo.app.global.useourapp +import android.webkit.WebView +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.browser.logindetection.WebNavigationEvent +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventKey +import com.duckduckgo.app.global.events.db.UserEventsStore +import com.duckduckgo.app.runBlocking +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.* import org.junit.Before +import org.junit.Rule import org.junit.Test +@ExperimentalCoroutinesApi class UseOurAppDetectorTest { + @get:Rule + var coroutineRule = CoroutineTestRule() + private lateinit var testee: UseOurAppDetector + private val mockUserEventsStore: UserEventsStore = mock() + @Before fun setup() { - testee = UseOurAppDetector() + testee = UseOurAppDetector(mockUserEventsStore) } @Test @@ -54,4 +73,88 @@ class UseOurAppDetectorTest { assertFalse(testee.isUseOurAppUrl("http://example.com")) } + @Test + fun whenAllowLoginDetectionAndShortcutNotAddedThenReturnFalse() = coroutineRule.runBlocking { + val webView: WebView = mock() + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(null) + + assertFalse(testee.allowLoginDetection(WebNavigationEvent.OnPageStarted(webView))) + } + + @Test + fun whenAllowLoginDetectionAndFireProofAlreadySeenThenReturnFalse() = coroutineRule.runBlocking { + val webView: WebView = mock() + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + + assertFalse(testee.allowLoginDetection(WebNavigationEvent.OnPageStarted(webView))) + } + + @Test + fun whenAllowLoginDetectionWithOnPageStartedEventAndUrlIsUseOurAppThenReturnTrue() = coroutineRule.runBlocking { + val webView: WebView = mock() + whenever(webView.url).thenReturn("http://m.facebook.com") + givenShortcutIsAddedAndFireproofNotSeen() + + assertTrue(testee.allowLoginDetection(WebNavigationEvent.OnPageStarted(webView))) + } + + @Test + fun whenAllowLoginDetectionWithOnPageStartedEventAndUrlIsNotUseOurAppThenReturnFalse() = coroutineRule.runBlocking { + val webView: WebView = mock() + whenever(webView.url).thenReturn("http://example.com") + givenShortcutIsAddedAndFireproofNotSeen() + + assertFalse(testee.allowLoginDetection(WebNavigationEvent.OnPageStarted(webView))) + } + + @Test + fun whenAllowLoginDetectionWithShouldInterceptEventAndUrlIsUseOurAppThenReturnTrue() = coroutineRule.runBlocking { + val webView: WebView = mock() + whenever(webView.url).thenReturn("http://m.facebook.com") + givenShortcutIsAddedAndFireproofNotSeen() + + assertTrue(testee.allowLoginDetection(WebNavigationEvent.ShouldInterceptRequest(webView, mock()))) + } + + @Test + fun whenAllowLoginDetectionWithOnShouldInterceptEventAndUrlIsNotUseOurAppThenReturnFalse() = coroutineRule.runBlocking { + val webView: WebView = mock() + whenever(webView.url).thenReturn("http://example.com") + givenShortcutIsAddedAndFireproofNotSeen() + + assertFalse(testee.allowLoginDetection(WebNavigationEvent.ShouldInterceptRequest(webView, mock()))) + } + + @Test + fun whenRegisterIfFireproofSeenForTheFirstTimeAndUrlIsUseOurAppThenRegisterUserEvent() = coroutineRule.runBlocking { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + + testee.registerIfFireproofSeenForTheFirstTime("http://m.facebook.com") + + verify(mockUserEventsStore).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + } + + @Test + fun whenRegisterIfFireproofSeenForTheFirstTimeAndUrlIsNotUseOurAppThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + + testee.registerIfFireproofSeenForTheFirstTime("example.com") + + verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + } + + @Test + fun whenRegisterIfFireproofSeenForTheFirstTimeButAlreadySeenThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + + testee.registerIfFireproofSeenForTheFirstTime("http://m.facebook.com") + + verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + } + + private suspend fun givenShortcutIsAddedAndFireproofNotSeen() { + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 9cb955c2a5a9..2f6a384e5c3f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -291,6 +291,8 @@ class BrowserTabViewModel( private val loginDetectionObserver = Observer { loginEvent -> Timber.i("LoginDetection for $loginEvent") + viewModelScope.launch { useOurAppDetector.registerIfFireproofSeenForTheFirstTime(loginEvent.forwardedToDomain) } + if (!isFireproofWebsite(loginEvent.forwardedToDomain)) { pixel.fire(PixelName.FIREPROOF_LOGIN_DIALOG_SHOWN) command.value = AskToFireproofWebsite(FireproofWebsiteEntity(loginEvent.forwardedToDomain)) diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 7cbcdec8d694..376ac08a0d85 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -45,6 +45,7 @@ import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.file.FileDeleter import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.httpsupgrade.HttpsUpgrader import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao import com.duckduckgo.app.referral.AppReferrerDataStore @@ -220,8 +221,8 @@ class BrowserModule { } @Provides - fun domLoginDetector(settingsDataStore: SettingsDataStore): DOMLoginDetector { - return JsLoginDetector(settingsDataStore) + fun domLoginDetector(settingsDataStore: SettingsDataStore, useOurAppDetector: UseOurAppDetector): DOMLoginDetector { + return JsLoginDetector(settingsDataStore, useOurAppDetector) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/browser/logindetection/DOMLoginDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/logindetection/DOMLoginDetector.kt index 7e1f52553e6e..0ebd5c8936ed 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/logindetection/DOMLoginDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/logindetection/DOMLoginDetector.kt @@ -22,6 +22,7 @@ import android.webkit.WebView import androidx.annotation.UiThread import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.logindetection.LoginDetectionJavascriptInterface.Companion.JAVASCRIPT_INTERFACE_NAME +import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.settings.db.SettingsDataStore import timber.log.Timber import javax.inject.Inject @@ -36,7 +37,8 @@ sealed class WebNavigationEvent { data class ShouldInterceptRequest(val webView: WebView, val request: WebResourceRequest) : WebNavigationEvent() } -class JsLoginDetector @Inject constructor(private val settingsDataStore: SettingsDataStore) : DOMLoginDetector { +class JsLoginDetector @Inject constructor(private val settingsDataStore: SettingsDataStore, private val useOurAppDetector: UseOurAppDetector) : + DOMLoginDetector { private val javaScriptDetector = JavaScriptDetector() private val loginPathRegex = Regex("login|sign-in|signin|sessions") @@ -46,7 +48,7 @@ class JsLoginDetector @Inject constructor(private val settingsDataStore: Setting @UiThread override fun onEvent(event: WebNavigationEvent) { - if (settingsDataStore.appLoginDetection) { + if (settingsDataStore.appLoginDetection || useOurAppDetector.allowLoginDetection(event)) { when (event) { is WebNavigationEvent.OnPageStarted -> injectLoginFormDetectionJS(event.webView) is WebNavigationEvent.ShouldInterceptRequest -> { diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt index 0545c0e3bb80..5736c988bfcd 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt @@ -22,7 +22,7 @@ import javax.inject.Inject interface UserEventsStore { suspend fun getUserEvent(userEventKey: UserEventKey): UserEventEntity? - suspend fun registerUserEvent(timestampEntity: UserEventEntity) + suspend fun registerUserEvent(userEventEntity: UserEventEntity) } class AppUserEventsStore @Inject constructor( @@ -36,9 +36,9 @@ class AppUserEventsStore @Inject constructor( } } - override suspend fun registerUserEvent(timestampEntity: UserEventEntity) { + override suspend fun registerUserEvent(userEventEntity: UserEventEntity) { withContext(dispatcher.io()) { - userEventsDao.insert(timestampEntity) + userEventsDao.insert(userEventEntity) } } } diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt index 527ae9328d89..02347b7f3885 100644 --- a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt @@ -18,10 +18,15 @@ package com.duckduckgo.app.global.useourapp import android.net.Uri import androidx.core.net.toUri +import com.duckduckgo.app.browser.logindetection.WebNavigationEvent import com.duckduckgo.app.global.baseHost +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventKey +import com.duckduckgo.app.global.events.db.UserEventsStore +import kotlinx.coroutines.runBlocking import javax.inject.Inject -class UseOurAppDetector @Inject constructor() { +class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore) { fun isUseOurAppUrl(url: String?): Boolean { if (url == null) return false @@ -36,6 +41,31 @@ class UseOurAppDetector @Inject constructor() { return uri.baseHost == matchingUrl.toUri().baseHost } + fun allowLoginDetection(event: WebNavigationEvent): Boolean { + val canShowFireproof = runBlocking { + if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) == null) { + false + } else { + (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN) == null) + } + } + + return if (canShowFireproof) { + when (event) { + is WebNavigationEvent.OnPageStarted -> isUseOurAppUrl(event.webView.url) + is WebNavigationEvent.ShouldInterceptRequest -> isUseOurAppUrl(event.webView.url) + } + } else { + false + } + } + + suspend fun registerIfFireproofSeenForTheFirstTime(url: String) { + if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN) == null && isUseOurAppUrl(url)) { + userEventsStore.registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + } + } + companion object { const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" From 7c78d4ca18f8535a47b206c7211797177380de79 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Tue, 30 Jun 2020 18:17:54 +0100 Subject: [PATCH 28/38] Only migrate to use our app flow 10% of old users --- .../app/global/db/AppDatabaseTest.kt | 25 ++++++++++- .../onboarding/store/AppUserStageStoreTest.kt | 21 +-------- .../com/duckduckgo/app/di/DatabaseModule.kt | 6 ++- .../com/duckduckgo/app/di/VariantModule.kt | 7 +++ .../duckduckgo/app/global/db/AppDatabase.kt | 8 +++- .../useourapp/UseOurAppMigrationManager.kt | 45 +++++++++++++++++++ .../app/onboarding/store/UserStageStore.kt | 8 +--- 7 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 58b30014dac2..57c7988c12f6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -32,6 +32,7 @@ import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSource +import com.duckduckgo.app.global.useourapp.MigrationManager import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.runBlocking import com.duckduckgo.app.settings.db.SettingsDataStore @@ -62,8 +63,10 @@ class AppDatabaseTest { private val context = mock() private val mockSettingsDataStore = mock() private val mockAddToHomeCapabilityDetector = mock() + private val mockUseOurAppMigrationManager = mock() - private val migrationsProvider: MigrationsProvider = MigrationsProvider(context, mockSettingsDataStore, mockAddToHomeCapabilityDetector) + private val migrationsProvider: MigrationsProvider = + MigrationsProvider(context, mockSettingsDataStore, mockAddToHomeCapabilityDetector, mockUseOurAppMigrationManager) @Before fun setup() { @@ -231,6 +234,7 @@ class AppDatabaseTest { @Test fun whenMigratingFromVersion21To22IfUserIsEstablishedAndConditionsAreMetThenMigrateToNotification() { + whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(true) whenever(mockSettingsDataStore.hideTips).thenReturn(false) whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) val values: ContentValues = ContentValues().apply { @@ -247,6 +251,7 @@ class AppDatabaseTest { @Test fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHideTipsIsTrueThenDoNotMigrateToNotification() { + whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(true) whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) val values: ContentValues = ContentValues().apply { @@ -263,6 +268,7 @@ class AppDatabaseTest { @Test fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHomeShortcutNotSupportedThenDoNotMigrateToNotification() { + whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(true) whenever(mockSettingsDataStore.hideTips).thenReturn(false) whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(false) val values: ContentValues = ContentValues().apply { @@ -277,6 +283,23 @@ class AppDatabaseTest { } } + @Test + fun whenMigratingFromVersion21To22IfShouldNotRunMigrationThenDoNotMigrateToNotification() { + whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(false) + whenever(mockSettingsDataStore.hideTips).thenReturn(false) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) + val values: ContentValues = ContentValues().apply { + put("key", 1) + put("appStage", AppStage.ESTABLISHED.name) + } + testHelper.createDatabase(TEST_DB_NAME, 21).use { + it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) + val stage = getUserStage(it) + assertEquals(AppStage.ESTABLISHED.name, stage) + } + } + private fun getUserStage(database: SupportSQLiteDatabase): String { var stage = "" database.query("SELECT appStage from userStage limit 1").use { diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt index 5efeb5e623e4..4c52fdb701be 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt @@ -59,7 +59,7 @@ class AppUserStageStoreTest { } @Test - fun whenStageDaxOnboardingCompletedAndNotAbleToAddShortcutsThenStageEstablishedReturned() = coroutineRule.runBlocking { + fun whenStageDaxOnboardingCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { givenCurrentStage(AppStage.DAX_ONBOARDING) val nextStage = testee.stageCompleted(AppStage.DAX_ONBOARDING) @@ -67,25 +67,6 @@ class AppUserStageStoreTest { assertEquals(AppStage.ESTABLISHED, nextStage) } - @Test - fun whenStageDaxOnboardingCompletedAndAbleToAddShortcutsThenStageUseOurAppNotificationReturned() = coroutineRule.runBlocking { - givenCurrentStage(AppStage.DAX_ONBOARDING) - whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) - val nextStage = testee.stageCompleted(AppStage.DAX_ONBOARDING) - - assertEquals(AppStage.USE_OUR_APP_NOTIFICATION, nextStage) - } - - @Test - fun whenStageDaxOnboardingCompletedAndAbleToAddShortcutsButHideTipsIsTrueThenStageEstablishedReturned() = coroutineRule.runBlocking { - givenCurrentStage(AppStage.DAX_ONBOARDING) - whenever(mockSettingsDataStore.hideTips).thenReturn(true) - whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) - val nextStage = testee.stageCompleted(AppStage.DAX_ONBOARDING) - - assertEquals(AppStage.ESTABLISHED, nextStage) - } - @Test fun whenStageUseOurAppNotificationCompletedThenStageEstablishedReturned() = coroutineRule.runBlocking { givenCurrentStage(AppStage.USE_OUR_APP_NOTIFICATION) diff --git a/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt b/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt index 836d1867c5a7..088f39bb3bc2 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DatabaseModule.kt @@ -21,6 +21,7 @@ import androidx.room.Room import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.db.MigrationsProvider +import com.duckduckgo.app.global.useourapp.UseOurAppMigrationManager import com.duckduckgo.app.settings.db.SettingsDataStore import dagger.Module import dagger.Provides @@ -41,8 +42,9 @@ class DatabaseModule { fun provideDatabaseMigrations( context: Context, settingsDataStore: SettingsDataStore, - addToHomeCapabilityDetector: AddToHomeCapabilityDetector + addToHomeCapabilityDetector: AddToHomeCapabilityDetector, + useOurAppMigrationManager: UseOurAppMigrationManager ): MigrationsProvider { - return MigrationsProvider(context, settingsDataStore, addToHomeCapabilityDetector) + return MigrationsProvider(context, settingsDataStore, addToHomeCapabilityDetector, useOurAppMigrationManager) } } diff --git a/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt b/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt index 6d216e7ff295..fc5d0a5b2f7e 100644 --- a/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.di +import com.duckduckgo.app.global.useourapp.UseOurAppMigrationManager import com.duckduckgo.app.statistics.ExperimentationVariantManager import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.WeightedRandomizer @@ -34,4 +35,10 @@ class VariantModule { @Provides fun weightedRandomizer() = WeightedRandomizer() + + @Provides + @Singleton + fun useOurAppMigrationManager(weightedRandomizer: WeightedRandomizer): UseOurAppMigrationManager { + return UseOurAppMigrationManager(weightedRandomizer) + } } diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 9fdb9024a3b3..db4484f97b4a 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -39,6 +39,7 @@ import com.duckduckgo.app.global.exception.UncaughtExceptionSourceConverter import com.duckduckgo.app.global.events.db.UserEventsDao import com.duckduckgo.app.global.events.db.UserEventEntity import com.duckduckgo.app.global.events.db.UserEventTypeConverter +import com.duckduckgo.app.global.useourapp.MigrationManager import com.duckduckgo.app.httpsupgrade.db.HttpsBloomFilterSpecDao import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao import com.duckduckgo.app.httpsupgrade.model.HttpsBloomFilterSpec @@ -133,7 +134,8 @@ abstract class AppDatabase : RoomDatabase() { class MigrationsProvider( val context: Context, val settingsDataStore: SettingsDataStore, - val addToHomeCapabilityDetector: AddToHomeCapabilityDetector + val addToHomeCapabilityDetector: AddToHomeCapabilityDetector, + val useOurAppMigrationManager: MigrationManager ) { val MIGRATION_1_TO_2: Migration = object : Migration(1, 2) { @@ -309,7 +311,9 @@ class MigrationsProvider( database.execSQL("CREATE TABLE IF NOT EXISTS `user_events` (`id` TEXT NOT NULL PRIMARY KEY, `timestamp` INTEGER NOT NULL)") if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { - database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") + if (useOurAppMigrationManager.shouldRunMigration()) { + database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") + } } } } diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt new file mode 100644 index 000000000000..7a8628a199b9 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt @@ -0,0 +1,45 @@ +/* + * 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.global.useourapp + +import com.duckduckgo.app.statistics.IndexRandomizer +import com.duckduckgo.app.statistics.Probabilistic + +interface MigrationManager { + fun shouldRunMigration(): Boolean +} + +class UseOurAppMigrationManager constructor(private val indexRandomizer: IndexRandomizer) : MigrationManager { + + override fun shouldRunMigration(): Boolean { + val randomizedIndex = indexRandomizer.random(MIGRATION_VARIANTS) + val variant = MIGRATION_VARIANTS[randomizedIndex] + return (variant == useOurAppMigration) + } + + companion object { + private val defaultMigration = MigrationWeight(0.9) // do not migrate 90% of old users + private val useOurAppMigration = MigrationWeight(0.1) // migrate 10% of old users + + val MIGRATION_VARIANTS = listOf( + defaultMigration, + useOurAppMigration + ) + } +} + +data class MigrationWeight(override val weight: Double) : Probabilistic diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 76869177bf90..3365ac9fa9dc 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -45,13 +45,7 @@ class AppUserStageStore @Inject constructor( return withContext(dispatcher.io()) { val newAppStage = when (appStage) { AppStage.NEW -> AppStage.DAX_ONBOARDING - AppStage.DAX_ONBOARDING -> { - if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { - AppStage.USE_OUR_APP_NOTIFICATION - } else { - AppStage.ESTABLISHED - } - } + AppStage.DAX_ONBOARDING -> AppStage.ESTABLISHED AppStage.USE_OUR_APP_NOTIFICATION -> AppStage.ESTABLISHED AppStage.USE_OUR_APP_ONBOARDING -> AppStage.ESTABLISHED AppStage.ESTABLISHED -> AppStage.ESTABLISHED From c1c77c851b5d9a0ed26c74e66a5c3f412569f724 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 1 Jul 2020 10:46:15 +0100 Subject: [PATCH 29/38] Amend code as per PR --- .../app/browser/BrowserTabViewModelTest.kt | 95 +++++++++++++++---- .../app/browser/BrowserViewModelTest.kt | 2 +- .../browser/shortcut/ShortcutReceiverTest.kt | 5 +- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 2 +- .../app/global/db/AppDatabaseTest.kt | 83 ++++++++-------- .../app/global/events/db/UserEventsDaoTest.kt | 6 +- .../global/useourapp/UseOurAppDetectorTest.kt | 16 ++-- .../NotificationHandlerServiceTest.kt | 2 +- .../onboarding/store/AppUserStageStoreTest.kt | 4 +- .../app/tabs/model/TabDataRepositoryTest.kt | 34 +++++-- .../app/browser/BrowserTabViewModel.kt | 5 +- .../app/browser/shortcut/ShortcutBuilder.kt | 4 +- .../app/global/DuckDuckGoApplication.kt | 2 +- .../duckduckgo/app/global/db/AppDatabase.kt | 2 +- .../app/global/events/db/UserEventEntity.kt | 6 +- .../app/global/events/db/UserEventsDao.kt | 2 +- .../app/global/useourapp/UseOurAppDetector.kt | 6 +- .../NotificationHandlerService.kt | 2 +- .../app/onboarding/store/UserStageStore.kt | 4 +- 19 files changed, 183 insertions(+), 99 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 2a84d4cb7d42..890d42b6ec49 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -339,7 +339,7 @@ class BrowserTabViewModelTest { } @Test - fun whenViewBecomesVisibleAndHomeShowingAndUserIsInUseOurAppOnboardingStageThenKeyboardShown() = coroutineRule.runBlocking { + fun whenViewBecomesVisibleAndHomeShowingAndUserIsNotInUseOurAppOnboardingStageThenKeyboardShown() = coroutineRule.runBlocking { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) setBrowserShowing(false) @@ -349,7 +349,7 @@ class BrowserTabViewModelTest { } @Test - fun whenViewBecomesVisibleAndHomeShowingAndUserIsNotInUseOurAppOnboardingStageThenKeyboardHidden() = coroutineRule.runBlocking { + fun whenViewBecomesVisibleAndHomeShowingAndUserIsInUseOurAppOnboardingStageThenKeyboardHidden() = coroutineRule.runBlocking { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_ONBOARDING) setBrowserShowing(false) @@ -1981,7 +1981,7 @@ class BrowserTabViewModelTest { @Test fun whenLoginDetectedAndUrlIsUseOurAppThenRegisterUserEvent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) loginEventLiveData.value = givenLoginDetected(USE_OUR_APP_SHORTCUT_URL) verify(mockUserEventsStore).registerUserEvent(any()) @@ -1989,7 +1989,7 @@ class BrowserTabViewModelTest { @Test fun whenLoginDetectedAndUrlIsNotUseOurAppThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) loginEventLiveData.value = givenLoginDetected("example.com") verify(mockUserEventsStore, never()).registerUserEvent(any()) @@ -1997,7 +1997,7 @@ class BrowserTabViewModelTest { @Test fun whenLoginDetectedAndDialogAlreadySeenThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) loginEventLiveData.value = givenLoginDetected(USE_OUR_APP_SHORTCUT_URL) verify(mockUserEventsStore, never()).registerUserEvent(any()) @@ -2149,7 +2149,9 @@ class BrowserTabViewModelTest { fun whenViewReadyIfDomainSameAsUseOurAppAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) + testee.onViewReady() + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) } @@ -2157,7 +2159,9 @@ class BrowserTabViewModelTest { fun whenViewReadyIfDomainSameAsUseOurAppAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + testee.onViewReady() + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @@ -2165,53 +2169,100 @@ class BrowserTabViewModelTest { fun whenViewReadyIfDomainSameAsUseOurAppAfterDeleteCtaShownThenPixelSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) + testee.onViewReady() + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) } @Test - fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking { + fun whenViewReadyIfDomainIsNotTheSameAsUseOurAppAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking { + givenUseOurAppSiteIsNotSelected() + whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) + + testee.onViewReady() + + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) + } + + @Test + fun whenViewReadyIfDomainIsNotTheSameAsUseOurAppAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { + givenUseOurAppSiteIsNotSelected() + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + + testee.onViewReady() + + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) + } + + @Test + fun whenViewReadyIfDomainIsNotTheSameAsUseOurAppAfterDeleteCtaShownThenPixelNotSent() = coroutineRule.runBlocking { + givenUseOurAppSiteIsNotSelected() + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) + + testee.onViewReady() + + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) + } + + @Test + fun whenPageChangedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking { + givenUseOurAppSiteIsNotSelected() whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) - testee.pageRefreshed(USE_OUR_APP_DOMAIN) + + loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true) + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) } @Test - fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { + fun whenPageChangedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelSent() = coroutineRule.runBlocking { + givenUseOurAppSiteIsNotSelected() whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) - testee.pageRefreshed(USE_OUR_APP_DOMAIN) + + loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true) + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @Test - fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterDeleteCtaShownThenPixelSent() = coroutineRule.runBlocking { + fun whenPageChangedIfPreviousOneWasNotUseOurAppSiteAfterDeleteCtaShownThenPixelSent() = coroutineRule.runBlocking { + givenUseOurAppSiteIsNotSelected() whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) - testee.pageRefreshed(USE_OUR_APP_DOMAIN) + + loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true) + verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) } @Test - fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking { + fun whenPageChangedIfPreviousOneWasUseOurAppSiteAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockNotificationDao.exists(UseOurAppNotification.ID)).thenReturn(true) - testee.pageRefreshed(USE_OUR_APP_DOMAIN) + + loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true) + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_NOTIFICATION) } @Test - fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { + fun whenPageChangedIfPreviousOneWasUseOurAppSiteAfterShortcutAddedThenPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) - testee.pageRefreshed(USE_OUR_APP_DOMAIN) + + loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true) + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_SHORTCUT) } @Test - fun whenPageRefreshedIfPreviousOneWasNotUseOurAppSiteThenAfterDeleteCtaShownPixelNotSent() = coroutineRule.runBlocking { + fun whenPageChangedIfPreviousOneWasUseOurAppSiteThenAfterDeleteCtaShownPixelNotSent() = coroutineRule.runBlocking { givenUseOurAppSiteSelected() whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP_DELETION)).thenReturn(true) - testee.pageRefreshed(USE_OUR_APP_DOMAIN) + + loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true) + verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA) } @@ -2264,6 +2315,16 @@ class BrowserTabViewModelTest { testee.loadData("TAB_ID", USE_OUR_APP_DOMAIN, false) } + private fun givenUseOurAppSiteIsNotSelected() { + whenever(mockOmnibarConverter.convertQueryToUrl("example.com", null)).thenReturn("example.com") + val site: Site = mock() + whenever(site.url).thenReturn("example.com") + val siteLiveData = MutableLiveData() + siteLiveData.value = site + whenever(mockTabsRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + testee.loadData("TAB_ID", "example.com", false) + } + private fun givenFireproofWebsiteDomain(vararg fireproofWebsitesDomain: String) { fireproofWebsitesDomain.forEach { fireproofWebsiteDao.insert(FireproofWebsiteEntity(domain = it)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index e8a521eef1bb..6dd4b1636438 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -186,7 +186,7 @@ class BrowserViewModelTest { } @Test - fun whenOpenShortcutIfUrlIsUSeOurAppUrlThenFirePixel() { + fun whenOpenShortcutIfUrlIsUseOurAppUrlThenFirePixel() { val url = USE_OUR_APP_SHORTCUT_URL whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) testee.onOpenShortcut(url) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index 94d6e1735c9f..803cc9bc98ad 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -26,6 +26,7 @@ import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyZeroInteractions import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Rule @@ -67,7 +68,7 @@ class ShortcutReceiverTest { } @Test - fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotRegisterTimestamp() = coroutinesTestRule.runBlocking { + fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotRegisterEvent() = coroutinesTestRule.runBlocking { val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") @@ -83,6 +84,6 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) + verifyZeroInteractions(mockPixel) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 5c8f31de6174..d37e3b56d8cb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -487,7 +487,7 @@ class CtaViewModelTest { } @Test - fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { + fun whenRefreshCtaWhileBrowsingAndSiteIsUseOurAppAndLessThanTwoDaysSinceShortcutAddedThenReturnNull() = runBlockingTest { givenUserIsEstablished() val timestampEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(timestampEntity) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 57c7988c12f6..e16f0f89e586 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -197,8 +197,9 @@ class AppDatabaseTest { @Test fun whenMigratingFromVersion17To18IfUserDidNotSeeOnboardingThenMigrateToNew() = coroutineRule.runBlocking { givenUserNeverSawOnboarding() - createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) - assertEquals(AppStage.NEW, database().userStageDao().currentUserAppStage()?.appStage) + val database = createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) + val stage = getUserStage(database) + assertEquals(AppStage.NEW.name, stage) } @Test @@ -234,77 +235,81 @@ class AppDatabaseTest { @Test fun whenMigratingFromVersion21To22IfUserIsEstablishedAndConditionsAreMetThenMigrateToNotification() { - whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(true) - whenever(mockSettingsDataStore.hideTips).thenReturn(false) - whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) - val values: ContentValues = ContentValues().apply { - put("key", 1) - put("appStage", AppStage.ESTABLISHED.name) - } testHelper.createDatabase(TEST_DB_NAME, 21).use { - it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = true) + givenUserIsEstablished(it) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) + assertEquals(AppStage.USE_OUR_APP_NOTIFICATION.name, stage) } } @Test fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHideTipsIsTrueThenDoNotMigrateToNotification() { - whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(true) - whenever(mockSettingsDataStore.hideTips).thenReturn(true) - whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) - val values: ContentValues = ContentValues().apply { - put("key", 1) - put("appStage", AppStage.ESTABLISHED.name) - } testHelper.createDatabase(TEST_DB_NAME, 21).use { - it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + givenUseOurAppStateIs(canMigrate = true, hideTips = true, canAddToHome = true) + givenUserIsEstablished(it) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) + assertEquals(AppStage.ESTABLISHED.name, stage) } } @Test fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHomeShortcutNotSupportedThenDoNotMigrateToNotification() { - whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(true) - whenever(mockSettingsDataStore.hideTips).thenReturn(false) - whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(false) - val values: ContentValues = ContentValues().apply { - put("key", 1) - put("appStage", AppStage.ESTABLISHED.name) - } testHelper.createDatabase(TEST_DB_NAME, 21).use { - it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = false) + givenUserIsEstablished(it) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) + assertEquals(AppStage.ESTABLISHED.name, stage) } } @Test - fun whenMigratingFromVersion21To22IfShouldNotRunMigrationThenDoNotMigrateToNotification() { - whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(false) - whenever(mockSettingsDataStore.hideTips).thenReturn(false) - whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(true) - val values: ContentValues = ContentValues().apply { - put("key", 1) - put("appStage", AppStage.ESTABLISHED.name) - } + fun whenMigratingFromVersion21To22IfShouldNotRunMigrationThenDoNotMigrateToNotification2() { testHelper.createDatabase(TEST_DB_NAME, 21).use { - it.insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + givenUseOurAppStateIs(canMigrate = false) + givenUserIsEstablished(it) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) + assertEquals(AppStage.ESTABLISHED.name, stage) } } + private fun givenUseOurAppStateIs(canMigrate: Boolean = true, hideTips: Boolean = false, canAddToHome: Boolean = true) { + whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(canMigrate) + whenever(mockSettingsDataStore.hideTips).thenReturn(hideTips) + whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(canAddToHome) + } + + private fun givenUserIsEstablished(database: SupportSQLiteDatabase) { + val values: ContentValues = ContentValues().apply { + put("key", 1) + put("appStage", AppStage.ESTABLISHED.name) + } + + // Do not close the database + database.apply { + insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) + } + } + private fun getUserStage(database: SupportSQLiteDatabase): String { var stage = "" - database.query("SELECT appStage from userStage limit 1").use { - it.moveToFirst() - stage = it.getString(0) + + // Do not close the database + database.query("SELECT appStage from userStage limit 1").apply { + moveToFirst() + stage = getString(0) } return stage } @@ -322,7 +327,7 @@ class AppDatabaseTest { return runMigrations(newVersion, *migrations) } - @Deprecated("Don't use anymore, instead execute a query directly to the database, see getUserStage as an example") + @Deprecated("Don't use anymore, instead execute a query directly to the database, see getUserStage as an example. Using this methods runs all the migrations using the latest schema version and cannot be used to validate intermediate migrations") private fun database(): AppDatabase { val database = Room .databaseBuilder(getInstrumentation().targetContext, AppDatabase::class.java, TEST_DB_NAME) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt index 0155d139c470..2ab6adc890a3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt @@ -59,13 +59,13 @@ class UserEventsDaoTest { } @Test - fun whenGetTimestampAndDatabaseEmptyThenReturnNull() = coroutineRule.runBlocking { + fun whenGetUserEventAndDatabaseEmptyThenReturnNull() = coroutineRule.runBlocking { val value = testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) assertNull(value) } @Test - fun whenInsertingTimestampThenReturnSameTimestamp() = coroutineRule.runBlocking { + fun whenInsertingUserEventThenReturnSameTimestamp() = coroutineRule.runBlocking { val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) testee.registerUserEvent(entity) @@ -73,7 +73,7 @@ class UserEventsDaoTest { } @Test - fun whenInsertingSameTimestampThenReplaceOldTimestampWithTheNew() = coroutineRule.runBlocking { + fun whenInsertingSameUserEventThenReplaceOldTimestampWithTheNew() = coroutineRule.runBlocking { val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) val newEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(1)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt index 9be5192567aa..6519369b63b5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt @@ -85,7 +85,7 @@ class UseOurAppDetectorTest { fun whenAllowLoginDetectionAndFireProofAlreadySeenThenReturnFalse() = coroutineRule.runBlocking { val webView: WebView = mock() whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) assertFalse(testee.allowLoginDetection(WebNavigationEvent.OnPageStarted(webView))) } @@ -128,33 +128,33 @@ class UseOurAppDetectorTest { @Test fun whenRegisterIfFireproofSeenForTheFirstTimeAndUrlIsUseOurAppThenRegisterUserEvent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) testee.registerIfFireproofSeenForTheFirstTime("http://m.facebook.com") - verify(mockUserEventsStore).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + verify(mockUserEventsStore).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) } @Test fun whenRegisterIfFireproofSeenForTheFirstTimeAndUrlIsNotUseOurAppThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) testee.registerIfFireproofSeenForTheFirstTime("example.com") - verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) } @Test fun whenRegisterIfFireproofSeenForTheFirstTimeButAlreadySeenThenDoNotRegisterUserEvent() = coroutineRule.runBlocking { - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) testee.registerIfFireproofSeenForTheFirstTime("http://m.facebook.com") - verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) } private suspend fun givenShortcutIsAddedAndFireproofNotSeen() { whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) - whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)).thenReturn(null) + whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt index 11388275dcc3..43c7eac4e8eb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/NotificationHandlerServiceTest.kt @@ -90,6 +90,6 @@ class NotificationHandlerServiceTest { intent.type = USE_OUR_APP intent.putExtra(PIXEL_SUFFIX_EXTRA, "abc") testee.onHandleIntent(intent) - verify(mockUserStageStore).registerInStage(AppStage.USE_OUR_APP_ONBOARDING) + verify(mockUserStageStore).moveToStage(AppStage.USE_OUR_APP_ONBOARDING) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt index 4c52fdb701be..d9772d057555 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt @@ -95,8 +95,8 @@ class AppUserStageStoreTest { } @Test - fun whenRegisterInStageThenUpdateUserStageInDao() = coroutineRule.runBlocking { - testee.registerInStage(AppStage.USE_OUR_APP_ONBOARDING) + fun whenMoveToStageThenUpdateUserStageInDao() = coroutineRule.runBlocking { + testee.moveToStage(AppStage.USE_OUR_APP_ONBOARDING) verify(userStageDao).updateUserStage(AppStage.USE_OUR_APP_ONBOARDING) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt index 4889c0c309b9..2807e5d9583d 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt @@ -20,10 +20,14 @@ package com.duckduckgo.app.tabs.model import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData +import androidx.room.Room import androidx.test.annotation.UiThreadTest +import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.InstantSchedulersRule +import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister +import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.privacy.model.PrivacyPractices @@ -231,23 +235,39 @@ class TabDataRepositoryTest { @Test fun whenSelectByUrlOrNewTabIfUrlAlreadyExistedInATabThenSelectTheTab() = runBlocking { - val url = "http://www.example.com" + val db = createDatabase() + val dao = db.tabsDao() + dao.insertTab(TabEntity(tabId = "id", url = "http://www.example.com", skipHome = false, viewed = true, position = 0)) - whenever(mockDao.selectTabByUrl(url)).thenReturn("tabid") + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister) - testee.selectByUrlOrNewTab(url) + testee.selectByUrlOrNewTab("http://www.example.com") + + val value = testee.liveSelectedTab.blockingObserve()?.tabId + assertEquals("id", value) - verify(mockDao).insertTabSelection(TabSelectionEntity(tabId = "tabid")) + db.close() } @Test fun whenSelectByUrlOrNewTabIfUrlNotExistedInATabThenAddNewTab() = runBlocking { - whenever(mockDao.tabs()).thenReturn(emptyList()) + val db = createDatabase() + val dao = db.tabsDao() + + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister) testee.selectByUrlOrNewTab("http://www.example.com") - val captor = argumentCaptor() - verify(mockDao).addAndSelectTab(captor.capture()) + val value = testee.liveSelectedTab.blockingObserve()?.url + assertEquals("http://www.example.com", value) + + db.close() + } + + private fun createDatabase(): AppDatabase { + return Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) + .allowMainThreadQueries() + .build() } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 2f6a384e5c3f..81a815ceaf3b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -60,7 +60,6 @@ import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener -import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxDialogCta @@ -1252,12 +1251,12 @@ class BrowserTabViewModel( is HomePanelCta.Survey -> LaunchSurvey(cta.survey) is HomePanelCta.AddWidgetAuto -> LaunchAddWidget is HomePanelCta.AddWidgetInstructions -> LaunchLegacyAddWidget - is UseOurAppCta -> navigateToUrlAndAddShortcut(url = USE_OUR_APP_SHORTCUT_URL, title = USE_OUR_APP_SHORTCUT_TITLE) + is UseOurAppCta -> navigateToUrlAndLaunchShortcut(url = USE_OUR_APP_SHORTCUT_URL, title = USE_OUR_APP_SHORTCUT_TITLE) else -> return } } - private fun navigateToUrlAndAddShortcut(url: String, title: String): AddHomeShortcut { + private fun navigateToUrlAndLaunchShortcut(url: String, title: String): AddHomeShortcut { onUserSubmittedQuery(url) return AddHomeShortcut(title, url) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt index a1c4bd97e166..b65aee79a86c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt @@ -52,7 +52,7 @@ class ShortcutBuilder @Inject constructor() { } private fun buildPendingIntent(context: Context, url: String, title: String): PendingIntent? { - val pinnedShortcutCallbackIntent = Intent(USE_OUR_APP_SHORTCUT_ADDED) + val pinnedShortcutCallbackIntent = Intent(USE_OUR_APP_SHORTCUT_ADDED_ACTION) pinnedShortcutCallbackIntent.putExtra(SHORTCUT_URL_ARG, url) pinnedShortcutCallbackIntent.putExtra(SHORTCUT_TITLE_ARG, title) return PendingIntent.getBroadcast(context, USE_OUR_APP_SHORTCUT_ADDED_CODE, pinnedShortcutCallbackIntent, FLAG_UPDATE_CURRENT) @@ -66,7 +66,7 @@ class ShortcutBuilder @Inject constructor() { } companion object { - const val USE_OUR_APP_SHORTCUT_ADDED: String = "useOurAppShortcutAdded" + const val USE_OUR_APP_SHORTCUT_ADDED_ACTION: String = "useOurAppShortcutAdded" const val USE_OUR_APP_SHORTCUT_ADDED_CODE = 9000 const val SHORTCUT_EXTRA_ARG = "shortCutAdded" diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt index 4adde8a7b47a..361e2cd36b8c 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt @@ -299,7 +299,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO Timber.i("Suppressing app launch pixel") return } - registerReceiver(shortcutReceiver, IntentFilter(ShortcutBuilder.USE_OUR_APP_SHORTCUT_ADDED)) + registerReceiver(shortcutReceiver, IntentFilter(ShortcutBuilder.USE_OUR_APP_SHORTCUT_ADDED_ACTION)) pixel.fire(APP_LAUNCH) } diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index db4484f97b4a..24ca008d3b0a 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -308,7 +308,7 @@ class MigrationsProvider( val MIGRATION_21_TO_22: Migration = object : Migration(21, 22) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `user_events` (`id` TEXT NOT NULL PRIMARY KEY, `timestamp` INTEGER NOT NULL)") + database.execSQL("CREATE TABLE IF NOT EXISTS `user_events` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))") if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { if (useOurAppMigrationManager.shouldRunMigration()) { diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt index ceac3b81e4aa..17dfab2abfbf 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventEntity.kt @@ -20,9 +20,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverter -const val USER_EVENTS_TABLE_NAME = "user_events" - -@Entity(tableName = USER_EVENTS_TABLE_NAME) +@Entity(tableName = "user_events") data class UserEventEntity( @PrimaryKey val id: UserEventKey, val timestamp: Long = System.currentTimeMillis() @@ -30,7 +28,7 @@ data class UserEventEntity( enum class UserEventKey { USE_OUR_APP_SHORTCUT_ADDED, - USE_OUR_APP_FIREPROOF_SEEN + USE_OUR_APP_FIREPROOF_DIALOG_SEEN } class UserEventTypeConverter { diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt index d611cc8ab73c..dea2a6db5d35 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsDao.kt @@ -24,7 +24,7 @@ import androidx.room.Query @Dao interface UserEventsDao { - @Query("select * from $USER_EVENTS_TABLE_NAME where id=:userEventKey") + @Query("select * from user_events where id=:userEventKey") suspend fun getUserEvent(userEventKey: UserEventKey): UserEventEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt index 02347b7f3885..a665f130c428 100644 --- a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt @@ -46,7 +46,7 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) == null) { false } else { - (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN) == null) + (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) == null) } } @@ -61,8 +61,8 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore } suspend fun registerIfFireproofSeenForTheFirstTime(url: String) { - if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN) == null && isUseOurAppUrl(url)) { - userEventsStore.registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_SEEN)) + if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) == null && isUseOurAppUrl(url)) { + userEventsStore.registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) } } diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt index 7ae2db73abf3..0b4cb8be138a 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationHandlerService.kt @@ -86,7 +86,7 @@ class NotificationHandlerService : IntentService("NotificationHandlerService") { CHANGE_ICON_FEATURE -> onCustomizeIconLaunched(pixelSuffix) USE_OUR_APP -> { GlobalScope.launch(dispatcher.io()) { - userStageStore.registerInStage(AppStage.USE_OUR_APP_ONBOARDING) + userStageStore.moveToStage(AppStage.USE_OUR_APP_ONBOARDING) onAppLaunched(pixelSuffix) } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 3365ac9fa9dc..8cc9dd594bd7 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -25,7 +25,7 @@ import javax.inject.Inject interface UserStageStore { suspend fun getUserAppStage(): AppStage suspend fun stageCompleted(appStage: AppStage): AppStage - suspend fun registerInStage(appStage: AppStage) + suspend fun moveToStage(appStage: AppStage) } class AppUserStageStore @Inject constructor( @@ -59,7 +59,7 @@ class AppUserStageStore @Inject constructor( } } - override suspend fun registerInStage(appStage: AppStage) { + override suspend fun moveToStage(appStage: AppStage) { userStageDao.updateUserStage(appStage) } } From 698ced876372eb44bba163634ccda3ca81de8566 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 1 Jul 2020 11:21:34 +0100 Subject: [PATCH 30/38] Do not rely on actual timestamps for tests --- .../app/browser/BrowserTabViewModelTest.kt | 6 ++--- .../browser/shortcut/ShortcutReceiverTest.kt | 6 ++--- .../app/global/events/db/UserEventsDaoTest.kt | 23 ++++++++----------- .../global/useourapp/UseOurAppDetectorTest.kt | 6 ++--- .../app/browser/BrowserTabViewModel.kt | 10 ++++---- .../app/browser/shortcut/ShortcutReceiver.kt | 3 +-- .../app/global/events/db/UserEventsStore.kt | 6 ++--- .../app/global/useourapp/UseOurAppDetector.kt | 10 ++++---- 8 files changed, 31 insertions(+), 39 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 890d42b6ec49..b778cca00b13 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1984,7 +1984,7 @@ class BrowserTabViewModelTest { whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) loginEventLiveData.value = givenLoginDetected(USE_OUR_APP_SHORTCUT_URL) - verify(mockUserEventsStore).registerUserEvent(any()) + verify(mockUserEventsStore).registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } @Test @@ -1992,7 +1992,7 @@ class BrowserTabViewModelTest { whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(null) loginEventLiveData.value = givenLoginDetected("example.com") - verify(mockUserEventsStore, never()).registerUserEvent(any()) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } @Test @@ -2000,7 +2000,7 @@ class BrowserTabViewModelTest { whenever(mockUserEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)).thenReturn(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) loginEventLiveData.value = givenLoginDetected(USE_OUR_APP_SHORTCUT_URL) - verify(mockUserEventsStore, never()).registerUserEvent(any()) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index 803cc9bc98ad..0dbcc9e2b7be 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -18,11 +18,11 @@ package com.duckduckgo.app.browser.shortcut import android.content.Intent import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel -import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify @@ -54,7 +54,7 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockUserEventsStore).registerUserEvent(any()) + verify(mockUserEventsStore).registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) } @Test @@ -74,7 +74,7 @@ class ShortcutReceiverTest { intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verify(mockUserEventsStore, never()).registerUserEvent(any()) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt index 2ab6adc890a3..1e82c01385a8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/events/db/UserEventsDaoTest.kt @@ -25,11 +25,9 @@ import com.duckduckgo.app.runBlocking import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Test - import org.junit.Assert.* import org.junit.Before import org.junit.Rule -import java.util.concurrent.TimeUnit @ExperimentalCoroutinesApi class UserEventsDaoTest { @@ -60,26 +58,23 @@ class UserEventsDaoTest { @Test fun whenGetUserEventAndDatabaseEmptyThenReturnNull() = coroutineRule.runBlocking { - val value = testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - assertNull(value) + assertNull(testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) } @Test - fun whenInsertingUserEventThenReturnSameTimestamp() = coroutineRule.runBlocking { - val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - testee.registerUserEvent(entity) + fun whenInsertingUserEventThenTimestampIsNotNull() = coroutineRule.runBlocking { + testee.registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - assertEquals(entity.timestamp, testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + assertNotNull(testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) } @Test - fun whenInsertingSameUserEventThenReplaceOldTimestampWithTheNew() = coroutineRule.runBlocking { - val entity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - val newEntity = UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED, System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(1)) + fun whenInsertingSameUserEventThenReplaceOldTimestamp() = coroutineRule.runBlocking { + testee.registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) + val timestamp = testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp - testee.registerUserEvent(entity) - testee.registerUserEvent(newEntity) + testee.registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) - assertEquals(newEntity.timestamp, testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) + assertNotEquals(timestamp, testee.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)?.timestamp) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt index 6519369b63b5..5e69dd985b64 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/useourapp/UseOurAppDetectorTest.kt @@ -132,7 +132,7 @@ class UseOurAppDetectorTest { testee.registerIfFireproofSeenForTheFirstTime("http://m.facebook.com") - verify(mockUserEventsStore).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) + verify(mockUserEventsStore).registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } @Test @@ -141,7 +141,7 @@ class UseOurAppDetectorTest { testee.registerIfFireproofSeenForTheFirstTime("example.com") - verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } @Test @@ -150,7 +150,7 @@ class UseOurAppDetectorTest { testee.registerIfFireproofSeenForTheFirstTime("http://m.facebook.com") - verify(mockUserEventsStore, never()).registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) + verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } private suspend fun givenShortcutIsAddedAndFireproofNotSeen() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index cfc9ba642537..b820c74379d4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -315,7 +315,7 @@ class BrowserTabViewModel( fun onViewReady() { url?.let { - isDomainSameAsUseOurAppDomain(it) + sendPixelIfUserOurAppSiteVisitedFirstTime(it) onUserSubmittedQuery(it) } } @@ -616,7 +616,7 @@ class BrowserTabViewModel( // Navigating from different website to use our app website if (!useOurAppDetector.isUseOurAppUrl(oldUrl)) { - isDomainSameAsUseOurAppDomain(url) + sendPixelIfUserOurAppSiteVisitedFirstTime(url) } command.value = RefreshUserAgent(site?.uri?.host, currentBrowserViewState().isDesktopBrowsingMode) @@ -657,13 +657,13 @@ class BrowserTabViewModel( registerSiteVisit() } - private fun isDomainSameAsUseOurAppDomain(url: String) { + private fun sendPixelIfUserOurAppSiteVisitedFirstTime(url: String) { if (useOurAppDetector.isUseOurAppUrl(url)) { - viewModelScope.launch { sendPixelIfUseOurAppSiteVisited() } + viewModelScope.launch { sendUseOurAppSiteVisitedPixel() } } } - private suspend fun sendPixelIfUseOurAppSiteVisited() { + private suspend fun sendUseOurAppSiteVisitedPixel() { withContext(dispatchers.io()) { val isShortcutAdded = userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) val isUseOurAppNotificationSeen = notificationDao.exists(UseOurAppNotification.ID) diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index 0d39364d1a6f..7401f57cd907 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -25,7 +25,6 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TITLE_ARG import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG import com.duckduckgo.app.global.DispatcherProvider -import com.duckduckgo.app.global.events.db.UserEventEntity import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL @@ -49,7 +48,7 @@ class ShortcutReceiver @Inject constructor(private val userEventsStore: UserEven GlobalScope.launch(dispatcher.io()) { if (originUrl == USE_OUR_APP_SHORTCUT_URL) { - userEventsStore.registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)) + userEventsStore.registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) } } diff --git a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt index 5736c988bfcd..e1777f25346f 100644 --- a/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt +++ b/app/src/main/java/com/duckduckgo/app/global/events/db/UserEventsStore.kt @@ -22,7 +22,7 @@ import javax.inject.Inject interface UserEventsStore { suspend fun getUserEvent(userEventKey: UserEventKey): UserEventEntity? - suspend fun registerUserEvent(userEventEntity: UserEventEntity) + suspend fun registerUserEvent(userEventKey: UserEventKey) } class AppUserEventsStore @Inject constructor( @@ -36,9 +36,9 @@ class AppUserEventsStore @Inject constructor( } } - override suspend fun registerUserEvent(userEventEntity: UserEventEntity) { + override suspend fun registerUserEvent(userEventKey: UserEventKey) { withContext(dispatcher.io()) { - userEventsDao.insert(userEventEntity) + userEventsDao.insert(UserEventEntity(userEventKey)) } } } diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt index a665f130c428..b5dd8b0df0a4 100644 --- a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt @@ -20,7 +20,6 @@ import android.net.Uri import androidx.core.net.toUri import com.duckduckgo.app.browser.logindetection.WebNavigationEvent import com.duckduckgo.app.global.baseHost -import com.duckduckgo.app.global.events.db.UserEventEntity import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.events.db.UserEventsStore import kotlinx.coroutines.runBlocking @@ -34,11 +33,11 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore } private fun isUseOurAppUrl(uri: Uri): Boolean { - return domainMatchesUrl(uri, USE_OUR_APP_DOMAIN) || domainMatchesUrl(uri, USE_OUR_APP_MOBILE_DOMAIN) + return domainMatchesUrl(uri) } - private fun domainMatchesUrl(uri: Uri, matchingUrl: String): Boolean { - return uri.baseHost == matchingUrl.toUri().baseHost + private fun domainMatchesUrl(uri: Uri): Boolean { + return uri.baseHost?.contains(USE_OUR_APP_DOMAIN) ?: false } fun allowLoginDetection(event: WebNavigationEvent): Boolean { @@ -62,7 +61,7 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore suspend fun registerIfFireproofSeenForTheFirstTime(url: String) { if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) == null && isUseOurAppUrl(url)) { - userEventsStore.registerUserEvent(UserEventEntity(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN)) + userEventsStore.registerUserEvent(UserEventKey.USE_OUR_APP_FIREPROOF_DIALOG_SEEN) } } @@ -70,6 +69,5 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" const val USE_OUR_APP_DOMAIN = "facebook.com" - const val USE_OUR_APP_MOBILE_DOMAIN = "m.facebook.com" } } From 3328b32d288cbfd22fe213dd5e4427d57d90cec3 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 1 Jul 2020 13:10:30 +0100 Subject: [PATCH 31/38] Check if user is established before apply migration --- .../app/global/db/AppDatabaseTest.kt | 27 +++++++++++++------ .../onboarding/store/AppUserStageStoreTest.kt | 6 +---- .../systemsearch/SystemSearchViewModelTest.kt | 6 +---- .../duckduckgo/app/global/db/AppDatabase.kt | 16 ++++++++++- .../app/global/useourapp/UseOurAppDetector.kt | 18 ++++++------- .../app/onboarding/store/UserStageStore.kt | 6 +---- 6 files changed, 46 insertions(+), 33 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index e16f0f89e586..3173f8df0377 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -237,7 +237,7 @@ class AppDatabaseTest { fun whenMigratingFromVersion21To22IfUserIsEstablishedAndConditionsAreMetThenMigrateToNotification() { testHelper.createDatabase(TEST_DB_NAME, 21).use { givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = true) - givenUserIsEstablished(it) + givenUserStageIs(it, AppStage.ESTABLISHED) testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) @@ -250,7 +250,7 @@ class AppDatabaseTest { fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHideTipsIsTrueThenDoNotMigrateToNotification() { testHelper.createDatabase(TEST_DB_NAME, 21).use { givenUseOurAppStateIs(canMigrate = true, hideTips = true, canAddToHome = true) - givenUserIsEstablished(it) + givenUserStageIs(it, AppStage.ESTABLISHED) testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) @@ -263,7 +263,20 @@ class AppDatabaseTest { fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHomeShortcutNotSupportedThenDoNotMigrateToNotification() { testHelper.createDatabase(TEST_DB_NAME, 21).use { givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = false) - givenUserIsEstablished(it) + givenUserStageIs(it, AppStage.ESTABLISHED) + + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) + val stage = getUserStage(it) + + assertEquals(AppStage.ESTABLISHED.name, stage) + } + } + + @Test + fun whenMigratingFromVersion21To22IfUserIsNotEstablishedThenDoNotMigrateToNotification() { + testHelper.createDatabase(TEST_DB_NAME, 21).use { + givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = false) + givenUserStageIs(it, AppStage.ESTABLISHED) testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) @@ -276,7 +289,7 @@ class AppDatabaseTest { fun whenMigratingFromVersion21To22IfShouldNotRunMigrationThenDoNotMigrateToNotification2() { testHelper.createDatabase(TEST_DB_NAME, 21).use { givenUseOurAppStateIs(canMigrate = false) - givenUserIsEstablished(it) + givenUserStageIs(it, AppStage.ESTABLISHED) testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22) val stage = getUserStage(it) @@ -291,13 +304,12 @@ class AppDatabaseTest { whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(canAddToHome) } - private fun givenUserIsEstablished(database: SupportSQLiteDatabase) { + private fun givenUserStageIs(database: SupportSQLiteDatabase, appStage: AppStage) { val values: ContentValues = ContentValues().apply { put("key", 1) - put("appStage", AppStage.ESTABLISHED.name) + put("appStage", appStage.name) } - // Do not close the database database.apply { insert("userStage", SQLiteDatabase.CONFLICT_REPLACE, values) } @@ -306,7 +318,6 @@ class AppDatabaseTest { private fun getUserStage(database: SupportSQLiteDatabase): String { var stage = "" - // Do not close the database database.query("SELECT appStage from userStage limit 1").apply { moveToFirst() stage = getString(0) diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt index d9772d057555..554b8aa6e6d8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/store/AppUserStageStoreTest.kt @@ -17,9 +17,7 @@ package com.duckduckgo.app.onboarding.store import com.duckduckgo.app.CoroutineTestRule -import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.runBlocking -import com.duckduckgo.app.settings.db.SettingsDataStore import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever @@ -35,10 +33,8 @@ class AppUserStageStoreTest { var coroutineRule = CoroutineTestRule() private val userStageDao = mock() - private val mockAddToHomeCapabilityDetector = mock() - private val mockSettingsDataStore = mock() - private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider, mockAddToHomeCapabilityDetector, mockSettingsDataStore) + private val testee = AppUserStageStore(userStageDao, coroutineRule.testDispatcherProvider) @Test fun whenGetUserAppStageThenRetunCurrentStage() = coroutineRule.runBlocking { diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 822e58d21ecd..9cf35607a05b 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -24,10 +24,8 @@ import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion -import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.runBlocking -import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command @@ -59,8 +57,6 @@ class SystemSearchViewModelTest { private val mockDeviceAppLookup: DeviceAppLookup = mock() private val mockAutoComplete: AutoComplete = mock() private val mockPixel: Pixel = mock() - private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector = mock() - private val settingsDataStore: SettingsDataStore = mock() private val commandObserver: Observer = mock() private val commandCaptor = argumentCaptor() @@ -277,7 +273,7 @@ class SystemSearchViewModelTest { override suspend fun currentUserAppStage() = UserStage(appStage = AppStage.NEW) override fun insert(userStage: UserStage) {} } - return AppUserStageStore(emptyUserStageDao, coroutineRule.testDispatcherProvider, addToHomeCapabilityDetector, settingsDataStore) + return AppUserStageStore(emptyUserStageDao, coroutineRule.testDispatcherProvider) } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 24ca008d3b0a..84d130f3ec41 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -310,7 +310,7 @@ class MigrationsProvider( override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `user_events` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))") - if (!settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported()) { + if (canUserBeMigratedToUseOurAppFlow(database)) { if (useOurAppMigrationManager.shouldRunMigration()) { database.execSQL("UPDATE $USER_STAGE_TABLE_NAME SET appStage = \"${AppStage.USE_OUR_APP_NOTIFICATION}\" WHERE appStage = \"${AppStage.ESTABLISHED}\"") } @@ -318,6 +318,9 @@ class MigrationsProvider( } } + private fun canUserBeMigratedToUseOurAppFlow(database: SupportSQLiteDatabase): Boolean = + isUserEstablished(database) && !settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported() + val ALL_MIGRATIONS: List get() = listOf( MIGRATION_1_TO_2, @@ -343,6 +346,17 @@ class MigrationsProvider( MIGRATION_21_TO_22 ) + private fun isUserEstablished(database: SupportSQLiteDatabase): Boolean { + var stage: String + + database.query("SELECT appStage from userStage limit 1").apply { + moveToFirst() + if (count == 0) return false + stage = getString(0) + } + return (stage == AppStage.ESTABLISHED.name) + } + @Deprecated( message = "This class should be only used by database migrations.", replaceWith = ReplaceWith(expression = "UserStageStore", imports = ["com.duckduckgo.app.onboarding.store"]) diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt index b5dd8b0df0a4..a526bc9ed71e 100644 --- a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppDetector.kt @@ -32,14 +32,6 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore return isUseOurAppUrl(url.toUri()) } - private fun isUseOurAppUrl(uri: Uri): Boolean { - return domainMatchesUrl(uri) - } - - private fun domainMatchesUrl(uri: Uri): Boolean { - return uri.baseHost?.contains(USE_OUR_APP_DOMAIN) ?: false - } - fun allowLoginDetection(event: WebNavigationEvent): Boolean { val canShowFireproof = runBlocking { if (userEventsStore.getUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) == null) { @@ -65,8 +57,16 @@ class UseOurAppDetector @Inject constructor(val userEventsStore: UserEventsStore } } + private fun isUseOurAppUrl(uri: Uri): Boolean { + return domainMatchesUrl(uri) + } + + private fun domainMatchesUrl(uri: Uri): Boolean { + return uri.baseHost?.contains(USE_OUR_APP_DOMAIN) ?: false + } + companion object { - const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com" + const val USE_OUR_APP_SHORTCUT_URL: String = "https://m.facebook.com/" const val USE_OUR_APP_SHORTCUT_TITLE: String = "Facebook" const val USE_OUR_APP_DOMAIN = "facebook.com" } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt index 8cc9dd594bd7..6c517284bbf1 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/UserStageStore.kt @@ -16,9 +16,7 @@ package com.duckduckgo.app.onboarding.store -import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.global.DispatcherProvider -import com.duckduckgo.app.settings.db.SettingsDataStore import kotlinx.coroutines.withContext import javax.inject.Inject @@ -30,9 +28,7 @@ interface UserStageStore { class AppUserStageStore @Inject constructor( private val userStageDao: UserStageDao, - private val dispatcher: DispatcherProvider, - private val addToHomeCapabilityDetector: AddToHomeCapabilityDetector, - private val settingsDataStore: SettingsDataStore + private val dispatcher: DispatcherProvider ) : UserStageStore { override suspend fun getUserAppStage(): AppStage { return withContext(dispatcher.io()) { From 2f0dca342a026d9cea5b2265de9b8432fdd0f4d3 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 1 Jul 2020 16:53:14 +0100 Subject: [PATCH 32/38] Amend code as per PR --- .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index b820c74379d4..b77e991ee24c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -315,7 +315,7 @@ class BrowserTabViewModel( fun onViewReady() { url?.let { - sendPixelIfUserOurAppSiteVisitedFirstTime(it) + sendPixelIfUseOurAppSiteVisitedFirstTime(it) onUserSubmittedQuery(it) } } @@ -610,13 +610,13 @@ class BrowserTabViewModel( private fun pageChanged(url: String, title: String?) { Timber.v("Page changed: $url") - val oldUrl = site?.url + val previousUrl = site?.url buildSiteFactory(url, title) // Navigating from different website to use our app website - if (!useOurAppDetector.isUseOurAppUrl(oldUrl)) { - sendPixelIfUserOurAppSiteVisitedFirstTime(url) + if (!useOurAppDetector.isUseOurAppUrl(previousUrl)) { + sendPixelIfUseOurAppSiteVisitedFirstTime(url) } command.value = RefreshUserAgent(site?.uri?.host, currentBrowserViewState().isDesktopBrowsingMode) @@ -657,7 +657,7 @@ class BrowserTabViewModel( registerSiteVisit() } - private fun sendPixelIfUserOurAppSiteVisitedFirstTime(url: String) { + private fun sendPixelIfUseOurAppSiteVisitedFirstTime(url: String) { if (useOurAppDetector.isUseOurAppUrl(url)) { viewModelScope.launch { sendUseOurAppSiteVisitedPixel() } } From df28e5da040657f45086f4fcd1468d1586a2214a Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 1 Jul 2020 17:00:03 +0100 Subject: [PATCH 33/38] Amend test as per PR --- .../app/notification/AndroidNotificationSchedulerTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt index eba96fe906c3..2599c70d4a96 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt @@ -405,6 +405,7 @@ class AndroidNotificationSchedulerTest { @Test fun whenStageIsUseOurAppNotificationThenNotificationScheduled() = runBlocking { + givenNoInactiveUserNotifications() givenStageIsUseOurAppNotification() testee.scheduleNextNotification() @@ -414,6 +415,7 @@ class AndroidNotificationSchedulerTest { @Test fun whenStageIsUseOurAppNotificationAndNotificationScheduledThenStageCompleted() = runBlocking { + givenNoInactiveUserNotifications() givenStageIsUseOurAppNotification() testee.scheduleNextNotification() @@ -475,9 +477,12 @@ class AndroidNotificationSchedulerTest { ) } - private suspend fun givenStageIsUseOurAppNotification() { + private suspend fun givenNoInactiveUserNotifications() { whenever(privacyNotification.canShow()).thenReturn(false) whenever(clearNotification.canShow()).thenReturn(false) + } + + private suspend fun givenStageIsUseOurAppNotification() { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_NOTIFICATION) } From 34600df3975c3f74d6a3aa09b73f3dfeadc07d4c Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 2 Jul 2020 10:54:52 +0100 Subject: [PATCH 34/38] Filter by english locale and send pixel only if cta has been seen --- .../browser/shortcut/ShortcutReceiverTest.kt | 30 ++++++++++++++++--- .../app/browser/shortcut/ShortcutReceiver.kt | 13 ++++++-- .../useourapp/UseOurAppMigrationManager.kt | 8 ++++- .../duckduckgo/app/statistics/pixels/Pixel.kt | 1 + 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt index 0dbcc9e2b7be..c8c45b745555 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt @@ -18,6 +18,8 @@ package com.duckduckgo.app.browser.shortcut import android.content.Intent import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.cta.db.DismissedCtaDao +import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL @@ -26,7 +28,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyZeroInteractions +import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Rule @@ -40,15 +42,17 @@ class ShortcutReceiverTest { private val mockUserEventsStore: UserEventsStore = mock() private val mockPixel: Pixel = mock() + private val mockDismissedCtaDao: DismissedCtaDao = mock() private lateinit var testee: ShortcutReceiver @Before fun before() { - testee = ShortcutReceiver(mockUserEventsStore, coroutinesTestRule.testDispatcherProvider, mockPixel) + testee = ShortcutReceiver(mockUserEventsStore, mockDismissedCtaDao, coroutinesTestRule.testDispatcherProvider, mockPixel) } @Test fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenRegisterTimestamp() = coroutinesTestRule.runBlocking { + givenUseOurAppCtaHasBeenSeen() val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL) intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") @@ -59,6 +63,7 @@ class ShortcutReceiverTest { @Test fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenFirePixel() { + givenUseOurAppCtaHasBeenSeen() val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL) intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") @@ -69,6 +74,7 @@ class ShortcutReceiverTest { @Test fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotRegisterEvent() = coroutinesTestRule.runBlocking { + givenUseOurAppCtaHasBeenSeen() val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") @@ -78,12 +84,28 @@ class ShortcutReceiverTest { } @Test - fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotFirePixel() { + fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenFireShortcutAddedPixel() { val intent = Intent() intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com") intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") testee.onReceive(null, intent) - verifyZeroInteractions(mockPixel) + verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_ADDED) + } + + @Test + fun whenIntentReceivedAndCtaNotSeenIfUrlIsFromUseOurAppUrlThenDoNotFireUseOurAppPixelAndFireShortcutPixel() = coroutinesTestRule.runBlocking { + val intent = Intent() + intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL) + + intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title") + testee.onReceive(null, intent) + + verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) + verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_ADDED) + } + + private fun givenUseOurAppCtaHasBeenSeen() { + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP)).thenReturn(true) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt index 7401f57cd907..f7354be5fa05 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiver.kt @@ -24,6 +24,8 @@ import android.widget.Toast import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_TITLE_ARG import com.duckduckgo.app.browser.shortcut.ShortcutBuilder.Companion.SHORTCUT_URL_ARG +import com.duckduckgo.app.cta.db.DismissedCtaDao +import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.events.db.UserEventKey @@ -33,7 +35,12 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import javax.inject.Inject -class ShortcutReceiver @Inject constructor(private val userEventsStore: UserEventsStore, val dispatcher: DispatcherProvider, val pixel: Pixel) : +class ShortcutReceiver @Inject constructor( + private val userEventsStore: UserEventsStore, + private val ctaDao: DismissedCtaDao, + private val dispatcher: DispatcherProvider, + private val pixel: Pixel +) : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -47,9 +54,11 @@ class ShortcutReceiver @Inject constructor(private val userEventsStore: UserEven } GlobalScope.launch(dispatcher.io()) { - if (originUrl == USE_OUR_APP_SHORTCUT_URL) { + if (ctaDao.exists(CtaId.USE_OUR_APP) && originUrl == USE_OUR_APP_SHORTCUT_URL) { userEventsStore.registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED) pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED) + } else { + pixel.fire(Pixel.PixelName.SHORTCUT_ADDED) } } } diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt index 7a8628a199b9..f3b4f3a4bdf1 100644 --- a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.global.useourapp import com.duckduckgo.app.statistics.IndexRandomizer import com.duckduckgo.app.statistics.Probabilistic +import java.util.Locale interface MigrationManager { fun shouldRunMigration(): Boolean @@ -28,7 +29,12 @@ class UseOurAppMigrationManager constructor(private val indexRandomizer: IndexRa override fun shouldRunMigration(): Boolean { val randomizedIndex = indexRandomizer.random(MIGRATION_VARIANTS) val variant = MIGRATION_VARIANTS[randomizedIndex] - return (variant == useOurAppMigration) + return (isEnglishLocale() && variant == useOurAppMigration) + } + + private fun isEnglishLocale(): Boolean { + val locale = Locale.getDefault() + return locale != null && locale.language == "en" } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 482058e0f32f..461029cc22a0 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -205,6 +205,7 @@ interface Pixel { UOA_VISITED_AFTER_DELETE_CTA("m_uoa_vad"), USE_OUR_APP_SHORTCUT_OPENED("m_sho_uoa_o"), + SHORTCUT_ADDED("m_sho_a"), SHORTCUT_OPENED("m_sho_o"), } From d455fbb1f6d97f4db0c376266fa3342d00061e91 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 2 Jul 2020 14:36:39 +0100 Subject: [PATCH 35/38] Move language check to database migration --- .../main/java/com/duckduckgo/app/global/db/AppDatabase.kt | 8 +++++++- .../app/global/useourapp/UseOurAppMigrationManager.kt | 8 +------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 84d130f3ec41..936ee90715f8 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -62,6 +62,7 @@ import com.duckduckgo.app.usage.app.AppDaysUsedDao import com.duckduckgo.app.usage.app.AppDaysUsedEntity import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity +import java.util.Locale @Database( exportSchema = true, version = 22, entities = [ @@ -319,7 +320,12 @@ class MigrationsProvider( } private fun canUserBeMigratedToUseOurAppFlow(database: SupportSQLiteDatabase): Boolean = - isUserEstablished(database) && !settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported() + isEnglishLocale() && isUserEstablished(database) && !settingsDataStore.hideTips && addToHomeCapabilityDetector.isAddToHomeSupported() + + private fun isEnglishLocale(): Boolean { + val locale = Locale.getDefault() + return locale != null && locale.language == "en" + } val ALL_MIGRATIONS: List get() = listOf( diff --git a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt index f3b4f3a4bdf1..7a8628a199b9 100644 --- a/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt +++ b/app/src/main/java/com/duckduckgo/app/global/useourapp/UseOurAppMigrationManager.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.global.useourapp import com.duckduckgo.app.statistics.IndexRandomizer import com.duckduckgo.app.statistics.Probabilistic -import java.util.Locale interface MigrationManager { fun shouldRunMigration(): Boolean @@ -29,12 +28,7 @@ class UseOurAppMigrationManager constructor(private val indexRandomizer: IndexRa override fun shouldRunMigration(): Boolean { val randomizedIndex = indexRandomizer.random(MIGRATION_VARIANTS) val variant = MIGRATION_VARIANTS[randomizedIndex] - return (isEnglishLocale() && variant == useOurAppMigration) - } - - private fun isEnglishLocale(): Boolean { - val locale = Locale.getDefault() - return locale != null && locale.language == "en" + return (variant == useOurAppMigration) } companion object { From ffe4ba64f18dd47d9ccd3bddd69982c57946e4a4 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Thu, 2 Jul 2020 16:25:38 +0100 Subject: [PATCH 36/38] Send uoa pixel only if cta was seen --- .../app/browser/BrowserViewModelTest.kt | 22 ++++++++++++++++++- .../app/browser/BrowserViewModel.kt | 5 ++++- .../duckduckgo/app/global/ViewModelFactory.kt | 3 +++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 6dd4b1636438..12043ab09583 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,6 +23,8 @@ import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.BrowserViewModel.Command.DisplayMessage import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter +import com.duckduckgo.app.cta.db.DismissedCtaDao +import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions @@ -82,6 +84,9 @@ class BrowserViewModelTest { @Mock private lateinit var mockPixel: Pixel + @Mock + private lateinit var mockDismissedCtaDao: DismissedCtaDao + private lateinit var testee: BrowserViewModel @Before @@ -96,6 +101,7 @@ class BrowserViewModelTest { dataClearer = mockAutomaticDataClearer, appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter, appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder, + ctaDao = mockDismissedCtaDao, pixel = mockPixel ) @@ -186,13 +192,23 @@ class BrowserViewModelTest { } @Test - fun whenOpenShortcutIfUrlIsUseOurAppUrlThenFirePixel() { + fun whenOpenShortcutIfUrlIsUseOurAppUrlAndCtaHasBeenSeenThenFirePixel() { + givenUseOurAppCtaHasBeenSeen() val url = USE_OUR_APP_SHORTCUT_URL whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) testee.onOpenShortcut(url) verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) } + @Test + fun whenOpenShortcutIfUrlIsUseOurAppUrlAndCtaHasNotBeenSeenThenDoNotFireUseOurAppPixel() { + val url = USE_OUR_APP_SHORTCUT_URL + whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) + testee.onOpenShortcut(url) + verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) + verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_OPENED) + } + @Test fun whenOpenShortcutIfUrlIsNotUSeOurAppUrlThenFirePixel() { val url = "example.com" @@ -201,6 +217,10 @@ class BrowserViewModelTest { verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_OPENED) } + private fun givenUseOurAppCtaHasBeenSeen() { + whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP)).thenReturn(true) + } + companion object { const val TAB_ID = "TAB_ID" } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index d81e7ce6a5b5..4c196cbf68d4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -27,6 +27,8 @@ import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.rating.ui.AppEnjoymentDialogFragment import com.duckduckgo.app.browser.rating.ui.GiveFeedbackDialogFragment import com.duckduckgo.app.browser.rating.ui.RateAppDialogFragment +import com.duckduckgo.app.cta.db.DismissedCtaDao +import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.ApplicationClearDataState import com.duckduckgo.app.global.SingleLiveEvent @@ -51,6 +53,7 @@ class BrowserViewModel( private val dataClearer: DataClearer, private val appEnjoymentPromptEmitter: AppEnjoymentPromptEmitter, private val appEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder, + private val ctaDao: DismissedCtaDao, private val pixel: Pixel ) : AppEnjoymentDialogFragment.Listener, RateAppDialogFragment.Listener, @@ -203,7 +206,7 @@ class BrowserViewModel( fun onOpenShortcut(url: String) { launch { tabRepository.selectByUrlOrNewTab(queryUrlConverter.convertQueryToUrl(url)) } - if (url == USE_OUR_APP_SHORTCUT_URL) { + if (ctaDao.exists(CtaId.USE_OUR_APP) && url == USE_OUR_APP_SHORTCUT_URL) { pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) } else { pixel.fire(Pixel.PixelName.SHORTCUT_OPENED) diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index b90d9b78e9e4..ffa474091c52 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.favicon.FaviconDownloader import com.duckduckgo.app.browser.logindetection.NavigationAwareLoginDetector import com.duckduckgo.app.browser.omnibar.QueryUrlConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage +import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.feedback.api.FeedbackSubmitter import com.duckduckgo.app.feedback.ui.common.FeedbackViewModel @@ -116,6 +117,7 @@ class ViewModelFactory @Inject constructor( private val userEventsStore: UserEventsStore, private val notificationDao: NotificationDao, private val userOurAppDetector: UseOurAppDetector, + private val dismissedCtaDao: DismissedCtaDao, private val dispatcherProvider: DispatcherProvider ) : ViewModelProvider.NewInstanceFactory() { @@ -179,6 +181,7 @@ class ViewModelFactory @Inject constructor( dataClearer, appEnjoymentPromptEmitter, appEnjoymentUserEventRecorder, + dismissedCtaDao, pixel ) } From 365efb6bc1cad7a98dd1d99c889f6b8e0ac79c0b Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 3 Jul 2020 07:19:17 +0100 Subject: [PATCH 37/38] Wrap call in launch using IO --- .../app/browser/BrowserViewModelTest.kt | 1 + .../duckduckgo/app/browser/BrowserViewModel.kt | 15 ++++++++++----- .../com/duckduckgo/app/global/ViewModelFactory.kt | 14 +++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 12043ab09583..ced386fb72e5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -102,6 +102,7 @@ class BrowserViewModelTest { appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter, appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder, ctaDao = mockDismissedCtaDao, + dispatchers = coroutinesTestRule.testDispatcherProvider, pixel = mockPixel ) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 4c196cbf68d4..36f50fdb69f3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -31,6 +31,8 @@ import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.global.ApplicationClearDataState +import com.duckduckgo.app.global.DefaultDispatcherProvider +import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.SingleLiveEvent import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions @@ -54,6 +56,7 @@ class BrowserViewModel( private val appEnjoymentPromptEmitter: AppEnjoymentPromptEmitter, private val appEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder, private val ctaDao: DismissedCtaDao, + private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), private val pixel: Pixel ) : AppEnjoymentDialogFragment.Listener, RateAppDialogFragment.Listener, @@ -205,11 +208,13 @@ class BrowserViewModel( } fun onOpenShortcut(url: String) { - launch { tabRepository.selectByUrlOrNewTab(queryUrlConverter.convertQueryToUrl(url)) } - if (ctaDao.exists(CtaId.USE_OUR_APP) && url == USE_OUR_APP_SHORTCUT_URL) { - pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) - } else { - pixel.fire(Pixel.PixelName.SHORTCUT_OPENED) + launch(dispatchers.io()) { + tabRepository.selectByUrlOrNewTab(queryUrlConverter.convertQueryToUrl(url)) + if (ctaDao.exists(CtaId.USE_OUR_APP) && url == USE_OUR_APP_SHORTCUT_URL) { + pixel.fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED) + } else { + pixel.fire(Pixel.PixelName.SHORTCUT_OPENED) + } } } } diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index ffa474091c52..eec4fb406830 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -176,13 +176,13 @@ class ViewModelFactory @Inject constructor( private fun browserViewModel(): BrowserViewModel { return BrowserViewModel( - tabRepository, - queryUrlConverter, - dataClearer, - appEnjoymentPromptEmitter, - appEnjoymentUserEventRecorder, - dismissedCtaDao, - pixel + tabRepository = tabRepository, + queryUrlConverter = queryUrlConverter, + dataClearer = dataClearer, + appEnjoymentPromptEmitter = appEnjoymentPromptEmitter, + appEnjoymentUserEventRecorder = appEnjoymentUserEventRecorder, + ctaDao = dismissedCtaDao, + pixel = pixel ) } From a80bda1ab0817a2717f1786e0b54fa84559f8ac2 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 3 Jul 2020 07:43:36 +0100 Subject: [PATCH 38/38] Change order in when statement so we always get a high-res icon for use our app site --- .../java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt index b65aee79a86c..5fefe3ac6ce2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/shortcut/ShortcutBuilder.kt @@ -39,8 +39,8 @@ class ShortcutBuilder @Inject constructor() { intent.putExtra(SHORTCUT_EXTRA_ARG, true) val icon = when { - homeShortcut.icon != null -> IconCompat.createWithBitmap(homeShortcut.icon) homeShortcut.url == USE_OUR_APP_SHORTCUT_URL -> IconCompat.createWithResource(context, R.drawable.ic_fb_favicon) + homeShortcut.icon != null -> IconCompat.createWithBitmap(homeShortcut.icon) else -> IconCompat.createWithResource(context, R.drawable.logo_mini) }