/*
- * Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2015-2024 Soren Stoutner <soren@stoutner.com>.
*
* Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
*
import android.widget.ListView
import android.widget.ProgressBar
import android.widget.RadioButton
+import android.widget.RadioGroup
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBar
-import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources
private lateinit var userAgentNamesArrayAdapter: ArrayAdapter<CharSequence>
// Define the class variables.
- private var actionBarDrawerToggle: ActionBarDrawerToggle? = null
private var appBarHeight = 0
private var bookmarksCursor: Cursor? = null
private var bookmarksDatabaseHelper: BookmarksDatabaseHelper? = null
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
- // Get the theme entry values string array.
+ // Get the entry values string arrays.
val appThemeEntryValuesStringArray = resources.getStringArray(R.array.app_theme_entry_values)
// Get the current theme status.
urlRelativeLayout = findViewById(R.id.url_relativelayout)
urlEditText = findViewById(R.id.url_edittext)
- // Create the hamburger icon at the start of the AppBar.
- actionBarDrawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer)
-
// Initially disable the sliding drawers. They will be enabled once the filter lists are loaded.
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
drawerLayout.visibility = View.GONE
// Initialize the WebView state adapter.
- webViewStateAdapter = WebViewStateAdapter(this)
+ webViewStateAdapter = WebViewStateAdapter(this, bottomAppBar)
- // Set the pager adapter on the web view pager.
+ // Set the WebView pager adapter.
webViewViewPager2.adapter = webViewStateAdapter
// Store up to 100 tabs in memory.
}
}
- public override fun onPostCreate(savedInstanceState: Bundle?) {
- // Run the default commands.
- super.onPostCreate(savedInstanceState)
-
- // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
- // If the app is restarting to change the app theme the action bar drawer toggle will not yet be populated.
- actionBarDrawerToggle?.syncState()
- }
-
override fun onNewIntent(intent: Intent) {
// Run the default commands.
super.onNewIntent(intent)
R.id.save_url -> { // Save URL.
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(currentWebView!!.currentUrl)
+ saveWithExternalApp(currentWebView!!.currentUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, currentWebView!!.currentUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
}
R.id.save_archive -> {
- // Open the file picker with a default file name built from the current domain name.
- saveWebpageArchiveActivityResultLauncher.launch(currentWebView!!.currentDomainName + ".mht")
+ // Open the file picker with a default file name built from the website title.
+ saveWebpageArchiveActivityResultLauncher.launch(currentWebView!!.title + ".mht")
// Consume the event.
true
// Add the extra information to the intent.
domainsIntent.putExtra(LOAD_DOMAIN, currentWebView!!.domainSettingsDatabaseId)
domainsIntent.putExtra(CLOSE_ON_BACK, true)
- domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
// Get the current certificate.
// Set the font size integer.
val fontSizeInt = if (textZoomInt == defaultFontSizeString.toInt()) // The current system default is used, which is encoded as a zoom of `0`.
- 0
+ SYSTEM_DEFAULT
else // A custom font size is used.
textZoomInt
// Add the extra information to the intent.
domainsIntent.putExtra(LOAD_DOMAIN, newDomainDatabaseId)
domainsIntent.putExtra(CLOSE_ON_BACK, true)
- domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
// Get the current certificate.
val domainsIntent = Intent(this, DomainsActivity::class.java)
// Add the extra information to the intent.
- domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
// Get the current certificate.
contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(linkUrl)
+ saveWithExternalApp(linkUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) { // Download with an external app.
- downloadUrlWithExternalApp(imageUrl)
+ saveWithExternalApp(imageUrl)
} else { // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
}
contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(imageUrl)
+ saveWithExternalApp(imageUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(linkUrl)
+ saveWithExternalApp(linkUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
// Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
urlEditText.clearFocus()
- // Get the new tab position.
- val newTabPosition = if (adjacent) // The new tab position is immediately to the right of the current tab position.
- tabLayout.selectedTabPosition + 1
- else // The new tab position is at the end. The tab positions are 0 indexed, so the new page number will match the current count.
- tabLayout.tabCount
+ // Add the new tab after the tab layout has quiesced.
+ // Otherwise, there can be problems when restoring a large number of tabs and processing a new intent at the same time. <https://redmine.stoutner.com/issues/1136>
+ tabLayout.post {
+ // Get the new tab position.
+ val newTabPosition = if (adjacent) // The new tab position is immediately to the right of the current tab position.
+ tabLayout.selectedTabPosition + 1
+ else // The new tab position is at the end. The tab positions are 0 indexed, so the new page number will match the current count.
+ tabLayout.tabCount
- // Add the new WebView page.
- webViewStateAdapter!!.addPage(newTabPosition, urlString)
+ // Add the new WebView page.
+ webViewStateAdapter!!.addPage(newTabPosition, urlString)
- // Add the new tab.
- addNewTab(newTabPosition, moveToTab)
+ // Add the new tab.
+ addNewTab(newTabPosition, moveToTab)
+ }
}
private fun addNewTab(newTabPosition: Int, moveToTab: Boolean) {
defaultWideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true)
defaultDisplayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true)
- // Get the WebView theme entry values string array. This is done here so that expensive resource requests are not made each time a domain is loaded.
+ // Get the string arrays. These are done here so that expensive resource requests are not made each time a domain is loaded.
webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
-
- // Get the user agent string arrays. These are done here so that expensive resource requests are not made each time a domain is loaded.
userAgentDataArray = resources.getStringArray(R.array.user_agent_data)
userAgentNamesArray = resources.getStringArray(R.array.user_agent_names)
+ val downloadProviderEntryValuesStringArray = resources.getStringArray(R.array.download_provider_entry_values)
// Get the user agent array adapters. These are done here so that expensive resource requests are not made each time a domain is loaded.
userAgentDataArrayAdapter = ArrayAdapter.createFromResource(this, R.array.user_agent_data, R.layout.spinner_item)
proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value))!!
fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false)
hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true)
- downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false)
- scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true)
+ val downloadProvider = sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))!!
+ scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), false)
+
+ // Determine if downloading should be handled by an external app.
+ downloadWithExternalApp = (downloadProvider == downloadProviderEntryValuesStringArray[2])
// Apply the saved proxy mode if the app has been restarted.
if (savedProxyMode != null) {
bookmarksListView.setSelection(0)
}
- private fun downloadUrlWithExternalApp(url: String) {
- // Create a download intent. Not specifying the action type will display the maximum number of options.
- val downloadIntent = Intent()
-
- // Set the URI and the mime type.
- downloadIntent.setDataAndType(Uri.parse(url), "text/html")
-
- // Flag the intent to open in a new task.
- downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
-
- // Show the chooser.
- startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
- }
-
private fun exitFullScreenVideo() {
// Re-enable the screen timeout.
fullScreenVideoFrameLayout.keepScreenOn = false
override fun onDrawerOpened(drawerView: View) {}
- override fun onDrawerClosed(drawerView: View) {
- // Reset the drawer icon when the drawer is closed. Otherwise, it remains an arrow if the drawer is open when the app is restarted.
- actionBarDrawerToggle!!.syncState()
- }
+ override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {
if (newState == DrawerLayout.STATE_SETTLING || newState == DrawerLayout.STATE_DRAGGING) { // A drawer is opening or closing.
registerForContextMenu(nestedScrollWebView)
// Allow the downloading of files.
- nestedScrollWebView.setDownloadListener { downloadUrlString: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long ->
- // Check the download preference.
+ nestedScrollWebView.setDownloadListener { downloadUrlString: String, userAgent: String, contentDisposition: String, mimetype: String, contentLength: Long ->
+ // Use the specified download provider.
if (downloadWithExternalApp) { // Download with an external app.
- downloadUrlWithExternalApp(downloadUrlString!!)
- } else { // Handle the download inside of Privacy Browser.
- // Define a formatted file size string.
-
+ // Download with an external app.
+ saveWithExternalApp(downloadUrlString)
+ } else { // Download with Privacy Browser or Android's download manager.
// Process the content length if it contains data.
val formattedFileSizeString = if (contentLength > 0) { // The content length is greater than 0.
// Format the content length as a string.
}
// Get the file name from the content disposition.
- val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString!!)
+ val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString)
- // Instantiate the save dialog.
- val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent!!, nestedScrollWebView.acceptCookies)
+ // Instantiate the save dialog according.
+ val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent, nestedScrollWebView.acceptCookies)
// Try to show the dialog. The download listener continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash.
try {
currentWebView!!.loadUrl(openFilePath)
}
}
+ // The view parameter cannot be removed because it is called from the layout onClick.
+ fun openNavigationDrawer(@Suppress("UNUSED_PARAMETER")view: View) {
+ // Open the navigation drawer.
+ drawerLayout.openDrawer(GravityCompat.START)
+ }
private fun openWithApp(url: String) {
// Create an open with app intent with `ACTION_VIEW`.
return sanitizedUrlString
}
- override fun saveUrl(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
+ override fun saveWithAndroidDownloadManager(dialogFragment: DialogFragment) {
+ // Get the dialog.
+ val dialog = dialogFragment.dialog!!
+
+ // Get handles for the dialog views.
+ val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
+ val downloadDirectoryRadioGroup = dialog.findViewById<RadioGroup>(R.id.download_directory_radiogroup)
+ val dialogFileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
+
+ // Get the string from the edit texts, which may have been modified by the user.
+ val saveUrlString = dialogUrlEditText.text.toString()
+ val fileNameString = dialogFileNameEditText.text.toString()
+
+ // Get a handle for the system download service.
+ val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
+
+ // Parse the URL.
+ val downloadRequest = DownloadManager.Request(Uri.parse(saveUrlString))
+
+ // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for the URL.
+ val cookiesString = cookieManager.getCookie(saveUrlString)
+
+ // Add the cookies to the download request. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookiesString)
+ }
+
+ // Get the download directory.
+ val downloadDirectory = when (downloadDirectoryRadioGroup.checkedRadioButtonId) {
+ R.id.downloads_radiobutton -> Environment.DIRECTORY_DOWNLOADS
+ R.id.documents_radiobutton -> Environment.DIRECTORY_DOCUMENTS
+ R.id.pictures_radiobutton -> Environment.DIRECTORY_PICTURES
+ else -> Environment.DIRECTORY_MUSIC
+ }
+
+ // Set the download destination.
+ downloadRequest.setDestinationInExternalPublicDir(downloadDirectory, fileNameString)
+
+ // Allow media scanner to index the download if it is a media file. This is automatic for API >= 29.
+ @Suppress("DEPRECATION")
+ if (Build.VERSION.SDK_INT <= 28)
+ downloadRequest.allowScanningByMediaScanner()
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(saveUrlString)
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest)
+ }
+
+ private fun saveWithExternalApp(url: String) {
+ // Create a download intent. Not specifying the action type will display the maximum number of options.
+ val downloadIntent = Intent()
+
+ // Set the URI and the mime type.
+ downloadIntent.setDataAndType(Uri.parse(url), "text/html")
+
+ // Flag the intent to open in a new task.
+ downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ // Show the chooser.
+ startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
+ }
+
+ override fun saveWithPrivacyBrowser(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
// Store the URL. This will be used in the save URL activity result launcher.
saveUrlString = if (originalUrlString.startsWith("data:")) {
// Save the original URL.
// Get a handle for the dialog URL edit text.
val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
- // Get the URL from the edit text, which may have been modified.
+ // Get the URL from the edit text, which may have been modified by the user.
dialogUrlEditText.text.toString()
}