Skip to content

Commit 5b2bde5

Browse files
committed
Merge branch 'release/5.50.0'
2 parents c249572 + dccee69 commit 5b2bde5

32 files changed

+662
-147
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@ class BrowserTabViewModelTest {
800800
}
801801

802802
@Test
803-
fun whenUserSelectsDownloadImageOptionFromContextMenuThenDownloadFileCommandIssued() {
803+
fun whenUserSelectsDownloadImageOptionFromContextMenuThenDownloadCommandIssuedWithoutRequirementForFurtherUserConfirmation() {
804804
whenever(mockLongPressHandler.userSelectedMenuItem(any(), any()))
805805
.thenReturn(DownloadFile("example.com"))
806806

@@ -812,6 +812,7 @@ class BrowserTabViewModelTest {
812812

813813
val lastCommand = commandCaptor.lastValue as Command.DownloadImage
814814
assertEquals("example.com", lastCommand.url)
815+
assertFalse(lastCommand.requestUserConfirmation)
815816
}
816817

817818
@Test

app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchDuckD
3333
import com.nhaarman.mockitokotlin2.*
3434
import io.reactivex.Observable
3535
import kotlinx.coroutines.test.TestCoroutineDispatcher
36-
import kotlinx.coroutines.test.TestCoroutineScope
3736
import kotlinx.coroutines.test.runBlockingTest
3837
import org.junit.After
3938
import org.junit.Assert.*
@@ -155,7 +154,6 @@ class SystemSearchViewModelTest {
155154

156155
val newViewState = testee.resultsViewState.value
157156
assertNotNull(newViewState)
158-
assertEquals(QUERY, newViewState?.queryText)
159157
assertEquals(appQueryResult, newViewState?.appResults)
160158
assertEquals(autocompleteQueryResult, newViewState?.autocompleteResults)
161159
}
@@ -167,20 +165,18 @@ class SystemSearchViewModelTest {
167165

168166
val newViewState = testee.resultsViewState.value
169167
assertNotNull(newViewState)
170-
assertEquals("$QUERY ", newViewState?.queryText)
171168
assertEquals(appQueryResult, newViewState?.appResults)
172169
assertEquals(autocompleteQueryResult, newViewState?.autocompleteResults)
173170
}
174171

175172
@Test
176173
fun whenUserClearsQueryThenViewStateReset() = coroutineRule.runBlocking {
177174
testee.userUpdatedQuery(QUERY)
178-
testee.userClearedQuery()
175+
testee.userRequestedClear()
179176

180177
val newViewState = testee.resultsViewState.value
181178
assertNotNull(newViewState)
182-
assertTrue(newViewState!!.queryText.isEmpty())
183-
assertTrue(newViewState.appResults.isEmpty())
179+
assertTrue(newViewState!!.appResults.isEmpty())
184180
assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults)
185181
}
186182

@@ -191,26 +187,39 @@ class SystemSearchViewModelTest {
191187

192188
val newViewState = testee.resultsViewState.value
193189
assertNotNull(newViewState)
194-
assertTrue(newViewState!!.queryText.isEmpty())
195-
assertTrue(newViewState.appResults.isEmpty())
190+
assertTrue(newViewState!!.appResults.isEmpty())
196191
assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults)
197192
}
198193

199194
@Test
200-
fun whenUserSubmitsQueryThenBrowserLaunchedAndPixelSent() {
195+
fun whenUserSubmitsQueryThenBrowserLaunchedWithQueryAndPixelSent() {
201196
testee.userSubmittedQuery(QUERY)
202197
verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
203198
assertEquals(Command.LaunchBrowser(QUERY), commandCaptor.lastValue)
204199
verify(mockPixel).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY)
205200
}
206201

