Skip to content

Commit 7bf0ba1

Browse files
authored
Fix crash that could happen when trying to download a large data URI image (#898)
* Fix crash on data uri downloaded callback This also tidies up some of the code around the DownloadConfirmationFragment, which had a leak and was doing a fair bit; moving some of the logic out of here to keep this dialog focussed on obtaining the user choice makes things simpler. * Limit length of dialog title to prevent large data URI causing ANRs
1 parent 0f478e5 commit 7bf0ba1

File tree

10 files changed

+234
-175
lines changed

10 files changed

+234
-175
lines changed

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.Command.Navigate
4040
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.DownloadFile
4141
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.OpenInNewTab
4242
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
43+
import com.duckduckgo.app.browser.downloader.FileDownloader
4344
import com.duckduckgo.app.browser.favicon.FaviconDownloader
4445
import com.duckduckgo.app.browser.logindetection.LoginDetected
4546
import com.duckduckgo.app.browser.logindetection.NavigationAwareLoginDetector
@@ -52,27 +53,22 @@ import com.duckduckgo.app.browser.session.WebViewSessionStorage
5253
import com.duckduckgo.app.cta.db.DismissedCtaDao
5354
import com.duckduckgo.app.cta.model.CtaId
5455
import com.duckduckgo.app.cta.model.DismissedCta
56+
import com.duckduckgo.app.cta.ui.*
5557
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao
5658
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
5759
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
58-
import com.duckduckgo.app.cta.ui.Cta
59-
import com.duckduckgo.app.cta.ui.CtaViewModel
60-
import com.duckduckgo.app.cta.ui.DaxBubbleCta
61-
import com.duckduckgo.app.cta.ui.DaxDialogCta
62-
import com.duckduckgo.app.cta.ui.HomePanelCta
63-
import com.duckduckgo.app.cta.ui.UseOurAppCta
64-
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_DOMAIN
65-
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
6660
import com.duckduckgo.app.global.db.AppDatabase
61+
import com.duckduckgo.app.global.events.db.UserEventEntity
62+
import com.duckduckgo.app.global.events.db.UserEventKey
63+
import com.duckduckgo.app.global.events.db.UserEventsStore
6764
import com.duckduckgo.app.global.install.AppInstallStore
6865
import com.duckduckgo.app.global.model.Site
6966
import com.duckduckgo.app.global.model.SiteFactory
70-
import com.duckduckgo.app.global.events.db.UserEventKey
71-
import com.duckduckgo.app.notification.model.UseOurAppNotification
72-
import com.duckduckgo.app.global.events.db.UserEventEntity
73-
import com.duckduckgo.app.global.events.db.UserEventsStore
7467
import com.duckduckgo.app.global.useourapp.UseOurAppDetector
68+
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_DOMAIN
69+
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
7570
import com.duckduckgo.app.notification.db.NotificationDao
71+
import com.duckduckgo.app.notification.model.UseOurAppNotification
7672
import com.duckduckgo.app.onboarding.store.AppStage
7773
import com.duckduckgo.app.onboarding.store.OnboardingStore
7874
import com.duckduckgo.app.onboarding.store.UserStageStore
@@ -209,6 +205,9 @@ class BrowserTabViewModelTest {
209205
@Mock
210206
private lateinit var mockNotificationDao: NotificationDao
211207

208+
@Mock
209+
private lateinit var mockFileDownloader: FileDownloader
210+
212211
private lateinit var mockAutoCompleteApi: AutoCompleteApi
213212

214213
private lateinit var ctaViewModel: CtaViewModel
@@ -289,7 +288,8 @@ class BrowserTabViewModelTest {
289288
userEventsStore = mockUserEventsStore,
290289
notificationDao = mockNotificationDao,
291290
useOurAppDetector = UseOurAppDetector(mockUserEventsStore),
292-
variantManager = mockVariantManager
291+
variantManager = mockVariantManager,
292+
fileDownloader = mockFileDownloader
293293
)
294294

295295
testee.loadData("abc", null, false)

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

Lines changed: 98 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,18 @@ import android.webkit.WebView.HitTestResult
4040
import android.webkit.WebView.HitTestResult.*
4141
import android.widget.EditText
4242
import android.widget.TextView
43+
import android.widget.Toast
4344
import androidx.annotation.AnyThread
4445
import androidx.annotation.StringRes
4546
import androidx.appcompat.app.AlertDialog
4647
import androidx.core.content.ContextCompat
48+
import androidx.core.content.FileProvider
49+
import androidx.core.net.toUri
4750
import androidx.core.text.HtmlCompat
4851
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
4952
import androidx.core.view.*
5053
import androidx.fragment.app.Fragment
54+
import androidx.fragment.app.commitNow
5155
import androidx.fragment.app.transaction
5256
import androidx.lifecycle.*
5357
import androidx.recyclerview.widget.LinearLayoutManager
@@ -56,6 +60,8 @@ import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment
5660
import com.duckduckgo.app.brokensite.BrokenSiteActivity
5761
import com.duckduckgo.app.brokensite.BrokenSiteData
5862
import com.duckduckgo.app.browser.BrowserTabViewModel.*
63+
import com.duckduckgo.app.browser.BrowserTabViewModel.Command.DownloadCommand
64+
import com.duckduckgo.app.browser.DownloadConfirmationFragment.DownloadConfirmationDialogListener
5965
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter
6066
import com.duckduckgo.app.browser.downloader.DownloadFailReason
6167
import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager
@@ -107,10 +113,9 @@ import org.jetbrains.anko.share
107113
import timber.log.Timber
108114
import java.io.File
109115
import javax.inject.Inject
110-
import kotlin.concurrent.thread
111116
import kotlin.coroutines.CoroutineContext
112117

113-
class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogListener, TrackersAnimatorListener {
118+
class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogListener, TrackersAnimatorListener, DownloadConfirmationDialogListener {
114119

115120
private val supervisorJob = SupervisorJob()
116121

@@ -249,14 +254,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
249254
removeDaxDialogFromActivity()
250255
renderer = BrowserTabFragmentRenderer()
251256
decorator = BrowserTabFragmentDecorator()
252-
if (savedInstanceState != null) {
253-
updateFragmentListener()
254-
}
255-
}
256-
257-
private fun updateFragmentListener() {
258-
val fragment = fragmentManager?.findFragmentByTag(DOWNLOAD_CONFIRMATION_TAG) as? DownloadConfirmationFragment
259-
fragment?.downloadListener = createDownloadListener()
260257
}
261258

262259
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -556,6 +553,35 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
556553
is Command.ShowWebContent -> webView?.show()
557554
is Command.RefreshUserAgent -> refreshUserAgent(it.host, it.isDesktop)
558555
is Command.AskToFireproofWebsite -> askToFireproofWebsite(requireContext(), it.fireproofWebsite)
556+
is DownloadCommand -> processDownloadCommand(it)
557+
}
558+
}
559+
560+
private fun processDownloadCommand(it: DownloadCommand) {
561+
when (it) {
562+
is DownloadCommand.ScanMediaFiles -> {
563+
context?.applicationContext?.let { context ->
564+
MediaScannerConnection.scanFile(context, arrayOf(it.file.absolutePath), null, null)
565+
}
566+
}
567+
is DownloadCommand.ShowDownloadFinishedNotification -> {
568+
fileDownloadNotificationManager.showDownloadFinishedNotification(it.file.name, it.file.absolutePath.toUri(), it.mimeType)
569+
}
570+
DownloadCommand.ShowDownloadInProgressNotification -> {
571+
fileDownloadNotificationManager.showDownloadInProgressNotification()
572+
}
573+
is DownloadCommand.ShowDownloadFailedNotification -> {
574+
fileDownloadNotificationManager.showDownloadFailedNotification()
575+
576+
val snackbar = Snackbar.make(toolbar, R.string.downloadFailed, Snackbar.LENGTH_INDEFINITE)
577+
if (it.reason == DownloadFailReason.DownloadManagerDisabled) {
578+
snackbar.setText(it.message)
579+
snackbar.setAction(getString(R.string.enable)) {
580+
showDownloadManagerAppSettings()
581+
}
582+
}
583+
snackbar.show()
584+
}
559585
}
560586
}
561587

@@ -1092,90 +1118,28 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
10921118

10931119
@AnyThread
10941120
private fun downloadFile(requestUserConfirmation: Boolean) {
1095-
val pendingDownload = pendingFileDownload
1096-
pendingFileDownload = null
1121+
val pendingDownload = pendingFileDownload ?: return
10971122

1098-
if (pendingDownload == null) {
1099-
return
1100-
}
1123+
pendingFileDownload = null
11011124

1102-
val downloadListener = createDownloadListener()
11031125
if (requestUserConfirmation) {
1104-
requestDownloadConfirmation(pendingDownload, downloadListener)
1126+
requestDownloadConfirmation(pendingDownload)
11051127
} else {
1106-
completeDownload(pendingDownload, downloadListener)
1128+
continueDownload(pendingDownload)
11071129
}
11081130
}
11091131

1110-
private fun closeAndReturnToSourceIfBlankTab() {
1111-
if (viewModel.url == null) {
1112-
launch {
1113-
viewModel.closeAndSelectSourceTab()
1114-
}
1115-
}
1116-
}
1117-
1118-
private fun createDownloadListener(): FileDownloadListener {
1119-
return object : FileDownloadListener {
1120-
override fun downloadStarted() {
1121-
fileDownloadNotificationManager.showDownloadInProgressNotification()
1122-
closeAndReturnToSourceIfBlankTab()
1123-
}
1124-
1125-
override fun downloadFinished(file: File, mimeType: String?) {
1126-
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null) { _, uri ->
1127-
fileDownloadNotificationManager.showDownloadFinishedNotification(file.name, uri, mimeType)
1128-
}
1129-
}
1130-
1131-
override fun downloadFailed(message: String, downloadFailReason: DownloadFailReason) {
1132-
Timber.w("Failed to download file [$message]")
1133-
1134-
fileDownloadNotificationManager.showDownloadFailedNotification()
1135-
1136-
val snackbar = Snackbar.make(toolbar, R.string.downloadFailed, Snackbar.LENGTH_INDEFINITE)
1137-
if (downloadFailReason == DownloadFailReason.DownloadManagerDisabled) {
1138-
snackbar.setText(message)
1139-
snackbar.setAction(getString(R.string.enable)) {
1140-
showDownloadManagerAppSettings()
1141-
}
1142-
}
1143-
snackbar.show()
1144-
}
1145-
1146-
override fun downloadCancelled() {
1147-
closeAndReturnToSourceIfBlankTab()
1148-
}
1149-
1150-
override fun downloadOpened() {
1151-
closeAndReturnToSourceIfBlankTab()
1152-
}
1153-
1154-
private fun showDownloadManagerAppSettings() {
1155-
try {
1156-
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
1157-
intent.data = DownloadFailReason.DOWNLOAD_MANAGER_SETTINGS_URI
1158-
startActivity(intent)
1159-
} catch (e: ActivityNotFoundException) {
1160-
Timber.w(e, "Could not open DownloadManager settings")
1161-
Snackbar.make(toolbar, R.string.downloadManagerIncompatible, Snackbar.LENGTH_INDEFINITE).show()
1162-
}
1163-
}
1164-
}
1165-
}
1166-
1167-
private fun requestDownloadConfirmation(pendingDownload: PendingFileDownload, downloadListener: FileDownloadListener) {
1168-
fragmentManager?.let {
1169-
if (!it.isStateSaved) {
1170-
DownloadConfirmationFragment.instance(pendingDownload, downloadListener).show(it, DOWNLOAD_CONFIRMATION_TAG)
1171-
}
1132+
private fun requestDownloadConfirmation(pendingDownload: PendingFileDownload) {
1133+
val downloadConfirmationFragment = DownloadConfirmationFragment.instance(pendingDownload)
1134+
childFragmentManager.findFragmentByTag(DOWNLOAD_CONFIRMATION_TAG)?.let {
1135+
Timber.i("Found existing dialog; removing it now")
1136+
childFragmentManager.commitNow { remove(it) }
11721137
}
1138+
downloadConfirmationFragment.show(childFragmentManager, DOWNLOAD_CONFIRMATION_TAG)
11731139
}
11741140

11751141
private fun completeDownload(pendingDownload: PendingFileDownload, callback: FileDownloadListener) {
1176-
thread {
1177-
fileDownloader.download(pendingDownload, callback)
1178-
}
1142+
viewModel.download(pendingDownload)
11791143
}
11801144

11811145
private fun launchFilePicker(command: Command.ShowFileChooser) {
@@ -1787,4 +1751,55 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
17871751
private fun shouldUpdateOmnibarTextInput(viewState: OmnibarViewState, omnibarInput: String?) =
17881752
(!viewState.isEditing || omnibarInput.isNullOrEmpty()) && omnibarTextInput.isDifferent(omnibarInput)
17891753
}
1754+
1755+
override fun openExistingFile(file: File?) {
1756+
if (file == null) {
1757+
Toast.makeText(activity, R.string.downloadConfirmationUnableToOpenFileText, Toast.LENGTH_SHORT).show()
1758+
return
1759+
}
1760+
1761+
val intent = context?.let { createIntentToOpenFile(it, file) }
1762+
activity?.packageManager?.let { packageManager ->
1763+
if (intent?.resolveActivity(packageManager) != null) {
1764+
startActivity(intent)
1765+
} else {
1766+
Timber.e("No suitable activity found")
1767+
Toast.makeText(activity, R.string.downloadConfirmationUnableToOpenFileText, Toast.LENGTH_SHORT).show()
1768+
}
1769+
}
1770+
}
1771+
1772+
override fun replaceExistingFile(file: File?, pendingFileDownload: PendingFileDownload) {
1773+
Timber.i("Deleting existing file: $file")
1774+
runCatching { file?.delete() }
1775+
continueDownload(pendingFileDownload)
1776+
}
1777+
1778+
private fun showDownloadManagerAppSettings() {
1779+
try {
1780+
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
1781+
intent.data = DownloadFailReason.DOWNLOAD_MANAGER_SETTINGS_URI
1782+
startActivity(intent)
1783+
} catch (e: ActivityNotFoundException) {
1784+
Timber.w(e, "Could not open DownloadManager settings")
1785+
Snackbar.make(toolbar, R.string.downloadManagerIncompatible, Snackbar.LENGTH_INDEFINITE).show()
1786+
}
1787+
}
1788+
1789+
private fun createIntentToOpenFile(context: Context, file: File): Intent? {
1790+
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", file)
1791+
val mime = activity?.contentResolver?.getType(uri) ?: return null
1792+
val intent = Intent(Intent.ACTION_VIEW)
1793+
intent.setDataAndType(uri, mime)
1794+
return intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
1795+
}
1796+
1797+
override fun continueDownload(pendingFileDownload: PendingFileDownload) {
1798+
Timber.i("Continuing to download $pendingFileDownload")
1799+
viewModel.download(pendingFileDownload)
1800+
}
1801+
1802+
override fun cancelDownload() {
1803+
viewModel.closeAndReturnToSourceIfBlankTab()
1804+
}
17901805
}

0 commit comments

Comments
 (0)