diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 5b43df010b87..25aa57574d63 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -800,7 +800,7 @@ class BrowserTabViewModelTest { } @Test - fun whenUserSelectsDownloadImageOptionFromContextMenuThenDownloadFileCommandIssued() { + fun whenUserSelectsDownloadImageOptionFromContextMenuThenDownloadCommandIssuedWithoutRequirementForFurtherUserConfirmation() { whenever(mockLongPressHandler.userSelectedMenuItem(any(), any())) .thenReturn(DownloadFile("example.com")) @@ -812,6 +812,7 @@ class BrowserTabViewModelTest { val lastCommand = commandCaptor.lastValue as Command.DownloadImage assertEquals("example.com", lastCommand.url) + assertFalse(lastCommand.requestUserConfirmation) } @Test 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 82265bc171ce..e2b59724e9d4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -93,9 +93,8 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.OmnibarViewState import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager import com.duckduckgo.app.browser.downloader.FileDownloader +import com.duckduckgo.app.browser.downloader.FileDownloader.FileDownloadListener import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload -import com.duckduckgo.app.browser.downloader.NetworkFileDownloadManager.DownloadFileData -import com.duckduckgo.app.browser.downloader.NetworkFileDownloadManager.UserDownloadAction import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest @@ -296,6 +295,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) renderer = BrowserTabFragmentRenderer() + if(savedInstanceState != null) { + updateFragmentListener() + } + } + + private fun updateFragmentListener() { + val fragment = fragmentManager?.findFragmentByTag(DOWNLOAD_CONFIRMATION_TAG) as? DownloadConfirmationFragment + fragment?.downloadListener = createDownloadListener() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -387,9 +394,15 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { override fun onPause() { daxDialog = null logoHidingListener.onPause() + dismissDownloadFragment() super.onPause() } + private fun dismissDownloadFragment() { + val fragment = fragmentManager?.findFragmentByTag(DOWNLOAD_CONFIRMATION_TAG) as? DownloadConfirmationFragment + fragment?.dismiss() + } + private fun createPopupMenu() { popupMenu = BrowserPopupMenu(layoutInflater) val view = popupMenu.contentView @@ -561,7 +574,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { ) ) } - is Command.DownloadImage -> requestImageDownload(it.url) + is Command.DownloadImage -> requestImageDownload(it.url, it.requestUserConfirmation) is Command.FindInPageCommand -> webView?.findAllAsync(it.searchTerm) is Command.DismissFindInPage -> webView?.findAllAsync("") is Command.ShareLink -> launchSharePageChooser(it.url) @@ -865,7 +878,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { } it.setDownloadListener { url, _, contentDisposition, mimeType, _ -> - requestFileDownload(url, contentDisposition, mimeType) + requestFileDownload(url, contentDisposition, mimeType, true) } it.setOnTouchListener { _, _ -> @@ -1067,7 +1080,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { webView = null } - private fun requestFileDownload(url: String, contentDisposition: String, mimeType: String) { + private fun requestFileDownload(url: String, contentDisposition: String, mimeType: String, requestUserConfirmation: Boolean) { pendingFileDownload = PendingFileDownload( url = url, contentDisposition = contentDisposition, @@ -1076,55 +1089,72 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { subfolder = Environment.DIRECTORY_DOWNLOADS ) - downloadFileWithPermissionCheck() + if (hasWriteStoragePermission()) { + downloadFile(requestUserConfirmation) + } else { + requestWriteStoragePermission() + } } - private fun requestImageDownload(url: String) { + private fun requestImageDownload(url: String, requestUserConfirmation: Boolean) { pendingFileDownload = PendingFileDownload( url = url, userAgent = userAgentProvider.getUserAgent(), subfolder = Environment.DIRECTORY_PICTURES ) - downloadFileWithPermissionCheck() - } - - private fun downloadFileWithPermissionCheck() { if (hasWriteStoragePermission()) { - downloadFile() + downloadFile(requestUserConfirmation) } else { requestWriteStoragePermission() } } @AnyThread - private fun downloadFile() { + private fun downloadFile(requestUserConfirmation: Boolean) { val pendingDownload = pendingFileDownload pendingFileDownload = null - thread { - fileDownloader.download(pendingDownload, object : FileDownloader.FileDownloadListener { - override fun confirmDownload(downloadFileData: DownloadFileData, userDownloadAction: UserDownloadAction) { - val downloadConfirmationFragment = DownloadConfirmationFragment(downloadFileData, userDownloadAction) - fragmentManager?.let { - downloadConfirmationFragment.show(it, DOWNLAOD_CONFIRM_TAG) - } - } - override fun downloadStarted() { - fileDownloadNotificationManager.showDownloadInProgressNotification() - } + if (pendingDownload == null) { + return + } - override fun downloadFinished(file: File, mimeType: String?) { - MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null) { _, uri -> - fileDownloadNotificationManager.showDownloadFinishedNotification(file.name, uri, mimeType) - } - } + val downloadListener = createDownloadListener() + if (requestUserConfirmation) { + requestDownloadConfirmation(pendingDownload, downloadListener) + } else { + completeDownload(pendingDownload, downloadListener) + } + } - override fun downloadFailed(message: String) { - Timber.w("Failed to download file [$message]") - fileDownloadNotificationManager.showDownloadFailedNotification() + private fun createDownloadListener(): FileDownloadListener { + return object : FileDownloadListener { + override fun downloadStarted() { + fileDownloadNotificationManager.showDownloadInProgressNotification() + } + + override fun downloadFinished(file: File, mimeType: String?) { + MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null) { _, uri -> + fileDownloadNotificationManager.showDownloadFinishedNotification(file.name, uri, mimeType) } - }) + } + + override fun downloadFailed(message: String) { + Timber.w("Failed to download file [$message]") + fileDownloadNotificationManager.showDownloadFailedNotification() + } + } + } + + private fun requestDownloadConfirmation(pendingDownload: PendingFileDownload, downloadListener: FileDownloadListener) { + fragmentManager?.let { + DownloadConfirmationFragment.instance(pendingDownload, downloadListener).show(it, DOWNLOAD_CONFIRMATION_TAG) + } + } + + private fun completeDownload(pendingDownload: PendingFileDownload, callback: FileDownloadListener) { + thread { + fileDownloader.download(pendingDownload, callback) } } @@ -1147,7 +1177,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE) { if ((grantResults.isNotEmpty()) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Timber.i("Write external storage permission granted") - downloadFile() + downloadFile(requestUserConfirmation = false) } else { Timber.i("Write external storage permission refused") Snackbar.make(toolbar, R.string.permissionRequiredToDownload, Snackbar.LENGTH_LONG).show() @@ -1189,7 +1219,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { private const val URL_BUNDLE_KEY = "url" private const val AUTHENTICATION_DIALOG_TAG = "AUTH_DIALOG_TAG" - private const val DOWNLAOD_CONFIRM_TAG = "DOWNLAOD_CONFIRM_TAG" + private const val DOWNLOAD_CONFIRMATION_TAG = "DOWNLOAD_CONFIRMATION_TAG" private const val DAX_DIALOG_DIALOG_TAG = "DAX_DIALOG_TAG" private const val MAX_PROGRESS = 100 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 05850c553ff4..f26cc7e795c8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -175,7 +175,7 @@ class BrowserTabViewModel( object ShowKeyboard : Command() object HideKeyboard : Command() class ShowFullScreen(val view: View) : Command() - class DownloadImage(val url: String) : Command() + class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() @@ -734,7 +734,7 @@ class BrowserTabViewModel( true } is RequiredAction.DownloadFile -> { - command.value = DownloadImage(requiredAction.url) + command.value = DownloadImage(requiredAction.url, false) true } is RequiredAction.ShareLink -> { diff --git a/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt index 036b20a7eabd..ccec131898f1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt @@ -24,46 +24,73 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.core.content.FileProvider.getUriForFile -import com.duckduckgo.app.browser.downloader.NetworkFileDownloadManager.DownloadFileData -import com.duckduckgo.app.browser.downloader.NetworkFileDownloadManager.UserDownloadAction +import com.duckduckgo.app.browser.downloader.FileDownloader +import com.duckduckgo.app.browser.downloader.FileDownloader.FileDownloadListener +import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload +import com.duckduckgo.app.browser.downloader.guessFileName +import com.duckduckgo.app.browser.downloader.isDataUrl import com.duckduckgo.app.global.view.gone import com.duckduckgo.app.global.view.leftDrawable import com.duckduckgo.app.global.view.show import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.android.support.AndroidSupportInjection import kotlinx.android.synthetic.main.download_confirmation.view.* import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject +import kotlin.concurrent.thread -class DownloadConfirmationFragment( - private val downloadFileData: DownloadFileData, - private val userDownloadAction: UserDownloadAction -) : BottomSheetDialogFragment() { +class DownloadConfirmationFragment : BottomSheetDialogFragment() { + + @Inject + lateinit var downloader: FileDownloader + + lateinit var downloadListener: FileDownloadListener + + private val pendingDownload: PendingFileDownload by lazy { + arguments!![PENDING_DOWNLOAD_BUNDLE_KEY] as PendingFileDownload + } + + private var file: File? = null + + override fun onAttach(context: Context) { + AndroidSupportInjection.inject(this) + super.onAttach(context) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.download_confirmation, container, false) + setupDownload() setupViews(view) return view } + private fun setupDownload() { + file = if (!pendingDownload.isDataUrl) File(pendingDownload.directory, pendingDownload.guessFileName()) else null + } + private fun setupViews(view: View) { - view.downloadMessage.text = getString(R.string.downloadConfirmationSaveFileTitle, downloadFileData.file.name) - view.openWith.setOnClickListener { - openFile() - dismiss() - } + view.downloadMessage.text = getString(R.string.downloadConfirmationSaveFileTitle, file?.name ?: "") view.replace.setOnClickListener { - userDownloadAction.acceptAndReplace() + deleteFile() + completeDownload(pendingDownload, downloadListener) dismiss() } view.continueDownload.setOnClickListener { - userDownloadAction.accept() + completeDownload(pendingDownload, downloadListener) + dismiss() + } + view.openWith.setOnClickListener { + openFile() dismiss() } view.cancel.setOnClickListener { - userDownloadAction.cancel() + Timber.i("Cancelled download for url ${pendingDownload.url}") dismiss() } - if (downloadFileData.alreadyDownloaded) { + if (file?.exists() == true) { view.openWith.show() view.replace.show() view.continueDownload.text = getString(R.string.downloadConfirmationKeepBothFilesText) @@ -76,6 +103,20 @@ class DownloadConfirmationFragment( } } + private fun deleteFile() { + try { + file?.delete() + } catch (e: IOException) { + Toast.makeText(activity, R.string.downloadConfirmationUnableToDeleteFileText, Toast.LENGTH_SHORT).show() + } + } + + private fun completeDownload(pendingDownload: PendingFileDownload, callback: FileDownloadListener) { + thread { + downloader.download(pendingDownload, callback) + } + } + private fun openFile() { val intent = context?.let { createIntentToOpenFile(it) } activity?.packageManager?.let { packageManager -> @@ -88,11 +129,26 @@ class DownloadConfirmationFragment( } } - private fun createIntentToOpenFile(context: Context): Intent { - val uri = getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", downloadFileData.file) - val mime = activity?.contentResolver?.getType(uri) + private fun createIntentToOpenFile(context: Context): Intent? { + val file = file ?: return null + val uri = getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", file) + val mime = activity?.contentResolver?.getType(uri) ?: return null val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(uri, mime) return intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } + + companion object { + + private const val PENDING_DOWNLOAD_BUNDLE_KEY = "PENDING_DOWNLOAD_BUNDLE_KEY" + + fun instance(pendingDownload: PendingFileDownload, downloadListener: FileDownloadListener): DownloadConfirmationFragment { + val fragment = DownloadConfirmationFragment() + val bundle = Bundle() + bundle.putSerializable(PENDING_DOWNLOAD_BUNDLE_KEY, pendingDownload) + fragment.arguments = bundle + fragment.downloadListener = downloadListener + return fragment + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/downloader/DataUriDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/downloader/DataUriDownloader.kt index 2ae9bc331385..9b684783faa0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/downloader/DataUriDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/downloader/DataUriDownloader.kt @@ -35,8 +35,7 @@ class DataUriDownloader @Inject constructor( try { callback?.downloadStarted() - val parsedDataUri = dataUriParser.generate(pending.url) - when (parsedDataUri) { + when (val parsedDataUri = dataUriParser.generate(pending.url)) { is ParseResult.Invalid -> { Timber.w("Failed to extract data from data URI") callback?.downloadFailed("Failed to extract data from data URI") diff --git a/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt index d0003e2651ba..f1824c92e802 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt @@ -19,26 +19,21 @@ package com.duckduckgo.app.browser.downloader import android.os.Environment import android.webkit.URLUtil import androidx.annotation.WorkerThread -import com.duckduckgo.app.browser.downloader.NetworkFileDownloadManager.DownloadFileData -import com.duckduckgo.app.browser.downloader.NetworkFileDownloadManager.UserDownloadAction +import timber.log.Timber import java.io.File +import java.io.Serializable import javax.inject.Inject class FileDownloader @Inject constructor( private val dataUriDownloader: DataUriDownloader, - private val networkFileDownloadManager: NetworkFileDownloadManager + private val networkFileDownloader: NetworkFileDownloader ) { @WorkerThread - fun download(pending: PendingFileDownload?, callback: FileDownloadListener) { - - if (pending == null) { - return - } - + fun download(pending: PendingFileDownload, callback: FileDownloadListener) { when { - URLUtil.isNetworkUrl(pending.url) -> networkFileDownloadManager.download(pending, callback) - URLUtil.isDataUrl(pending.url) -> dataUriDownloader.download(pending, callback) + pending.isNetworkUrl -> networkFileDownloader.download(pending) + pending.isDataUrl -> dataUriDownloader.download(pending, callback) else -> callback.downloadFailed("Not supported") } } @@ -50,15 +45,21 @@ class FileDownloader @Inject constructor( val subfolder: String, val userAgent: String, val directory: File = Environment.getExternalStoragePublicDirectory(subfolder) - ) + ): Serializable interface FileDownloadListener { - fun confirmDownload( - downloadFileData: DownloadFileData, - userDownloadAction: UserDownloadAction - ) fun downloadStarted() fun downloadFinished(file: File, mimeType: String?) fun downloadFailed(message: String) } -} \ No newline at end of file +} + +fun FileDownloader.PendingFileDownload.guessFileName(): String { + val guessedFileName = URLUtil.guessFileName(url, contentDisposition, mimeType) + Timber.i("Guessed filename of $guessedFileName for url $url") + return guessedFileName +} + +val FileDownloader.PendingFileDownload.isDataUrl get() = URLUtil.isDataUrl(url) + +val FileDownloader.PendingFileDownload.isNetworkUrl get() = URLUtil.isNetworkUrl(url) \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloadManager.kt b/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloadManager.kt deleted file mode 100644 index 750a23815ea7..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloadManager.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2019 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.browser.downloader - -import timber.log.Timber -import java.io.File -import javax.inject.Inject - - -class NetworkFileDownloadManager @Inject constructor(private val networkDownloader: NetworkFileDownloader) { - - fun download( - pendingDownload: FileDownloader.PendingFileDownload, - callback: FileDownloader.FileDownloadListener - ) { - val guessedFileName = networkDownloader.guessFileName(pendingDownload) - val fileToDownload = File(pendingDownload.directory, guessedFileName) - val alreadyDownloaded = fileToDownload.exists() - callback.confirmDownload( - DownloadFileData(fileToDownload, alreadyDownloaded), - object : UserDownloadAction { - override fun acceptAndReplace() { - File(pendingDownload.directory, guessedFileName).delete() - networkDownloader.download(pendingDownload) - } - - override fun accept() { - networkDownloader.download(pendingDownload) - } - - override fun cancel() { - Timber.i("Cancelled download for url ${pendingDownload.url}") - } - }) - } - - interface UserDownloadAction { - fun accept() - fun acceptAndReplace() - fun cancel() - } - - class DownloadFileData(val file: File, val alreadyDownloaded: Boolean) -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt index 861e448a8ac1..6434c1eeef6a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt @@ -19,15 +19,14 @@ package com.duckduckgo.app.browser.downloader import android.app.DownloadManager import android.content.Context import android.webkit.CookieManager -import android.webkit.URLUtil import androidx.core.net.toUri -import timber.log.Timber +import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload import javax.inject.Inject class NetworkFileDownloader @Inject constructor(private val context: Context) { - fun download(pendingDownload: FileDownloader.PendingFileDownload) { - val guessedFileName = guessFileName(pendingDownload) + fun download(pendingDownload: PendingFileDownload) { + val guessedFileName = pendingDownload.guessFileName() val request = DownloadManager.Request(pendingDownload.url.toUri()).apply { allowScanningByMediaScanner() @@ -40,10 +39,5 @@ class NetworkFileDownloader @Inject constructor(private val context: Context) { val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager? manager?.enqueue(request) } +} - fun guessFileName(pending: FileDownloader.PendingFileDownload): String? { - val guessedFileName = URLUtil.guessFileName(pending.url, pending.contentDisposition, pending.mimeType) - Timber.i("Guessed filename of $guessedFileName for url ${pending.url}") - return guessedFileName - } -} \ No newline at end of file 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 4d7ac174deaf..c47d80824cf0 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -21,6 +21,7 @@ import com.duckduckgo.app.bookmarks.ui.BookmarksActivity import com.duckduckgo.app.brokensite.BrokenSiteActivity import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.BrowserTabFragment +import com.duckduckgo.app.browser.DownloadConfirmationFragment import com.duckduckgo.app.browser.rating.ui.AppEnjoymentDialogFragment import com.duckduckgo.app.browser.rating.ui.GiveFeedbackDialogFragment import com.duckduckgo.app.browser.rating.ui.RateAppDialogFragment @@ -133,6 +134,9 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun browserTabFragment(): BrowserTabFragment + @ContributesAndroidInjector + abstract fun downloadConfirmationFragment(): DownloadConfirmationFragment + @ContributesAndroidInjector abstract fun onboardingDefaultBrowserFragment(): DefaultBrowserPage diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 41bd0f283728..054ebac8cee9 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -24,7 +24,8 @@ Keep both Replace Open - Can\'t open file + Could not open file + Could not delete old file App Icon