+
+ private fun loadBookmarksFolder() {
+ // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
+ bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
+
+ // Populate the bookmarks cursor adapter.
+ bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
+ override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
+ // Inflate the individual item layout.
+ return layoutInflater.inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false)
+ }
+
+ override fun bindView(view: View, context: Context, cursor: Cursor) {
+ // Get handles for the views.
+ val bookmarkFavoriteIcon = view.findViewById<ImageView>(R.id.bookmark_favorite_icon)
+ val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
+
+ // Get the favorite icon byte array from the cursor.
+ val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
+
+ // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
+ val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
+
+ // Display the bitmap in the bookmark favorite icon.
+ bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap)
+
+ // Display the bookmark name from the cursor in the bookmark name text view.
+ bookmarkNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+
+ // Make the font bold for folders.
+ if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
+ bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
+ else // Reset the font to default for normal bookmarks.
+ bookmarkNameTextView.typeface = Typeface.DEFAULT
+ }
+ }
+
+ // Populate the list view with the adapter.
+ bookmarksListView.adapter = bookmarksCursorAdapter
+
+ // Set the bookmarks drawer title.
+ if (currentBookmarksFolderId == HOME_FOLDER_ID) // The current bookmarks folder is the home folder.
+ bookmarksTitleTextView.setText(R.string.bookmarks)
+ else
+ bookmarksTitleTextView.text = bookmarksDatabaseHelper!!.getFolderName(currentBookmarksFolderId)
+ }
+
+ private fun loadUrl(nestedScrollWebView: NestedScrollWebView, url: String) {
+ // Sanitize the URL.
+ val urlString = sanitizeUrl(url)
+
+ // Apply the domain settings and load the URL.
+ applyDomainSettings(nestedScrollWebView, urlString, resetTab = true, reloadWebsite = false, loadUrl = true)
+ }
+
+ private fun loadUrlFromTextBox() {
+ // Get the text from URL text box and convert it to a string. trim() removes white spaces from the beginning and end of the string.
+ var unformattedUrlString = urlEditText.text.toString().trim { it <= ' ' }
+
+ // Create the formatted URL string.
+ var urlString = ""
+
+ // Check to see if the unformatted URL string is a valid URL. Otherwise, convert it into a search.
+ if (unformattedUrlString.startsWith("content://")) { // This is a content URL.
+ // Load the entire content URL.
+ urlString = unformattedUrlString
+ } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
+ unformattedUrlString.startsWith("file://")) { // This is a standard URL.
+
+ // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
+ if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://"))
+ unformattedUrlString = "https://$unformattedUrlString"
+
+ // Initialize the unformatted URL.
+ var unformattedUrl: URL? = null
+
+ // Convert the unformatted URL string to a URL.
+ try {
+ unformattedUrl = URL(unformattedUrlString)
+ } catch (exception: MalformedURLException) {
+ exception.printStackTrace()
+ }
+
+ // Get the components of the URL.
+ val scheme = unformattedUrl?.protocol
+ val authority = unformattedUrl?.authority
+ val path = unformattedUrl?.path
+ val query = unformattedUrl?.query
+ val fragment = unformattedUrl?.ref
+
+ // Create a URI.
+ val uri = Uri.Builder()
+
+ // Build the URI from the components of the URL.
+ uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment)
+
+ // Decode the URI as a UTF-8 string in.
+ try {
+ urlString = URLDecoder.decode(uri.build().toString(), "UTF-8")
+ } catch (exception: UnsupportedEncodingException) {
+ // Do nothing. The formatted URL string will remain blank.
+ }
+ } else if (unformattedUrlString.isNotEmpty()) { // This is not a URL, but rather a search string.
+ // Sanitize the search input.
+ val encodedSearchString = try {
+ URLEncoder.encode(unformattedUrlString, "UTF-8")
+ } catch (exception: UnsupportedEncodingException) {
+ ""
+ }
+
+ // Add the base search URL.
+ urlString = searchURL + encodedSearchString
+ }
+
+ // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
+ urlEditText.clearFocus()
+
+ // Make it so.
+ loadUrl(currentWebView!!, urlString)
+ }
+
+ override fun navigateHistory(url: String, steps: Int) {
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView!!, url, resetTab = false, reloadWebsite = false, loadUrl = false)
+
+ // Load the history entry.
+ currentWebView!!.goBackOrForward(steps)
+ }
+
+ override fun openFile(dialogFragment: DialogFragment) {
+ // Get the dialog.
+ val dialog = dialogFragment.dialog!!
+
+ // Get handles for the views.
+ val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
+ val mhtCheckBox = dialog.findViewById<CheckBox>(R.id.mht_checkbox)
+
+ // Get the file path string.
+ val openFilePath = fileNameEditText.text.toString()
+
+ // Apply the domain settings. This resets the favorite icon and removes any domain settings.
+ applyDomainSettings(currentWebView!!, openFilePath, resetTab = true, reloadWebsite = false, loadUrl = false)
+
+ // Open the file according to the type.
+ if (mhtCheckBox.isChecked) { // Force opening of an MHT file.
+ try {
+ // Get the MHT file input stream.
+ val mhtFileInputStream = contentResolver.openInputStream(Uri.parse(openFilePath))
+
+ // Create a temporary MHT file.
+ val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
+
+ // Get a file output stream for the temporary MHT file.
+ val temporaryMhtFileOutputStream = FileOutputStream(temporaryMhtFile)
+
+ // Create a transfer byte array.
+ val transferByteArray = ByteArray(1024)
+
+ // Create an integer to track the number of bytes read.
+ var bytesRead: Int
+
+ // Copy the temporary MHT file input stream to the MHT output stream.
+ while (mhtFileInputStream!!.read(transferByteArray).also { bytesRead = it } > 0)
+ temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead)
+
+ // Flush the temporary MHT file output stream.
+ temporaryMhtFileOutputStream.flush()
+
+ // Close the streams.
+ temporaryMhtFileOutputStream.close()
+ mhtFileInputStream.close()
+
+ // Load the temporary MHT file.
+ currentWebView!!.loadUrl(temporaryMhtFile.toString())
+ } catch (exception: Exception) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+ }
+ } else { // Let the WebView handle opening of the file.
+ // Open the file.
+ currentWebView!!.loadUrl(openFilePath)
+ }
+ }
+
+ private fun openWithApp(url: String) {
+ // Create an open with app intent with `ACTION_VIEW`.
+ val openWithAppIntent = Intent(Intent.ACTION_VIEW)
+
+ // Set the URI but not the MIME type. This should open all available apps.
+ openWithAppIntent.data = Uri.parse(url)
+
+ // Flag the intent to open in a new task.
+ openWithAppIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ // Try the intent.
+ try {
+ // Show the chooser.
+ startActivity(openWithAppIntent)
+ } catch (exception: ActivityNotFoundException) { // There are no apps available to open the URL.
+ // Show a snackbar with the error.
+ Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+ }
+ }
+
+ private fun openWithBrowser(url: String) {
+
+ // Create an open with browser intent with `ACTION_VIEW`.
+ val openWithBrowserIntent = Intent(Intent.ACTION_VIEW)
+
+ // Set the URI and the MIME type. `"text/html"` should load browser options.
+ openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html")
+
+ // Flag the intent to open in a new task.
+ openWithBrowserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ // Try the intent.
+ try {
+ // Show the chooser.
+ startActivity(openWithBrowserIntent)
+ } catch (exception: ActivityNotFoundException) { // There are no browsers available to open the URL.
+ // Show a snackbar with the error.
+ Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+ }
+ }
+
+ override fun pinnedErrorGoBack() {
+ // Get the current web back forward list.
+ val webBackForwardList = currentWebView!!.copyBackForwardList()
+
+ // Get the previous entry URL.
+ val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
+
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
+
+ // Go back.
+ currentWebView!!.goBack()
+ }
+
+ private fun sanitizeUrl(urlString: String): String {
+ // Initialize a sanitized URL string.
+ var sanitizedUrlString = urlString
+
+ // Sanitize tracking queries.
+ if (sanitizeTrackingQueries)
+ sanitizedUrlString = SanitizeUrlHelper.sanitizeTrackingQueries(sanitizedUrlString)
+
+ // Sanitize AMP redirects.
+ if (sanitizeAmpRedirects)
+ sanitizedUrlString = SanitizeUrlHelper.sanitizeAmpRedirects(sanitizedUrlString)
+
+ // Return the sanitized URL string.
+ return sanitizedUrlString
+ }
+
+ override fun saveUrl(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.
+ originalUrlString
+ } else {
+ // Get the dialog.
+ val dialog = dialogFragment.dialog!!
+
+ // 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.
+ dialogUrlEditText.text.toString()
+ }
+
+ // Open the file picker.
+ saveUrlActivityResultLauncher.launch(fileNameString)
+ }
+
+ private fun setCurrentWebView(pageNumber: Int) {
+ // Stop the swipe to refresh indicator if it is running
+ swipeRefreshLayout.isRefreshing = false
+
+ // Get the WebView tab fragment.
+ val webViewTabFragment = webViewStateAdapter!!.getPageFragment(pageNumber)
+
+ // Get the fragment view.
+ val webViewFragmentView = webViewTabFragment.view
+
+ // Set the current WebView if the fragment view is not null.
+ if (webViewFragmentView != null) { // The fragment has been populated.
+ // Store the current WebView.
+ currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview)
+
+ // Update the status of swipe to refresh.
+ if (currentWebView!!.swipeToRefresh) { // Swipe to refresh is enabled.
+ // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
+ swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
+ } else { // Swipe to refresh is disabled.
+ // Disable the swipe refresh layout.
+ swipeRefreshLayout.isEnabled = false
+ }
+
+ // Set the cookie status.
+ cookieManager.setAcceptCookie(currentWebView!!.acceptCookies)
+
+ // Update the privacy icons. `true` redraws the icons in the app bar.
+ updatePrivacyIcons(true)
+
+ // Get a handle for the input method manager.
+ val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
+
+ // Get the current URL.
+ val urlString = currentWebView!!.url
+
+ // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
+ if (!loadingNewIntent) { // A new intent is not being loaded.
+ if ((urlString == null) || (urlString == "about:blank")) { // The WebView is blank.
+ // Display the hint in the URL edit text.
+ urlEditText.setText("")
+
+ // Request focus for the URL text box.
+ urlEditText.requestFocus()
+
+ // Display the keyboard.
+ inputMethodManager.showSoftInput(urlEditText, 0)
+ } else { // The WebView has a loaded URL.
+ // Clear the focus from the URL text box.
+ urlEditText.clearFocus()
+
+ // Hide the soft keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
+
+ // Display the current URL in the URL text box.
+ urlEditText.setText(urlString)
+
+ // Highlight the URL syntax.
+ UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
+ }
+ } else { // A new intent is being loaded.
+ // Reset the loading new intent flag.
+ loadingNewIntent = false
+ }
+
+ // Set the background to indicate the domain settings status.
+ if (currentWebView!!.domainSettingsApplied) {
+ // Set a background on the URL relative layout to indicate that custom domain settings are being used.
+ urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
+ } else {
+ // Remove any background on the URL relative layout.
+ urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
+ }
+ } else if ((pageNumber == savedTabPosition) || (pageNumber >= (webViewStateAdapter!!.itemCount - 1))) { // The tab has not been populated yet.
+ // Try again in 100 milliseconds if the app is being restored or the a new tab has been added (the last tab).
+ // Create a handler to set the current WebView.
+ val setCurrentWebViewHandler = Handler(Looper.getMainLooper())
+
+ // Create a runnable to set the current WebView.
+ val setCurrentWebWebRunnable = Runnable {
+ // Set the current WebView.
+ setCurrentWebView(pageNumber)
+ }
+
+ // Try setting the current WebView again after 100 milliseconds.
+ setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100)
+ }
+ }
+
+ // The view parameter cannot be removed because it is called from the layout onClick.
+ fun toggleBookmarksDrawerPinned(@Suppress("UNUSED_PARAMETER")view: View?) {
+ // Toggle the bookmarks drawer pinned tracker.
+ bookmarksDrawerPinned = !bookmarksDrawerPinned
+
+ // Update the bookmarks drawer pinned image view.
+ updateBookmarksDrawerPinnedImageView()
+ }
+
+ private fun updateBookmarksDrawerPinnedImageView() {
+ // Set the current icon.
+ if (bookmarksDrawerPinned)
+ bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected)
+ else
+ bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin)
+ }
+
+ private fun updateDomainsSettingsSet() {
+ // Reset the domains settings set.
+ domainsSettingsSet = HashSet()
+
+ // Get a domains cursor.
+ val domainsCursor = domainsDatabaseHelper!!.domainNameCursorOrderedByDomain
+
+ // Get the current count of domains.
+ val domainsCount = domainsCursor.count
+
+ // Get the domain name column index.
+ val domainNameColumnIndex = domainsCursor.getColumnIndexOrThrow(DOMAIN_NAME)
+
+ // Populate the domain settings set.
+ for (i in 0 until domainsCount) {
+ // Move the domains cursor to the current row.
+ domainsCursor.moveToPosition(i)
+
+ // Store the domain name in the domain settings set.
+ domainsSettingsSet.add(domainsCursor.getString(domainNameColumnIndex))
+ }
+
+ // Close the domains cursor.
+ domainsCursor.close()
+ }
+
+ override fun updateFontSize(dialogFragment: DialogFragment) {
+ // Get the dialog.
+ val dialog = dialogFragment.dialog!!
+
+ // Get a handle for the font size edit text.
+ val fontSizeEditText = dialog.findViewById<EditText>(R.id.font_size_edittext)
+
+ // Initialize the new font size variable with the current font size.
+ var newFontSize = currentWebView!!.settings.textZoom
+
+ // Get the font size from the edit text.
+ try {
+ newFontSize = fontSizeEditText.text.toString().toInt()
+ } catch (exception: Exception) {
+ // If the edit text does not contain a valid font size do nothing.
+ }
+
+ // Apply the new font size.
+ currentWebView!!.settings.textZoom = newFontSize
+ }
+
+ private fun updatePrivacyIcons(runInvalidateOptionsMenu: Boolean) {
+ // Only update the privacy icons if the options menu and the current WebView have already been populated.
+ if ((optionsMenu != null) && (currentWebView != null)) {
+ // Update the privacy icon.
+ if (currentWebView!!.settings.javaScriptEnabled) // JavaScript is enabled.
+ optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled)
+ else if (currentWebView!!.acceptCookies) // JavaScript is disabled but cookies are enabled.
+ optionsPrivacyMenuItem.setIcon(R.drawable.warning)
+ else // All the dangerous features are disabled.
+ optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode)
+
+ // Update the cookies icon.
+ if (currentWebView!!.acceptCookies)
+ optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled)
+ else
+ optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled)
+
+ // Update the refresh icon.
+ if (optionsRefreshMenuItem.title == getString(R.string.refresh)) // The refresh icon is displayed.
+ optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
+ else // The stop icon is displayed.
+ optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
+
+ // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
+ if (runInvalidateOptionsMenu)
+ invalidateOptionsMenu()
+ }
+ }