Skip to content

Commit 4831e05

Browse files
authored
Add feedback and broken site forms (#294)
• Replace website with modal feedback form in settings • Add broken site form to browser • Switch to cornflower blue globally for modal card layout backgrounds
1 parent 5f1c100 commit 4831e05

29 files changed

+854
-48
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.duckduckgo.app.feedback.ui
2+
3+
import android.arch.core.executor.testing.InstantTaskExecutorRule
4+
import android.arch.lifecycle.Observer
5+
import com.duckduckgo.app.InstantSchedulersRule
6+
import com.duckduckgo.app.feedback.api.FeedbackSender
7+
import com.duckduckgo.app.feedback.ui.FeedbackViewModel.*
8+
import com.nhaarman.mockito_kotlin.*
9+
import org.junit.Assert.*
10+
import org.junit.Before
11+
import org.junit.Rule
12+
import org.junit.Test
13+
import org.mockito.Mock
14+
import org.mockito.MockitoAnnotations
15+
16+
class FeedbackViewModelTest {
17+
18+
@get:Rule
19+
@Suppress("unused")
20+
var instantTaskExecutorRule = InstantTaskExecutorRule()
21+
22+
@get:Rule
23+
@Suppress("unused")
24+
val schedulers = InstantSchedulersRule()
25+
26+
@Mock
27+
private lateinit var mockFeedbackSender: FeedbackSender
28+
29+
@Mock
30+
private lateinit var mockCommandObserver: Observer<FeedbackViewModel.Command>
31+
32+
private lateinit var testee: FeedbackViewModel
33+
34+
private val viewState: FeedbackViewModel.ViewState
35+
get() = testee.viewState.value!!
36+
37+
@Before
38+
fun before() {
39+
MockitoAnnotations.initMocks(this)
40+
testee = FeedbackViewModel(mockFeedbackSender)
41+
testee.command.observeForever(mockCommandObserver)
42+
}
43+
44+
@Test
45+
fun whenInitializedThenCannotSubmit() {
46+
assertFalse(viewState.submitAllowed)
47+
}
48+
49+
@Test
50+
fun whenBrokenUrlSwitchedOnWithNoUrlThenUrlFocused() {
51+
testee.onBrokenSiteUrlChanged(null)
52+
testee.onBrokenSiteChanged(true)
53+
verify(mockCommandObserver).onChanged(Command.FocusUrl)
54+
}
55+
56+
@Test
57+
fun whenBrokenUrlSwitchedOnWithUrlThenMessageFocused() {
58+
testee.onBrokenSiteUrlChanged("http://example.com")
59+
testee.onBrokenSiteChanged(true)
60+
verify(mockCommandObserver).onChanged(Command.FocusMessage)
61+
}
62+
63+
@Test
64+
fun whenBrokenUrlOnWithUrlThenCanSubmit() {
65+
testee.onBrokenSiteChanged(true)
66+
testee.onBrokenSiteUrlChanged("http://example.com")
67+
assertTrue(viewState.submitAllowed)
68+
}
69+
70+
@Test
71+
fun whenBrokenUrlOnWithNullUrlThenCannotSubmit() {
72+
testee.onBrokenSiteChanged(true)
73+
testee.onBrokenSiteUrlChanged(null)
74+
assertFalse(viewState.submitAllowed)
75+
}
76+
77+
@Test
78+
fun whenBrokenUrlOnWithBlankUrlThenCannotSubmit() {
79+
testee.onBrokenSiteChanged(true)
80+
testee.onBrokenSiteUrlChanged(" ")
81+
assertFalse(viewState.submitAllowed)
82+
}
83+
84+
@Test
85+
fun whenBrokenUrlOffWithMessageThenCanSubmit() {
86+
testee.onBrokenSiteChanged(false)
87+
testee.onFeedbackMessageChanged("Feedback message")
88+
assertTrue(viewState.submitAllowed)
89+
}
90+
91+
@Test
92+
fun whenBrokenUrlOffWithNullMessageThenCannotSubmit() {
93+
testee.onBrokenSiteChanged(false)
94+
testee.onFeedbackMessageChanged(null)
95+
assertFalse(viewState.submitAllowed)
96+
}
97+
98+
@Test
99+
fun whenBrokenUrlOffWithBlankMessageThenCannotSubmit() {
100+
testee.onBrokenSiteChanged(false)
101+
testee.onFeedbackMessageChanged(" ")
102+
assertFalse(viewState.submitAllowed)
103+
}
104+
105+
@Test
106+
fun whenCanSubmitBrokenUrlAndSubmitPressedThenFeedbackSubmitted() {
107+
val url = "http://example.com"
108+
testee.onBrokenSiteChanged(true)
109+
testee.onBrokenSiteUrlChanged(url)
110+
testee.onSubmitPressed()
111+
112+
verify(mockFeedbackSender).submitBrokenSiteFeedback(null, url)
113+
verify(mockCommandObserver).onChanged(Command.ConfirmAndFinish)
114+
}
115+
116+
@Test
117+
fun whenCannotSubmitBrokenUrlAndSubmitPressedThenFeedbackNotSubmitted() {
118+
testee.onBrokenSiteChanged(true)
119+
testee.onSubmitPressed()
120+
verify(mockFeedbackSender, never()).submitBrokenSiteFeedback(any(), any())
121+
verify(mockCommandObserver, never()).onChanged(Command.ConfirmAndFinish)
122+
}
123+
124+
@Test
125+
fun whenCanSubmitMessageAndSubmitPressedThenFeedbackSubmitted() {
126+
val message = "Message"
127+
testee.onBrokenSiteChanged(false)
128+
testee.onFeedbackMessageChanged(message)
129+
testee.onSubmitPressed()
130+
verify(mockFeedbackSender).submitGeneralFeedback(message)
131+
verify(mockCommandObserver).onChanged(Command.ConfirmAndFinish)
132+
}
133+
134+
@Test
135+
fun whenCannotSubmitMessageAndSubmitPressedThenFeedbackNotSubmitted() {
136+
testee.onBrokenSiteChanged(false)
137+
testee.onSubmitPressed()
138+
verify(mockFeedbackSender, never()).submitGeneralFeedback(any())
139+
verify(mockCommandObserver, never()).onChanged(Command.ConfirmAndFinish)
140+
}
141+
142+
}

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@
9898
<activity
9999
android:name="com.duckduckgo.app.settings.SettingsActivity"
100100
android:label="@string/settingsActivityTitle" />
101+
<activity
102+
android:name="com.duckduckgo.app.feedback.ui.FeedbackActivity"
103+
android:label="@string/feedbackActivityTitle"
104+
android:theme="@style/ModalCardTheme" />
101105
<activity
102106
android:name="com.duckduckgo.app.about.AboutDuckDuckGoActivity"
103107
android:label="@string/aboutActivityTitle"
@@ -123,7 +127,7 @@
123127

124128
<activity
125129
android:name=".defaultBrowsing.DefaultBrowserInfoActivity"
126-
android:theme="@style/Theme.AppCompat.Translucent" />
130+
android:theme="@style/ModalCardTheme" />
127131
</application>
128132

129133
</manifest>

app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksActivity
2626
import com.duckduckgo.app.browser.BrowserViewModel.Command
2727
import com.duckduckgo.app.browser.BrowserViewModel.Command.Query
2828
import com.duckduckgo.app.browser.BrowserViewModel.Command.Refresh
29+
import com.duckduckgo.app.feedback.ui.FeedbackActivity
2930
import com.duckduckgo.app.global.DuckDuckGoActivity
3031
import com.duckduckgo.app.global.ViewModelFactory
3132
import com.duckduckgo.app.global.intentText
@@ -184,6 +185,10 @@ class BrowserActivity : DuckDuckGoActivity() {
184185
viewModel.onOpenInNewTabRequested(query)
185186
}
186187

188+
fun launchBrokenSiteFeedback(url: String?) {
189+
startActivity(FeedbackActivity.intent(this, true, url))
190+
}
191+
187192
fun launchSettings() {
188193
startActivity(SettingsActivity.intent(this))
189194
}
@@ -192,7 +197,6 @@ class BrowserActivity : DuckDuckGoActivity() {
192197
startActivity(BookmarksActivity.intent(this))
193198
}
194199

195-
196200
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
197201
if (requestCode == DASHBOARD_REQUEST_CODE) {
198202
viewModel.receivedDashboardResult(resultCode)

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,9 @@ class BrowserTabFragment : Fragment(), FindListener {
201201
onMenuItemClicked(view.newTabPopupMenuItem) { browserActivity?.launchNewTab() }
202202
onMenuItemClicked(view.bookmarksPopupMenuItem) { browserActivity?.launchBookmarks() }
203203
onMenuItemClicked(view.addBookmarksPopupMenuItem) { addBookmark() }
204-
onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() }
205204
onMenuItemClicked(view.findInPageMenuItem) { viewModel.userRequestingToFindInPage() }
205+
onMenuItemClicked(view.brokenSitePopupMenuItem) { browserActivity?.launchBrokenSiteFeedback(viewModel.url.value) }
206+
onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() }
206207
onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) {
207208
viewModel.desktopSiteModeToggled(
208209
urlString = webView?.url,

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ class BrowserTabViewModel(
510510

511511
fun resetView() {
512512
site = null
513+
url.value = null
513514
onSiteChanged()
514515
initializeViewStates()
515516
}

app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksActivity
2020
import com.duckduckgo.app.browser.BrowserActivity
2121
import com.duckduckgo.app.browser.BrowserTabFragment
2222
import com.duckduckgo.app.browser.defaultBrowsing.DefaultBrowserInfoActivity
23+
import com.duckduckgo.app.feedback.ui.FeedbackActivity
2324
import com.duckduckgo.app.job.AppConfigurationJobService
2425
import com.duckduckgo.app.launch.LaunchActivity
2526
import com.duckduckgo.app.onboarding.ui.OnboardingActivity
@@ -70,6 +71,10 @@ abstract class AndroidBindingModule {
7071
@ContributesAndroidInjector
7172
abstract fun privacyTermsActivity(): PrivacyPracticesActivity
7273

74+
@ActivityScoped
75+
@ContributesAndroidInjector
76+
abstract fun feedbackActivity(): FeedbackActivity
77+
7378
@ActivityScoped
7479
@ContributesAndroidInjector
7580
abstract fun settingsActivity(): SettingsActivity

app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ package com.duckduckgo.app.di
1919
import android.app.job.JobScheduler
2020
import android.content.Context
2121
import com.duckduckgo.app.autocomplete.api.AutoCompleteService
22+
import com.duckduckgo.app.feedback.api.FeedbackSender
23+
import com.duckduckgo.app.feedback.api.FeedbackService
24+
import com.duckduckgo.app.feedback.api.FeedbackSubmitter
2225
import com.duckduckgo.app.global.AppUrl.Url
2326
import com.duckduckgo.app.global.api.ApiRequestInterceptor
2427
import com.duckduckgo.app.global.job.JobBuilder
2528
import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeListService
2629
import com.duckduckgo.app.job.AppConfigurationSyncer
2730
import com.duckduckgo.app.job.ConfigurationDownloader
31+
import com.duckduckgo.app.statistics.VariantManager
32+
import com.duckduckgo.app.statistics.store.StatisticsDataStore
2833
import com.duckduckgo.app.surrogates.api.ResourceSurrogateListService
2934
import com.duckduckgo.app.trackerdetection.api.TrackerListService
3035
import com.squareup.moshi.Moshi
@@ -83,6 +88,13 @@ class NetworkModule {
8388
fun surrogatesService(retrofit: Retrofit): ResourceSurrogateListService =
8489
retrofit.create(ResourceSurrogateListService::class.java)
8590

91+
@Provides
92+
fun feedbackService(retrofit: Retrofit): FeedbackService =
93+
retrofit.create(FeedbackService::class.java)
94+
95+
@Provides
96+
fun feedbackSender(statisticsStore: StatisticsDataStore, variantManager: VariantManager, feedbackSerice: FeedbackService): FeedbackSender =
97+
FeedbackSubmitter(statisticsStore, variantManager, feedbackSerice)
8698

8799
@Provides
88100
@Singleton
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.feedback.api
18+
19+
import android.annotation.SuppressLint
20+
import android.os.Build
21+
import com.duckduckgo.app.browser.BuildConfig
22+
import com.duckduckgo.app.feedback.api.FeedbackService.Platform
23+
import com.duckduckgo.app.feedback.api.FeedbackService.Reason
24+
import com.duckduckgo.app.statistics.VariantManager
25+
import com.duckduckgo.app.statistics.store.StatisticsDataStore
26+
import io.reactivex.schedulers.Schedulers
27+
import timber.log.Timber
28+
29+
interface FeedbackSender {
30+
fun submitGeneralFeedback(comment: String)
31+
fun submitBrokenSiteFeedback(comment: String?, url: String)
32+
}
33+
34+
class FeedbackSubmitter(
35+
private val statisticsStore: StatisticsDataStore,
36+
private val variantManager: VariantManager,
37+
private val service: FeedbackService
38+
) : FeedbackSender {
39+
40+
@SuppressLint("CheckResult")
41+
override fun submitGeneralFeedback(comment: String) {
42+
submitFeedback(Reason.GENERAL, comment, "")
43+
}
44+
45+
override fun submitBrokenSiteFeedback(comment: String?, url: String) {
46+
submitFeedback(Reason.BROKEN_SITE, comment ?: "", url)
47+
}
48+
49+
private fun submitFeedback(type: String, comment: String, url: String) {
50+
service.feedback(
51+
type,
52+
url,
53+
comment,
54+
Platform.ANDROID,
55+
Build.VERSION.SDK_INT,
56+
Build.MANUFACTURER,
57+
Build.MODEL,
58+
BuildConfig.VERSION_NAME,
59+
atbWithVariant()
60+
)
61+
.subscribeOn(Schedulers.io())
62+
.subscribe({
63+
Timber.v("Feedback submission succeeded")
64+
}, {
65+
Timber.w("Feedback submission failed ${it.localizedMessage}")
66+
})
67+
}
68+
69+
private fun atbWithVariant(): String {
70+
return statisticsStore.atb?.formatWithVariant(variantManager.getVariant()) ?: ""
71+
}
72+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.feedback.api
18+
19+
import io.reactivex.Observable
20+
import okhttp3.ResponseBody
21+
import retrofit2.http.Field
22+
import retrofit2.http.FormUrlEncoded
23+
import retrofit2.http.POST
24+
25+
interface FeedbackService {
26+
27+
object Platform {
28+
const val ANDROID = "Android"
29+
}
30+
31+
object Reason {
32+
const val BROKEN_SITE = "broken_site"
33+
const val GENERAL = "general"
34+
}
35+
36+
@FormUrlEncoded
37+
@POST("/feedback.js?type=app-feedback")
38+
fun feedback(
39+
@Field("reason") reason: String,
40+
@Field("url") url: String,
41+
@Field("comment") comment: String,
42+
@Field("platform") platform: String,
43+
@Field("os") api: Int,
44+
@Field("manufacturer") manufacturer: String,
45+
@Field("model") model: String,
46+
@Field("v") appVersion: String,
47+
@Field("atb") atb: String
48+
): Observable<ResponseBody>
49+
}

0 commit comments

Comments
 (0)