202+
@Test
203+
fun whenUserSubmitsQueryWithSpaceThenBrowserLaunchedWithTrimmedQueryAndPixelSent() {
204+
testee.userSubmittedQuery("$QUERY ")
205+
verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
206+
assertEquals(Command.LaunchBrowser(QUERY), commandCaptor.lastValue)
207+
verify(mockPixel).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY)
208+
}
209+
210+
@Test
211+
fun whenUserSubmitsBlankQueryThenIgnored() {
212+
testee.userSubmittedQuery(BLANK_QUERY)
213+
assertFalse(commandCaptor.allValues.any { it is Command.LaunchBrowser })
214+
verify(mockPixel, never()).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY)
215+
}
216+
207217
@Test
208218
fun whenUserSubmitsQueryThenOnboardingCompleted() = coroutineRule.runBlocking {
209219
testee.userSubmittedQuery(QUERY)
210220
verify(mockUserStageStore).stageCompleted(AppStage.NEW)
211221
}
212222

213-
214223
@Test
215224
fun whenUserSubmitsAutocompleteResultThenBrowserLaunchedAndPixelSent() {
216225
testee.userSubmittedAutocompleteResult(AUTOCOMPLETE_RESULT)

app/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@
3030
android:exported="false"
3131
tools:replace="android:authorities" />
3232

33+
<provider
34+
android:name="androidx.core.content.FileProvider"
35+
android:authorities="${applicationId}.provider"
36+
android:exported="false"
37+
android:grantUriPermissions="true">
38+
<meta-data
39+
android:name="android.support.FILE_PROVIDER_PATHS"
40+
android:resource="@xml/provider_paths"/>
41+
</provider>
42+
3343
<!--
3444
To protect user privacy, disable SafeBrowsing which could send URLs to Google servers
3545
https://developer.android.com/reference/android/webkit/WebView

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

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.*
6060
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter
6161
import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager
6262
import com.duckduckgo.app.browser.downloader.FileDownloader
63+
import com.duckduckgo.app.browser.downloader.FileDownloader.FileDownloadListener
6364
import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload
6465
import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder
6566
import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials
@@ -244,6 +245,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
244245
override fun onCreate(savedInstanceState: Bundle?) {
245246
super.onCreate(savedInstanceState)
246247
renderer = BrowserTabFragmentRenderer()
248+
if (savedInstanceState != null) {
249+
updateFragmentListener()
250+
}
251+
}
252+
253+
private fun updateFragmentListener() {
254+
val fragment = fragmentManager?.findFragmentByTag(DOWNLOAD_CONFIRMATION_TAG) as? DownloadConfirmationFragment
255+
fragment?.downloadListener = createDownloadListener()
247256
}
248257

249258
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -335,9 +344,15 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
335344
override fun onPause() {
336345
daxDialog = null
337346
logoHidingListener.onPause()
347+
dismissDownloadFragment()
338348
super.onPause()
339349
}
340350

351+
private fun dismissDownloadFragment() {
352+
val fragment = fragmentManager?.findFragmentByTag(DOWNLOAD_CONFIRMATION_TAG) as? DownloadConfirmationFragment
353+
fragment?.dismiss()
354+
}
355+
341356
private fun createPopupMenu() {
342357
popupMenu = BrowserPopupMenu(layoutInflater)
343358
val view = popupMenu.contentView
@@ -509,7 +524,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
509524
)
510525
)
511526
}
512-
is Command.DownloadImage -> requestImageDownload(it.url)
527+
is Command.DownloadImage -> requestImageDownload(it.url, it.requestUserConfirmation)
513528
is Command.FindInPageCommand -> webView?.findAllAsync(it.searchTerm)
514529
is Command.DismissFindInPage -> webView?.findAllAsync("")
515530
is Command.ShareLink -> launchSharePageChooser(it.url)
@@ -735,6 +750,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
735750
omnibarTextInput.onFocusChangeListener =
736751
OnFocusChangeListener { _, hasFocus: Boolean ->
737752
viewModel.onOmnibarInputStateChanged(omnibarTextInput.text.toString(), hasFocus, false)
753+
if (!hasFocus) {
754+
omnibarTextInput.hideKeyboard()
755+
focusDummy.requestFocus()
756+
}
738757
}
739758

