Skip to content

Commit 2316b5c

Browse files
authored
Feature/home screen call to action (#304)
* Add permission required to create home screen shortcuts * Add class to allow downloading favicons * Add "add to home" functionality as a first pass. * Choose better shortcut title; appropriately enable/disable menu item * Add support for two distinct layout states; browser and blank tab These have slightly different requirements in terms of their scroll-observing behaviours. We do want the browser layout mode to respect the toolbar scrolling for instance, but we don't want the same behaviour for the new tab view. Trying to use the same `app:layout_behavior="@string/appbar_scrolling_view_behavior"` behaviour in both modes means that the new tab layout is essentially forced off screen vertically and it makes it difficult to add a button that anchors to the bottom. Splitting these into two distinct modes lets us better control both modes without trying to find one set of views and flags which works well enough for both. * Further work on UX of call to action; wired to show screen when clicked * Rename "time based" feature analyzer class to something more generic * Add in feature for home screen call to action * Only show call to action at appropriate time for active variants * Remove "add to home" commits * Improve testing to ensure user dismissing c2a doesn't impact banner * Add static import for better readability * Undoing rename of dagger parameter * Remove unused function * Add light ripple effect to call to action * Code formatting * Adding activity transition for call to action * Upgrading tooling * Add fix to ensure only Nougat or newer devices get experiment * Fix centering of call to action button * Add correct menu reference for tools:menu link * Move new tab layout into its own layout file * Programmatically hide DDG logo when there's not enough space for it * Layout tweaks around call to action button * Add @synchronized to getVariant to prevent concurrent allocations * Tweaking animations and minor UI changes to call to action * Formatting * Fix ddgLogo visibility problem after setting DDG as default browser * Add comment to an otherwise unclear piece of code * Add new default browser illustrations * Configure variants
1 parent 7e8e799 commit 2316b5c

32 files changed

+607
-131
lines changed
Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package com.duckduckgo.app.browser.defaultBrowsing
1919
import com.duckduckgo.app.global.install.AppInstallStore
2020
import com.duckduckgo.app.statistics.Variant
2121
import com.duckduckgo.app.statistics.VariantManager
22-
import com.duckduckgo.app.statistics.VariantManager.VariantFeature.DefaultBrowserFeature.ShowTimedReminder
22+
import com.duckduckgo.app.statistics.VariantManager.VariantFeature.DefaultBrowserFeature.ShowBanner
2323
import com.nhaarman.mockito_kotlin.mock
2424
import com.nhaarman.mockito_kotlin.whenever
2525
import org.junit.Assert.assertFalse
@@ -28,7 +28,7 @@ import org.junit.Before
2828
import org.junit.Test
2929
import java.util.concurrent.TimeUnit
3030

31-
class DefaultBrowserTimeBasedNotificationTest {
31+
class DefaultBrowserBannerNotificationTest {
3232

3333
private lateinit var testee: DefaultBrowserTimeBasedNotification
3434

@@ -44,48 +44,55 @@ class DefaultBrowserTimeBasedNotificationTest {
4444
@Test
4545
fun whenDefaultBrowserNotSupportedByDeviceThenNotificationNotShown() {
4646
configureEnvironment(false, true, true, false)
47-
assertFalse(testee.shouldShowNotification(browserShowing = true))
47+
assertFalse(testee.shouldShowBannerNotification(browserShowing = true))
4848
}
4949

5050
@Test
5151
fun whenDefaultBrowserFeatureNotSupportedThenNotificationNotShown() {
5252
configureEnvironment(true, false, true, false)
53-
assertFalse(testee.shouldShowNotification(browserShowing = true))
53+
assertFalse(testee.shouldShowBannerNotification(browserShowing = true))
5454
}
5555

5656
@Test
5757
fun whenNoAppInstallTimeRecordedThenNotificationNotShown() {
5858
configureEnvironment(true, true, false, false)
59-
assertFalse(testee.shouldShowNotification(browserShowing = true))
59+
assertFalse(testee.shouldShowBannerNotification(browserShowing = true))
6060
}
6161

6262
@Test
6363
fun whenUserDeclinedPreviouslyThenNotificationNotShown() {
6464
configureEnvironment(true, true, true, true)
65-
assertFalse(testee.shouldShowNotification(browserShowing = true))
65+
assertFalse(testee.shouldShowBannerNotification(browserShowing = true))
6666
}
6767

6868
@Test
6969
fun whenNotEnoughTimeHasPassedSinceInstallThenNotificationNotShown() {
7070
configureEnvironment(true, true, true, false)
7171
whenever(appInstallStore.installTimestamp).thenReturn(0)
72-
assertFalse(testee.shouldShowNotification(browserShowing = true, timeNow = TimeUnit.SECONDS.toMillis(10)))
72+
assertFalse(testee.shouldShowBannerNotification(browserShowing = true, timeNow = TimeUnit.SECONDS.toMillis(10)))
7373
}
7474

7575
@Test
7676
fun whenEnoughTimeHasPassedSinceInstallThenNotificationShown() {
7777
configureEnvironment(true, true, true, false)
7878
whenever(appInstallStore.installTimestamp).thenReturn(0)
79-
assertTrue(testee.shouldShowNotification(browserShowing = true, timeNow = TimeUnit.DAYS.toMillis(100)))
79+
assertTrue(testee.shouldShowBannerNotification(browserShowing = true, timeNow = TimeUnit.DAYS.toMillis(100)))
80+
}
81+
82+
@Test
83+
fun whenUserDeclinedHomeScreenCallToActionPreviouslyThenNotificationStillShown() {
84+
configureEnvironment(true, true, true, false)
85+
whenever(appInstallStore.hasUserDeclinedDefaultBrowserHomeScreenCallToActionPreviously()).thenReturn(true)
86+
assertTrue(testee.shouldShowBannerNotification(browserShowing = true))
8087
}
8188

8289
private fun configureEnvironment(deviceSupported: Boolean, featureEnabled: Boolean, timestampRecorded: Boolean, previousDecline: Boolean) {
8390
whenever(mockDetector.deviceSupportsDefaultBrowserConfiguration()).thenReturn(deviceSupported)
8491
whenever(variantManager.getVariant()).thenReturn(if (featureEnabled) variantWithFeatureEnabled() else variantWithFeatureDisabled())
8592
whenever(appInstallStore.hasInstallTimestampRecorded()).thenReturn(timestampRecorded)
86-
whenever(appInstallStore.hasUserDeclinedDefaultBrowserPreviously()).thenReturn(previousDecline)
93+
whenever(appInstallStore.hasUserDeclinedDefaultBrowserBannerPreviously()).thenReturn(previousDecline)
8794
}
8895

89-
private fun variantWithFeatureEnabled() = Variant("", 0.0, listOf(ShowTimedReminder))
96+
private fun variantWithFeatureEnabled() = Variant("", 0.0, listOf(ShowBanner))
9097
private fun variantWithFeatureDisabled() = Variant("", 0.0, listOf())
9198
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) 2018 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.defaultBrowsing
18+
19+
import com.duckduckgo.app.global.install.AppInstallStore
20+
import com.duckduckgo.app.statistics.Variant
21+
import com.duckduckgo.app.statistics.VariantManager
22+
import com.duckduckgo.app.statistics.VariantManager.VariantFeature.DefaultBrowserFeature.ShowHomeScreenCallToAction
23+
import com.nhaarman.mockito_kotlin.mock
24+
import com.nhaarman.mockito_kotlin.whenever
25+
import org.junit.Assert.assertFalse
26+
import org.junit.Assert.assertTrue
27+
import org.junit.Before
28+
import org.junit.Test
29+
30+
class DefaultBrowserHomeScreenCallToActionTest {
31+
32+
private lateinit var testee: DefaultBrowserTimeBasedNotification
33+
34+
private val mockDetector: DefaultBrowserDetector = mock()
35+
private val appInstallStore: AppInstallStore = mock()
36+
private val variantManager: VariantManager = mock()
37+
38+
@Before
39+
fun setup() {
40+
testee = DefaultBrowserTimeBasedNotification(mockDetector, appInstallStore, variantManager)
41+
}
42+
43+
@Test
44+
fun whenDefaultBrowserNotSupportedByDeviceThenCallToActionNotShown() {
45+
configureEnvironment(false, true, true, false)
46+
assertFalse(testee.shouldShowHomeScreenCallToActionNotification())
47+
}
48+
49+
@Test
50+
fun whenDefaultBrowserFeatureNotSupportedThenCallToActionNotShown() {
51+
configureEnvironment(true, false, true, false)
52+
assertFalse(testee.shouldShowHomeScreenCallToActionNotification( ))
53+
}
54+
55+
@Test
56+
fun whenNoAppInstallTimeRecordedThenCallToActionNotShown() {
57+
configureEnvironment(true, true, false, false)
58+
assertFalse(testee.shouldShowHomeScreenCallToActionNotification( ))
59+
}
60+
61+
@Test
62+
fun whenUserDeclinedPreviouslyThenCallToActionNotShown() {
63+
configureEnvironment(true, true, true, true)
64+
assertFalse(testee.shouldShowHomeScreenCallToActionNotification( ))
65+
}
66+
67+
@Test
68+
fun whenAllOtherConditionsPassThenCallToActionShown() {
69+
configureEnvironment(true, true, true, false)
70+
whenever(appInstallStore.installTimestamp).thenReturn(0)
71+
assertTrue(testee.shouldShowHomeScreenCallToActionNotification())
72+
}
73+
74+
private fun configureEnvironment(deviceSupported: Boolean, featureEnabled: Boolean, timestampRecorded: Boolean, previousDecline: Boolean) {
75+
whenever(mockDetector.deviceSupportsDefaultBrowserConfiguration()).thenReturn(deviceSupported)
76+
whenever(variantManager.getVariant()).thenReturn(if (featureEnabled) variantWithFeatureEnabled() else variantWithFeatureDisabled())
77+
whenever(appInstallStore.hasInstallTimestampRecorded()).thenReturn(timestampRecorded)
78+
whenever(appInstallStore.hasUserDeclinedDefaultBrowserHomeScreenCallToActionPreviously()).thenReturn(previousDecline)
79+
}
80+
81+
private fun variantWithFeatureEnabled() = Variant("", 0.0, listOf(ShowHomeScreenCallToAction))
82+
private fun variantWithFeatureDisabled() = Variant("", 0.0, listOf())
83+
}

app/src/androidTest/java/com/duckduckgo/app/global/install/AppInstallSharedPreferencesTest.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ class AppInstallSharedPreferencesTest {
3636
}
3737

3838
@Test
39-
fun whenInitializedThenUserHasNotBeenMarkedAsHavingPreviouslyDeclined() {
40-
assertFalse(testee.hasUserDeclinedDefaultBrowserPreviously())
39+
fun whenInitializedThenUserHasNotBeenMarkedAsHavingPreviouslyDeclinedBanner() {
40+
assertFalse(testee.hasUserDeclinedDefaultBrowserBannerPreviously())
41+
}
42+
43+
@Test
44+
fun whenInitializedThenUserHasNotBeenMarkedAsHavingPreviouslyDeclinedHomeScreenCallToAction() {
45+
assertFalse(testee.hasUserDeclinedDefaultBrowserHomeScreenCallToActionPreviously())
4146
}
4247

4348
@Test
@@ -60,15 +65,17 @@ class AppInstallSharedPreferencesTest {
6065
}
6166

6267
@Test
63-
fun whenUserPreviouslyDeclinedThenThatIsReturnedWhenQueried() {
68+
fun whenUserPreviouslyDeclinedBannerThenThatIsReturnedWhenQueried() {
6469
val timestamp = 1L
65-
testee.recordUserDeclinedToSetDefaultBrowser(timestamp)
66-
assertTrue(testee.hasUserDeclinedDefaultBrowserPreviously())
70+
testee.recordUserDeclinedBannerToSetDefaultBrowser(timestamp)
71+
assertTrue(testee.hasUserDeclinedDefaultBrowserBannerPreviously())
6772
}
6873

6974
@Test
70-
fun whenUserNotPreviouslyDeclinedThenThatIsReturnedWhenQueried() {
71-
assertFalse(testee.hasUserDeclinedDefaultBrowserPreviously())
75+
fun whenUserPreviouslyDeclinedHomeScreenCallToActionThenThatIsReturnedWhenQueried() {
76+
val timestamp = 1L
77+
testee.recordUserDeclinedHomeScreenCallToActionToSetDefaultBrowser(timestamp)
78+
assertTrue(testee.hasUserDeclinedDefaultBrowserHomeScreenCallToActionPreviously())
7279
}
7380

7481
}

app/src/androidTest/java/com/duckduckgo/app/statistics/ExperimentationVariantManagerTest.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.duckduckgo.app.statistics
1818

19+
import android.os.Build
20+
import android.support.test.filters.SdkSuppress
1921
import com.duckduckgo.app.statistics.store.StatisticsDataStore
2022
import com.nhaarman.mockito_kotlin.*
2123
import org.junit.Assert.assertEquals
@@ -93,7 +95,8 @@ class ExperimentationVariantManagerTest {
9395

9496

9597
@Test
96-
fun whenNoVariantPersistedThenNewVariantAllocated() {
98+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
99+
fun whenNougatOrLaterAndNoVariantPersistedThenNewVariantAllocated() {
97100
activeVariants.add(Variant("foo", 100.0))
98101

99102
testee.getVariant(activeVariants)
@@ -102,11 +105,30 @@ class ExperimentationVariantManagerTest {
102105
}
103106

104107
@Test
105-
fun whenNoVariantPersistedThenNewVariantKeyIsAllocatedAndPersisted() {
108+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
109+
fun whenNougatOrLaterAndNoVariantPersistedThenNewVariantKeyIsAllocatedAndPersisted() {
106110
activeVariants.add(Variant("foo", 100.0))
107111

108112
testee.getVariant(activeVariants)
109113

110114
verify(mockStore).variant = "foo"
111115
}
116+
117+
@Test
118+
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
119+
fun whenMarshmallowOrEarlierAndNoVariantPersistedThenDefaultVariantAllocated() {
120+
activeVariants.add(Variant("foo", 100.0))
121+
122+
assertEquals(VariantManager.DEFAULT_VARIANT, testee.getVariant(activeVariants))
123+
}
124+
125+
@Test
126+
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
127+
fun whenMarshmallowOrEarlierAndNoVariantPersistedThenDefaultVariantKeyIsAllocatedAndPersisted() {
128+
activeVariants.add(Variant("foo", 100.0))
129+
130+
testee.getVariant(activeVariants)
131+
132+
verify(mockStore).variant = VariantManager.DEFAULT_VARIANT.key
133+
}
112134
}

app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package com.duckduckgo.app.statistics
1818

19-
import org.junit.Assert.assertNotNull
20-
import org.junit.Assert.fail
19+
import com.duckduckgo.app.statistics.VariantManager.VariantFeature.DefaultBrowserFeature.*
20+
import org.junit.Assert.*
2121
import org.junit.Test
2222

2323
class VariantManagerTest {
@@ -26,24 +26,43 @@ class VariantManagerTest {
2626
private val totalWeight = variants.sumByDouble { it.weight }
2727

2828
@Test
29-
fun whenChanceOfControlVariantCalculatedThenOddsAreOneInTwo() {
30-
val variant = variants.firstOrNull { it.key == "my" }
31-
assertNotNull(variant)
32-
assertEqualsDouble( 0.5, variant!!.weight / totalWeight)
29+
fun onboardingOnlyVariantConfiguredCorrectly() {
30+
val variant = variants.firstOrNull { it.key == "ms" }
31+
assertEqualsDouble( 0.20, variant!!.weight / totalWeight)
32+
assertTrue(variant.hasFeature(ShowInOnboarding))
33+
assertEquals(1, variant.features.size)
34+
}
35+
36+
@Test
37+
fun homeScreenCallToActionVariantConfiguredCorrectly() {
38+
val variant = variants.firstOrNull { it.key == "mt" }
39+
assertEqualsDouble( 0.20, variant!!.weight / totalWeight)
40+
assertTrue(variant.hasFeature(ShowHomeScreenCallToAction))
41+
assertEquals(1, variant.features.size)
3342
}
3443

3544
@Test
36-
fun whenChanceOfOnboardingOnlyVariantCalculatedThenOddsAreOneInFour() {
37-
val variant = variants.firstOrNull { it.key == "mw" }
38-
assertNotNull(variant)
39-
assertEqualsDouble( 0.25, variant!!.weight / totalWeight)
45+
fun showBannerVariantConfiguredCorrectly() {
46+
val variant = variants.firstOrNull { it.key == "mu" }
47+
assertEqualsDouble( 0.20, variant!!.weight / totalWeight)
48+
assertTrue(variant.hasFeature(ShowBanner))
49+
assertEquals(1, variant.features.size)
4050
}
4151

4252
@Test
43-
fun whenChanceOfOnboardingAndReminderVariantCalculatedThenOddsAreOneInFour() {
44-
val variant = variants.firstOrNull { it.key == "mx" }
45-
assertNotNull(variant)
46-
assertEqualsDouble( 0.25, variant!!.weight / totalWeight)
53+
fun showBannerAndShowHomeScreenCallToActionVariantConfiguredCorrectly() {
54+
val variant = variants.firstOrNull { it.key == "mv" }
55+
assertEqualsDouble( 0.20, variant!!.weight / totalWeight)
56+
assertTrue(variant.hasFeature(ShowBanner))
57+
assertTrue(variant.hasFeature(ShowHomeScreenCallToAction))
58+
assertEquals(2, variant.features.size)
59+
}
60+
61+
@Test
62+
fun controlVariantConfiguredCorrectly() {
63+
val variant = variants.firstOrNull { it.key == "my" }
64+
assertEqualsDouble( 0.2, variant!!.weight / totalWeight)
65+
assertEquals(0, variant.features.size)
4766
}
4867

4968
private fun assertEqualsDouble(expected: Double, actual: Double) {

0 commit comments

Comments
 (0)