diff --git a/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/FeedbackViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/FeedbackViewModelTest.kt new file mode 100644 index 000000000000..3332406ed658 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/FeedbackViewModelTest.kt @@ -0,0 +1,142 @@ +package com.duckduckgo.app.feedback.ui + +import android.arch.core.executor.testing.InstantTaskExecutorRule +import android.arch.lifecycle.Observer +import com.duckduckgo.app.InstantSchedulersRule +import com.duckduckgo.app.feedback.api.FeedbackSender +import com.duckduckgo.app.feedback.ui.FeedbackViewModel.* +import com.nhaarman.mockito_kotlin.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +class FeedbackViewModelTest { + + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + @Suppress("unused") + val schedulers = InstantSchedulersRule() + + @Mock + private lateinit var mockFeedbackSender: FeedbackSender + + @Mock + private lateinit var mockCommandObserver: Observer + + private lateinit var testee: FeedbackViewModel + + private val viewState: FeedbackViewModel.ViewState + get() = testee.viewState.value!! + + @Before + fun before() { + MockitoAnnotations.initMocks(this) + testee = FeedbackViewModel(mockFeedbackSender) + testee.command.observeForever(mockCommandObserver) + } + + @Test + fun whenInitializedThenCannotSubmit() { + assertFalse(viewState.submitAllowed) + } + + @Test + fun whenBrokenUrlSwitchedOnWithNoUrlThenUrlFocused() { + testee.onBrokenSiteUrlChanged(null) + testee.onBrokenSiteChanged(true) + verify(mockCommandObserver).onChanged(Command.FocusUrl) + } + + @Test + fun whenBrokenUrlSwitchedOnWithUrlThenMessageFocused() { + testee.onBrokenSiteUrlChanged("http://example.com") + testee.onBrokenSiteChanged(true) + verify(mockCommandObserver).onChanged(Command.FocusMessage) + } + + @Test + fun whenBrokenUrlOnWithUrlThenCanSubmit() { + testee.onBrokenSiteChanged(true) + testee.onBrokenSiteUrlChanged("http://example.com") + assertTrue(viewState.submitAllowed) + } + + @Test + fun whenBrokenUrlOnWithNullUrlThenCannotSubmit() { + testee.onBrokenSiteChanged(true) + testee.onBrokenSiteUrlChanged(null) + assertFalse(viewState.submitAllowed) + } + + @Test + fun whenBrokenUrlOnWithBlankUrlThenCannotSubmit() { + testee.onBrokenSiteChanged(true) + testee.onBrokenSiteUrlChanged(" ") + assertFalse(viewState.submitAllowed) + } + + @Test + fun whenBrokenUrlOffWithMessageThenCanSubmit() { + testee.onBrokenSiteChanged(false) + testee.onFeedbackMessageChanged("Feedback message") + assertTrue(viewState.submitAllowed) + } + + @Test + fun whenBrokenUrlOffWithNullMessageThenCannotSubmit() { + testee.onBrokenSiteChanged(false) + testee.onFeedbackMessageChanged(null) + assertFalse(viewState.submitAllowed) + } + + @Test + fun whenBrokenUrlOffWithBlankMessageThenCannotSubmit() { + testee.onBrokenSiteChanged(false) + testee.onFeedbackMessageChanged(" ") + assertFalse(viewState.submitAllowed) + } + + @Test + fun whenCanSubmitBrokenUrlAndSubmitPressedThenFeedbackSubmitted() { + val url = "http://example.com" + testee.onBrokenSiteChanged(true) + testee.onBrokenSiteUrlChanged(url) + testee.onSubmitPressed() + + verify(mockFeedbackSender).submitBrokenSiteFeedback(null, url) + verify(mockCommandObserver).onChanged(Command.ConfirmAndFinish) + } + + @Test + fun whenCannotSubmitBrokenUrlAndSubmitPressedThenFeedbackNotSubmitted() { + testee.onBrokenSiteChanged(true) + testee.onSubmitPressed() + verify(mockFeedbackSender, never()).submitBrokenSiteFeedback(any(), any()) + verify(mockCommandObserver, never()).onChanged(Command.ConfirmAndFinish) + } + + @Test + fun whenCanSubmitMessageAndSubmitPressedThenFeedbackSubmitted() { + val message = "Message" + testee.onBrokenSiteChanged(false) + testee.onFeedbackMessageChanged(message) + testee.onSubmitPressed() + verify(mockFeedbackSender).submitGeneralFeedback(message) + verify(mockCommandObserver).onChanged(Command.ConfirmAndFinish) + } + + @Test + fun whenCannotSubmitMessageAndSubmitPressedThenFeedbackNotSubmitted() { + testee.onBrokenSiteChanged(false) + testee.onSubmitPressed() + verify(mockFeedbackSender, never()).submitGeneralFeedback(any()) + verify(mockCommandObserver, never()).onChanged(Command.ConfirmAndFinish) + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index daabbe636b48..f76d8f05b86e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,6 +98,10 @@ + + android:theme="@style/ModalCardTheme" /> \ No newline at end of file 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 82a206a1d86f..b1146b1a8b35 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -26,6 +26,7 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.BrowserViewModel.Command.Query import com.duckduckgo.app.browser.BrowserViewModel.Command.Refresh +import com.duckduckgo.app.feedback.ui.FeedbackActivity import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.intentText @@ -184,6 +185,10 @@ class BrowserActivity : DuckDuckGoActivity() { viewModel.onOpenInNewTabRequested(query) } + fun launchBrokenSiteFeedback(url: String?) { + startActivity(FeedbackActivity.intent(this, true, url)) + } + fun launchSettings() { startActivity(SettingsActivity.intent(this)) } @@ -192,7 +197,6 @@ class BrowserActivity : DuckDuckGoActivity() { startActivity(BookmarksActivity.intent(this)) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == DASHBOARD_REQUEST_CODE) { viewModel.receivedDashboardResult(resultCode) 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 20419671e743..a145e97f57dc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -201,8 +201,9 @@ class BrowserTabFragment : Fragment(), FindListener { onMenuItemClicked(view.newTabPopupMenuItem) { browserActivity?.launchNewTab() } onMenuItemClicked(view.bookmarksPopupMenuItem) { browserActivity?.launchBookmarks() } onMenuItemClicked(view.addBookmarksPopupMenuItem) { addBookmark() } - onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } onMenuItemClicked(view.findInPageMenuItem) { viewModel.userRequestingToFindInPage() } + onMenuItemClicked(view.brokenSitePopupMenuItem) { browserActivity?.launchBrokenSiteFeedback(viewModel.url.value) } + onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) { viewModel.desktopSiteModeToggled( urlString = webView?.url, 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 7bd4d67a771d..3239579da9ea 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -510,6 +510,7 @@ class BrowserTabViewModel( fun resetView() { site = null + url.value = null onSiteChanged() initializeViewStates() } diff --git a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt index a17c260ba674..defec5f4e6cc 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -20,6 +20,7 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.BrowserTabFragment import com.duckduckgo.app.browser.defaultBrowsing.DefaultBrowserInfoActivity +import com.duckduckgo.app.feedback.ui.FeedbackActivity import com.duckduckgo.app.job.AppConfigurationJobService import com.duckduckgo.app.launch.LaunchActivity import com.duckduckgo.app.onboarding.ui.OnboardingActivity @@ -70,6 +71,10 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun privacyTermsActivity(): PrivacyPracticesActivity + @ActivityScoped + @ContributesAndroidInjector + abstract fun feedbackActivity(): FeedbackActivity + @ActivityScoped @ContributesAndroidInjector abstract fun settingsActivity(): SettingsActivity diff --git a/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt b/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt index e82c764a6f6a..c507303b5909 100644 --- a/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt @@ -19,12 +19,17 @@ package com.duckduckgo.app.di import android.app.job.JobScheduler import android.content.Context import com.duckduckgo.app.autocomplete.api.AutoCompleteService +import com.duckduckgo.app.feedback.api.FeedbackSender +import com.duckduckgo.app.feedback.api.FeedbackService +import com.duckduckgo.app.feedback.api.FeedbackSubmitter import com.duckduckgo.app.global.AppUrl.Url import com.duckduckgo.app.global.api.ApiRequestInterceptor import com.duckduckgo.app.global.job.JobBuilder import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeListService import com.duckduckgo.app.job.AppConfigurationSyncer import com.duckduckgo.app.job.ConfigurationDownloader +import com.duckduckgo.app.statistics.VariantManager +import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.app.surrogates.api.ResourceSurrogateListService import com.duckduckgo.app.trackerdetection.api.TrackerListService import com.squareup.moshi.Moshi @@ -83,6 +88,13 @@ class NetworkModule { fun surrogatesService(retrofit: Retrofit): ResourceSurrogateListService = retrofit.create(ResourceSurrogateListService::class.java) + @Provides + fun feedbackService(retrofit: Retrofit): FeedbackService = + retrofit.create(FeedbackService::class.java) + + @Provides + fun feedbackSender(statisticsStore: StatisticsDataStore, variantManager: VariantManager, feedbackSerice: FeedbackService): FeedbackSender = + FeedbackSubmitter(statisticsStore, variantManager, feedbackSerice) @Provides @Singleton diff --git a/app/src/main/java/com/duckduckgo/app/feedback/api/FeedbackSender.kt b/app/src/main/java/com/duckduckgo/app/feedback/api/FeedbackSender.kt new file mode 100644 index 000000000000..8001cb783e6b --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/feedback/api/FeedbackSender.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 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.feedback.api + +import android.annotation.SuppressLint +import android.os.Build +import com.duckduckgo.app.browser.BuildConfig +import com.duckduckgo.app.feedback.api.FeedbackService.Platform +import com.duckduckgo.app.feedback.api.FeedbackService.Reason +import com.duckduckgo.app.statistics.VariantManager +import com.duckduckgo.app.statistics.store.StatisticsDataStore +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +interface FeedbackSender { + fun submitGeneralFeedback(comment: String) + fun submitBrokenSiteFeedback(comment: String?, url: String) +} + +class FeedbackSubmitter( + private val statisticsStore: StatisticsDataStore, + private val variantManager: VariantManager, + private val service: FeedbackService +) : FeedbackSender { + + @SuppressLint("CheckResult") + override fun submitGeneralFeedback(comment: String) { + submitFeedback(Reason.GENERAL, comment, "") + } + + override fun submitBrokenSiteFeedback(comment: String?, url: String) { + submitFeedback(Reason.BROKEN_SITE, comment ?: "", url) + } + + private fun submitFeedback(type: String, comment: String, url: String) { + service.feedback( + type, + url, + comment, + Platform.ANDROID, + Build.VERSION.SDK_INT, + Build.MANUFACTURER, + Build.MODEL, + BuildConfig.VERSION_NAME, + atbWithVariant() + ) + .subscribeOn(Schedulers.io()) + .subscribe({ + Timber.v("Feedback submission succeeded") + }, { + Timber.w("Feedback submission failed ${it.localizedMessage}") + }) + } + + private fun atbWithVariant(): String { + return statisticsStore.atb?.formatWithVariant(variantManager.getVariant()) ?: "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/feedback/api/FeedbackService.kt b/app/src/main/java/com/duckduckgo/app/feedback/api/FeedbackService.kt new file mode 100644 index 000000000000..b9a3cfe0264e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/feedback/api/FeedbackService.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018 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.feedback.api + +import io.reactivex.Observable +import okhttp3.ResponseBody +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST + +interface FeedbackService { + + object Platform { + const val ANDROID = "Android" + } + + object Reason { + const val BROKEN_SITE = "broken_site" + const val GENERAL = "general" + } + + @FormUrlEncoded + @POST("/feedback.js?type=app-feedback") + fun feedback( + @Field("reason") reason: String, + @Field("url") url: String, + @Field("comment") comment: String, + @Field("platform") platform: String, + @Field("os") api: Int, + @Field("manufacturer") manufacturer: String, + @Field("model") model: String, + @Field("v") appVersion: String, + @Field("atb") atb: String + ): Observable +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/feedback/ui/FeedbackActivity.kt b/app/src/main/java/com/duckduckgo/app/feedback/ui/FeedbackActivity.kt new file mode 100644 index 000000000000..aec41442cbb2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/feedback/ui/FeedbackActivity.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2018 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.feedback.ui + +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.widget.EditText +import androidx.core.view.isVisible +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.feedback.ui.FeedbackViewModel.Command +import com.duckduckgo.app.feedback.ui.FeedbackViewModel.ViewState +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.ViewModelFactory +import com.duckduckgo.app.global.view.TextChangedWatcher +import kotlinx.android.synthetic.main.activity_feedback.* +import org.jetbrains.anko.longToast +import javax.inject.Inject + + +class FeedbackActivity : DuckDuckGoActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: FeedbackViewModel by lazy { + ViewModelProviders.of(this, viewModelFactory).get(FeedbackViewModel::class.java) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_feedback) + configureListeners() + configureObservers() + + if (savedInstanceState == null) { + consumeIntentExtra() + } + } + + private fun consumeIntentExtra() { + val brokenSite = intent.getBooleanExtra(BROKEN_SITE_EXTRA, false) + if (brokenSite) { + val url = intent.getStringExtra(URL_EXTRA) + viewModel.setInitialBrokenSite(url) + } + } + + private fun configureListeners() { + brokenSiteSwitch.setOnCheckedChangeListener { _, isChecked -> + viewModel.onBrokenSiteChanged(isChecked) + } + feedbackMessage.addTextChangedListener(object : TextChangedWatcher() { + override fun afterTextChanged(editable: Editable) { + viewModel.onFeedbackMessageChanged(editable.toString()) + } + }) + brokenSiteUrl.addTextChangedListener(object : TextChangedWatcher() { + override fun afterTextChanged(editable: Editable) { + viewModel.onBrokenSiteUrlChanged(editable.toString()) + } + }) + submitButton.setOnClickListener { _ -> + viewModel.onSubmitPressed() + } + } + + private fun configureObservers() { + viewModel.command.observe(this, Observer { + it?.let { processCommand(it) } + }) + viewModel.viewState.observe(this, Observer { + it?.let { render(it) } + }) + } + + private fun processCommand(command: Command) { + when (command) { + Command.FocusUrl -> brokenSiteUrl.requestFocus() + Command.FocusMessage -> feedbackMessage.requestFocus() + Command.ConfirmAndFinish -> confirmAndFinish() + } + } + + private fun confirmAndFinish() { + longToast(R.string.feedbackSubmitted) + finish() + } + + private fun render(viewState: ViewState) { + brokenSiteSwitch.isChecked = viewState.isBrokenSite + brokenSiteUrl.isVisible = viewState.showUrl + brokenSiteUrl.updateText(viewState.url ?: "") + submitButton.isEnabled = viewState.submitAllowed + } + + private fun EditText.updateText(newText: String) { + if (text.toString() != newText) { + setText(newText) + } + } + + companion object { + + private const val BROKEN_SITE_EXTRA = "BROKEN_SITE_EXTRA" + private const val URL_EXTRA = "URL_EXTRA" + + fun intent(context: Context, brokenSite: Boolean = false, url: String? = null): Intent { + val intent = Intent(context, FeedbackActivity::class.java) + intent.putExtra(BROKEN_SITE_EXTRA, brokenSite) + if (url != null) { + intent.putExtra(URL_EXTRA, url) + } + return intent + } + + } +} diff --git a/app/src/main/java/com/duckduckgo/app/feedback/ui/FeedbackViewModel.kt b/app/src/main/java/com/duckduckgo/app/feedback/ui/FeedbackViewModel.kt new file mode 100644 index 000000000000..15df3cb6fb0e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/feedback/ui/FeedbackViewModel.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018 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.feedback.ui + +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.ViewModel +import com.duckduckgo.app.feedback.api.FeedbackSender +import com.duckduckgo.app.global.SingleLiveEvent + + +class FeedbackViewModel(private val feedbackSender: FeedbackSender) : ViewModel() { + + data class ViewState( + val isBrokenSite: Boolean = false, + val url: String? = null, + val showUrl: Boolean = false, + val message: String? = null, + val submitAllowed: Boolean = false + ) + + sealed class Command { + object FocusUrl : Command() + object FocusMessage : Command() + object ConfirmAndFinish : Command() + } + + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() + + private val viewValue: ViewState get() = viewState.value!! + + init { + viewState.value = ViewState() + } + + fun setInitialBrokenSite(url: String?) { + onBrokenSiteUrlChanged(url) + onBrokenSiteChanged(true) + } + + fun onBrokenSiteChanged(isBroken: Boolean) { + if (isBroken == viewState.value?.isBrokenSite) { + return + } + + viewState.value = viewState.value?.copy( + isBrokenSite = isBroken, + showUrl = isBroken, + submitAllowed = canSubmit(isBroken, viewValue.url, viewValue.message) + ) + + if (isBroken && viewValue.url.isNullOrBlank()) { + command.value = Command.FocusUrl + } else { + command.value = Command.FocusMessage + } + } + + fun onBrokenSiteUrlChanged(newUrl: String?) { + viewState.value = viewState.value?.copy( + url = newUrl, + submitAllowed = canSubmit(viewValue.isBrokenSite, newUrl, viewValue.message) + ) + } + + fun onFeedbackMessageChanged(newMessage: String?) { + viewState.value = viewState.value?.copy( + message = newMessage, + submitAllowed = canSubmit(viewValue.isBrokenSite, viewValue.url, newMessage) + ) + } + + + private fun canSubmit(isBrokenSite: Boolean, url: String?, feedbackMessage: String?): Boolean { + return (isBrokenSite && !url.isNullOrBlank()) || (!isBrokenSite && !feedbackMessage.isNullOrBlank()) + } + + fun onSubmitPressed() { + if (viewValue.isBrokenSite) { + val url = viewValue.url ?: return + feedbackSender.submitBrokenSiteFeedback(viewValue.message, url) + } else { + val message = viewValue.message ?: return + feedbackSender.submitGeneralFeedback(message) + } + + command.value = Command.ConfirmAndFinish + } + +} \ No newline at end of file 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 d3129e746347..c732b286868d 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -28,6 +28,8 @@ import com.duckduckgo.app.browser.LongPressHandler import com.duckduckgo.app.browser.defaultBrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultBrowsing.DefaultBrowserNotification import com.duckduckgo.app.browser.omnibar.QueryUrlConverter +import com.duckduckgo.app.feedback.api.FeedbackSender +import com.duckduckgo.app.feedback.ui.FeedbackViewModel import com.duckduckgo.app.global.db.AppConfigurationDao import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.launch.LaunchViewModel @@ -65,7 +67,8 @@ class ViewModelFactory @Inject constructor( private val defaultBrowserNotification: DefaultBrowserNotification, private val webViewLongPressHandler: LongPressHandler, private val defaultBrowserDetector: DefaultBrowserDetector, - private val variantManager: VariantManager + private val variantManager: VariantManager, + private val feedbackSender: FeedbackSender ) : ViewModelProvider.NewInstanceFactory() { @@ -81,6 +84,7 @@ class ViewModelFactory @Inject constructor( isAssignableFrom(ScorecardViewModel::class.java) -> ScorecardViewModel(privacySettingsStore) isAssignableFrom(TrackerNetworksViewModel::class.java) -> TrackerNetworksViewModel() isAssignableFrom(PrivacyPracticesViewModel::class.java) -> PrivacyPracticesViewModel() + isAssignableFrom(FeedbackViewModel::class.java) -> FeedbackViewModel(feedbackSender) isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel( appSettingsPreferencesStore, defaultBrowserDetector) isAssignableFrom(BookmarksViewModel::class.java) -> BookmarksViewModel(bookmarksDao) else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index 6576b9c97921..ba20062465bd 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -26,9 +26,8 @@ import android.support.v7.widget.SwitchCompat import android.view.View import android.widget.CompoundButton.OnCheckedChangeListener import com.duckduckgo.app.about.AboutDuckDuckGoActivity -import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R -import com.duckduckgo.app.global.AppUrl +import com.duckduckgo.app.feedback.ui.FeedbackActivity import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.view.launchDefaultAppActivity @@ -120,8 +119,7 @@ class SettingsActivity : DuckDuckGoActivity() { } private fun launchFeedback() { - startActivity(BrowserActivity.intent(this, AppUrl.Url.FEEDBACK)) - finish() + startActivity(Intent(FeedbackActivity.intent(this))) } companion object { diff --git a/app/src/main/res/color/modal_card_button_background.xml b/app/src/main/res/color/modal_card_button_background.xml new file mode 100644 index 000000000000..78f05512e025 --- /dev/null +++ b/app/src/main/res/color/modal_card_button_background.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_icon_heart_26dp.xml b/app/src/main/res/drawable/ic_icon_heart_26dp.xml new file mode 100644 index 000000000000..b35660820d78 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_heart_26dp.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_icon_smiley_face.xml b/app/src/main/res/drawable/icon_smiley_face.xml similarity index 100% rename from app/src/main/res/drawable/ic_icon_smiley_face.xml rename to app/src/main/res/drawable/icon_smiley_face.xml diff --git a/app/src/main/res/drawable/default_browser_rounded_button_bg.xml b/app/src/main/res/drawable/modal_card_button_bg.xml similarity index 85% rename from app/src/main/res/drawable/default_browser_rounded_button_bg.xml rename to app/src/main/res/drawable/modal_card_button_bg.xml index a04f7c563cea..b7ef3feb285c 100644 --- a/app/src/main/res/drawable/default_browser_rounded_button_bg.xml +++ b/app/src/main/res/drawable/modal_card_button_bg.xml @@ -15,10 +15,10 @@ --> - + + android:color="@color/modal_card_button_background" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_info_top_background.xml b/app/src/main/res/drawable/notification_info_top_background.xml index c409991bef6f..fae4c133686c 100644 --- a/app/src/main/res/drawable/notification_info_top_background.xml +++ b/app/src/main/res/drawable/notification_info_top_background.xml @@ -15,10 +15,10 @@ --> - + + android:color="@color/cornflowerBlue" /> diff --git a/app/src/main/res/layout-land/activity_default_browser_info.xml b/app/src/main/res/layout-land/activity_default_browser_info.xml index 50d1c6b16b59..85bc3b955630 100644 --- a/app/src/main/res/layout-land/activity_default_browser_info.xml +++ b/app/src/main/res/layout-land/activity_default_browser_info.xml @@ -28,7 +28,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/notification_info_top_background" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:gravity="center_horizontal" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="@+id/description" @@ -41,9 +41,9 @@ android:layout_width="26dp" android:layout_height="26dp" android:layout_marginTop="30dp" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:importantForAccessibility="no" - android:src="@drawable/ic_icon_smiley_face" + android:src="@drawable/icon_smiley_face" app:layout_constraintStart_toStartOf="@+id/title" app:layout_constraintTop_toTopOf="parent" /> @@ -52,10 +52,10 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:text="@string/defaultBrowserInfoTitle" android:textColor="@color/white" - android:textSize="@dimen/defaultBrowserInfoTitleTextSize" + android:textSize="@dimen/modalCardTitleTextSize" app:layout_constraintEnd_toStartOf="@id/textEndVerticalGuideline" app:layout_constraintStart_toStartOf="@id/textStartVerticalGuideline" app:layout_constraintTop_toBottomOf="@id/smileyFace" /> @@ -66,11 +66,11 @@ android:layout_height="wrap_content" android:layout_marginEnd="38dp" android:layout_marginTop="6dp" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:paddingBottom="30dp" android:text="@string/defaultBrowserInfoMessage" android:textColor="@color/white" - android:textSize="@dimen/defaultBrowserInfoDescriptionTextSize" + android:textSize="@dimen/modalCardDescriptionTextSize" app:layout_constraintEnd_toStartOf="@id/textEndVerticalGuideline" app:layout_constraintStart_toStartOf="@id/textStartVerticalGuideline" app:layout_constraintTop_toBottomOf="@id/title" /> @@ -111,7 +111,7 @@ android:layout_width="320dp" android:layout_height="wrap_content" android:layout_marginBottom="22dp" - android:background="@color/softBlue" + android:background="@color/cornflowerBlue" android:text="@string/onboardingDefaultBrowserGoToSettingsAction" android:textColor="@color/white" android:transitionName="defaultBrowserBannerTransition" diff --git a/app/src/main/res/layout/activity_default_browser_info.xml b/app/src/main/res/layout/activity_default_browser_info.xml index 7cf549af480c..18b23f71b812 100644 --- a/app/src/main/res/layout/activity_default_browser_info.xml +++ b/app/src/main/res/layout/activity_default_browser_info.xml @@ -41,9 +41,9 @@ android:layout_width="26dp" android:layout_height="26dp" android:layout_marginTop="30dp" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:importantForAccessibility="no" - android:src="@drawable/ic_icon_smiley_face" + android:src="@drawable/icon_smiley_face" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/titleSection" /> @@ -53,11 +53,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:text="@string/defaultBrowserInfoTitle" android:textAlignment="center" android:textColor="@color/white" - android:textSize="@dimen/defaultBrowserInfoTitleTextSize" + android:textSize="@dimen/modalCardTitleTextSize" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/smileyFace" /> @@ -67,14 +67,14 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="6dp" - android:elevation="@dimen/defaultBrowserInfoCardTitleElevation" + android:elevation="@dimen/modalCardHeaderElevation" android:paddingBottom="30dp" android:paddingEnd="20dp" android:paddingStart="20dp" android:text="@string/defaultBrowserInfoMessage" android:textAlignment="center" android:textColor="@color/white" - android:textSize="@dimen/defaultBrowserInfoDescriptionTextSize" + android:textSize="@dimen/modalCardDescriptionTextSize" app:layout_constraintTop_toBottomOf="@id/title" /> + + + + + + + + + + + + + + + + + + + + + +