740759
omnibarTextInput.onBackKeyListener = object : KeyboardAwareEditText.OnBackKeyListener {
@@ -809,7 +828,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
809828
}
810829

811830
it.setDownloadListener { url, _, contentDisposition, mimeType, _ ->
812-
requestFileDownload(url, contentDisposition, mimeType)
831+
requestFileDownload(url, contentDisposition, mimeType, true)
813832
}
814833

815834
it.setOnTouchListener { _, _ ->
@@ -1011,7 +1030,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
10111030
webView = null
10121031
}
10131032

1014-
private fun requestFileDownload(url: String, contentDisposition: String, mimeType: String) {
1033+
private fun requestFileDownload(url: String, contentDisposition: String, mimeType: String, requestUserConfirmation: Boolean) {
10151034
pendingFileDownload = PendingFileDownload(
10161035
url = url,
10171036
contentDisposition = contentDisposition,
@@ -1020,48 +1039,72 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
10201039
subfolder = Environment.DIRECTORY_DOWNLOADS
10211040
)
10221041

1023-
downloadFileWithPermissionCheck()
1042+
if (hasWriteStoragePermission()) {
1043+
downloadFile(requestUserConfirmation)
1044+
} else {
1045+
requestWriteStoragePermission()
1046+
}
10241047
}
10251048

1026-
private fun requestImageDownload(url: String) {
1049+
private fun requestImageDownload(url: String, requestUserConfirmation: Boolean) {
10271050
pendingFileDownload = PendingFileDownload(
10281051
url = url,
10291052
userAgent = userAgentProvider.getUserAgent(),
10301053
subfolder = Environment.DIRECTORY_PICTURES
10311054
)
10321055

1033-
downloadFileWithPermissionCheck()
1034-
}
1035-
1036-
private fun downloadFileWithPermissionCheck() {
10371056
if (hasWriteStoragePermission()) {
1038-
downloadFile()
1057+
downloadFile(requestUserConfirmation)
10391058
} else {
10401059
requestWriteStoragePermission()
10411060
}
10421061
}
10431062

10441063
@AnyThread
1045-
private fun downloadFile() {
1064+
private fun downloadFile(requestUserConfirmation: Boolean) {
10461065
val pendingDownload = pendingFileDownload
10471066
pendingFileDownload = null
1048-
thread {
1049-
fileDownloader.download(pendingDownload, object : FileDownloader.FileDownloadListener {
1050-
override fun downloadStarted() {
1051-
fileDownloadNotificationManager.showDownloadInProgressNotification()
1052-
}
10531067

1054-
override fun downloadFinished(file: File, mimeType: String?) {
1055-
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null) { _, uri ->
1056-
fileDownloadNotificationManager.showDownloadFinishedNotification(file.name, uri, mimeType)
1057-
}
1058-
}
1068+
if (pendingDownload == null) {
1069+
return
1070+
}
1071+
1072+
val downloadListener = createDownloadListener()
1073+
if (requestUserConfirmation) {
1074+
requestDownloadConfirmation(pendingDownload, downloadListener)
1075+
} else {
1076+
completeDownload(pendingDownload, downloadListener)
1077+
}
1078+
}
10591079

1060-
override fun downloadFailed(message: String) {
1061-
Timber.w("Failed to download file [$message]")
1062-
fileDownloadNotificationManager.showDownloadFailedNotification()
1080+
private fun createDownloadListener(): FileDownloadListener {
1081+
return object : FileDownloadListener {
1082+
override fun downloadStarted() {
1083+
fileDownloadNotificationManager.showDownloadInProgressNotification()
1084+
}
1085+
1086+
override fun downloadFinished(file: File, mimeType: String?) {
1087+
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null) { _, uri ->
1088+
fileDownloadNotificationManager.showDownloadFinishedNotification(file.name, uri, mimeType)
10631089
}
1064-
})
1090+
}
1091+
1092+
override fun downloadFailed(message: String) {
1093+
Timber.w("Failed to download file [$message]")
1094+
fileDownloadNotificationManager.showDownloadFailedNotification()
1095+
}
1096+
}
1097+
}
1098+
1099+
private fun requestDownloadConfirmation(pendingDownload: PendingFileDownload, downloadListener: FileDownloadListener) {
1100+
fragmentManager?.let {
1101+
DownloadConfirmationFragment.instance(pendingDownload, downloadListener).show(it, DOWNLOAD_CONFIRMATION_TAG)
1102+
}
1103+
}
1104+
1105+
private fun completeDownload(pendingDownload: PendingFileDownload, callback: FileDownloadListener) {
1106+
thread {
1107+
fileDownloader.download(pendingDownload, callback)
10651108
}
10661109
}
10671110

@@ -1084,7 +1127,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
10841127
if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE) {
10851128
if ((grantResults.isNotEmpty()) && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
10861129
Timber.i("Write external storage permission granted")
1087-
downloadFile()
1130+
downloadFile(requestUserConfirmation = false)
10881131
} else {
10891132
Timber.i("Write external storage permission refused")
10901133
Snackbar.make(toolbar, R.string.permissionRequiredToDownload, Snackbar.LENGTH_LONG).show()
@@ -1126,6 +1169,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
11261169
private const val URL_BUNDLE_KEY = "url"
11271170

11281171
private const val AUTHENTICATION_DIALOG_TAG = "AUTH_DIALOG_TAG"
1172+
private const val DOWNLOAD_CONFIRMATION_TAG = "DOWNLOAD_CONFIRMATION_TAG"
11291173
private const val DAX_DIALOG_DIALOG_TAG = "DAX_DIALOG_TAG"
11301174

11311175
private const val MAX_PROGRESS = 100

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class BrowserTabViewModel(
175175
object ShowKeyboard : Command()
176176
object HideKeyboard : Command()
177177
class ShowFullScreen(val view: View) : Command()
178-
class DownloadImage(val url: String) : Command()
178+
class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command()
179179
class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command()
180180
class ShareLink(val url: String) : Command()
181181
class CopyLink(val url: String) : Command()
@@ -734,7 +734,7 @@ class BrowserTabViewModel(
734734
true
735735
}
736736
is RequiredAction.DownloadFile -> {
737-
command.value = DownloadImage(requiredAction.url)
737+
command.value = DownloadImage(requiredAction.url, false)
738738
true
739739
}
740740
is RequiredAction.ShareLink -> {

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package com.duckduckgo.app.browser
1818

1919
import android.animation.AnimatorSet
2020
import android.animation.ObjectAnimator
21-
import android.annotation.SuppressLint
2221
import android.app.Activity
2322
import android.graphics.drawable.AnimatedVectorDrawable
2423
import android.view.View
@@ -187,15 +186,13 @@ class BrowserTrackersAnimatorHelper {
187186
}
188187
}
189188

190-
@SuppressLint("ObjectAnimatorBinding")
191-
fun animateFadeOut(view: Any): ObjectAnimator {
189+
private fun animateFadeOut(view: View): ObjectAnimator {
192190
return ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
193191
duration = DEFAULT_ANIMATION_DURATION
194192
}
195193
}
196194

197-
@SuppressLint("ObjectAnimatorBinding")
198-
fun animateFadeIn(view: Any): ObjectAnimator {
195+
private fun animateFadeIn(view: View): ObjectAnimator {
199196
return ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply {
200197
duration = DEFAULT_ANIMATION_DURATION
201198
}

0 commit comments

Comments
 (0)