+ // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
+ swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
+ }
+ }
+
+ // Reset the list of resource requests.
+ nestedScrollWebView.clearResourceRequests()
+
+ // Reset the requests counters.
+ nestedScrollWebView.resetRequestsCounters()
+
+ // Get the current page position.
+ val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
+
+ // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
+ if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus()) {
+ // Display the formatted URL text. The nested scroll WebView current URL preserves any initial `view-source:`, and opposed to the method URL variable.
+ urlEditText.setText(nestedScrollWebView.currentUrl)
+
+ // Highlight the URL syntax.
+ UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
+ }
+
+ // Reset the list of host IP addresses.
+ nestedScrollWebView.currentIpAddresses = ""
+
+ // Get a URI for the current URL.
+ val currentUri = Uri.parse(url)
+
+ // Get the current domain name.
+ val currentDomainName = currentUri.host
+
+ // Get the IP addresses for the current domain.
+ if (!currentDomainName.isNullOrEmpty())
+ GetHostIpAddressesCoroutine.checkPinnedMismatch(currentDomainName, nestedScrollWebView, supportFragmentManager, getString(R.string.pinned_mismatch))
+
+ // Replace Refresh with Stop if the options menu has been created and the WebView is currently displayed. (The first WebView typically begins loading before the menu items are instantiated.)
+ if ((optionsMenu != null) && (webView == currentWebView)) {
+ // Set the title.
+ optionsRefreshMenuItem.setTitle(R.string.stop)
+
+ // Set the icon if it is displayed in the AppBar.
+ if (displayAdditionalAppBarIcons)
+ optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
+ }
+ }
+
+ override fun onPageFinished(webView: WebView, url: String) {
+ // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
+ if (nestedScrollWebView.acceptCookies)
+ cookieManager.flush()
+
+ // Update the Refresh menu item if the options menu has been created and the WebView is currently displayed.
+ if (optionsMenu != null && (webView == currentWebView)) {
+ // Reset the Refresh title.
+ optionsRefreshMenuItem.setTitle(R.string.refresh)
+
+ // Reset the icon if it is displayed in the app bar.
+ if (displayAdditionalAppBarIcons)
+ optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
+ }
+
+ // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
+ // which links to `/data/data/com.stoutner.privacybrowser.standard`.
+ val privateDataDirectoryString = applicationInfo.dataDir
+
+ // Clear the cache, history, and logcat if Incognito Mode is enabled.
+ if (incognitoModeEnabled) {
+ // Clear the cache. `true` includes disk files.
+ nestedScrollWebView.clearCache(true)
+
+ // Clear the back/forward history.
+ nestedScrollWebView.clearHistory()
+
+ // Manually delete cache folders.
+ try {
+ // Delete the main cache directory.
+ Runtime.getRuntime().exec("rm -rf $privateDataDirectoryString/cache")
+ } catch (exception: IOException) {
+ // Do nothing if an error is thrown.
+ }
+
+ // Clear the logcat.
+ try {
+ // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
+ Runtime.getRuntime().exec("logcat -b all -c")
+ } catch (exception: IOException) {
+ // Do nothing.
+ }
+ }
+
+ // Clear the `Service Worker` directory.
+ try {
+ // A string array must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
+ Runtime.getRuntime().exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
+ } catch (exception: IOException) {
+ // Do nothing.
+ }
+
+ // Get the current page position.
+ val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
+
+ // Get the current URL from the nested scroll WebView. This is more accurate than using the URL passed into the method, which is sometimes not the final one.
+ val currentUrl = nestedScrollWebView.url
+
+ // Get the current tab.
+ val tab = tabLayout.getTabAt(currentPagePosition)
+
+ // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
+ // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
+ // Probably some sort of race condition when Privacy Browser is being resumed.
+ if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
+ // Check to see if the URL is `about:blank`.
+ if (currentUrl == "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)
+
+ // Apply the domain settings. This clears any settings from the previous domain.
+ applyDomainSettings(nestedScrollWebView, "", resetTab = true, reloadWebsite = false, loadUrl = false)
+
+ // Only populate the title text view if the tab has been fully created.
+ if (tab != null) {
+ // Get the custom view from the tab.
+ val tabView = tab.customView!!
+
+ // Get the title text view from the tab.
+ val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
+
+ // Set the title as the tab text.
+ tabTitleTextView.setText(R.string.new_tab)
+ }
+ } else { // The WebView has loaded a webpage.
+ // Update the URL edit text if it is not currently being edited.
+ if (!urlEditText.hasFocus()) {
+ // Sanitize the current URL. This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
+ val sanitizedUrl = sanitizeUrl(currentUrl)
+
+ // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
+ urlEditText.setText(sanitizedUrl)
+
+ // Highlight the URL syntax.
+ UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
+ }
+
+ // Only populate the title text view if the tab has been fully created.
+ if (tab != null) {
+ // Get the custom view from the tab.
+ val tabView = tab.customView!!
+
+ // Get the title text view from the tab.
+ val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
+
+ // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history.
+ tabTitleTextView.text = nestedScrollWebView.title
+ }
+ }
+ }
+ }
+
+ // Handle SSL Certificate errors. Suppress the lint warning that ignoring the error might be dangerous.
+ @SuppressLint("WebViewClientOnReceivedSslError")
+ override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
+ // Get the current website SSL certificate.
+ val currentWebsiteSslCertificate = error.certificate
+
+ // Extract the individual pieces of information from the current website SSL certificate.
+ val currentWebsiteIssuedToCName = currentWebsiteSslCertificate.issuedTo.cName
+ val currentWebsiteIssuedToOName = currentWebsiteSslCertificate.issuedTo.oName
+ val currentWebsiteIssuedToUName = currentWebsiteSslCertificate.issuedTo.uName
+ val currentWebsiteIssuedByCName = currentWebsiteSslCertificate.issuedBy.cName
+ val currentWebsiteIssuedByOName = currentWebsiteSslCertificate.issuedBy.oName
+ val currentWebsiteIssuedByUName = currentWebsiteSslCertificate.issuedBy.uName
+ val currentWebsiteSslStartDate = currentWebsiteSslCertificate.validNotBeforeDate
+ val currentWebsiteSslEndDate = currentWebsiteSslCertificate.validNotAfterDate
+
+ // Get the pinned SSL certificate.
+ val (pinnedSslCertificateStringArray, pinnedSslCertificateDateArray) = nestedScrollWebView.getPinnedSslCertificate()
+
+ // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
+ if (nestedScrollWebView.hasPinnedSslCertificate() &&
+ (currentWebsiteIssuedToCName == pinnedSslCertificateStringArray[0]) &&
+ (currentWebsiteIssuedToOName == pinnedSslCertificateStringArray[1]) &&
+ (currentWebsiteIssuedToUName == pinnedSslCertificateStringArray[2]) &&
+ (currentWebsiteIssuedByCName == pinnedSslCertificateStringArray[3]) &&
+ (currentWebsiteIssuedByOName == pinnedSslCertificateStringArray[4]) &&
+ (currentWebsiteIssuedByUName == pinnedSslCertificateStringArray[5]) &&
+ (currentWebsiteSslStartDate == pinnedSslCertificateDateArray[0]) &&
+ (currentWebsiteSslEndDate == pinnedSslCertificateDateArray[1])) {
+
+ // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
+ handler.proceed()
+ } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
+ // Store the SSL error handler.
+ nestedScrollWebView.sslErrorHandler = handler
+
+ // Instantiate an SSL certificate error alert dialog.
+ val sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.webViewFragmentId)
+
+ // Try to show the dialog. The SSL error handler continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash.
+ try {
+ // Show the SSL certificate error dialog.
+ sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error))
+ } catch (exception: Exception) {
+ // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
+ pendingDialogsArrayList.add(PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)))
+ }
+ }
+ }
+ }
+
+ // Check to see if the state is being restored.
+ if (restoringState) { // The state is being restored.
+ // Resume the nested scroll WebView JavaScript timers.
+ nestedScrollWebView.resumeTimers()
+ } else if (pageNumber == 0) { // The first page is being loaded.
+ // Set this nested scroll WebView as the current WebView.
+ currentWebView = nestedScrollWebView
+
+ // Get the intent that started the app.
+ val launchingIntent = intent
+
+ // Reset the intent. This prevents a duplicate tab from being created on restart.
+ intent = Intent()
+
+ // Get the information from the intent.
+ val launchingIntentAction = launchingIntent.action
+ val launchingIntentUriData = launchingIntent.data
+ val launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT)
+
+ // Parse the launching intent URL. Suppress the suggestions of using elvis expressions as they make the logic very difficult to follow.
+ @Suppress("IfThenToElvis") val urlToLoadString = if ((launchingIntentAction != null) && (launchingIntentAction == Intent.ACTION_WEB_SEARCH)) { // The intent contains a search string.
+ // Sanitize the search input and convert it to a search.
+ val encodedSearchString = try {
+ URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8")
+ } catch (exception: UnsupportedEncodingException) {
+ ""
+ }
+
+ // Add the search URL to the encodedSearchString
+ searchURL + encodedSearchString
+ } else if (launchingIntentUriData != null) { // The launching intent contains a URL formatted as a URI.
+ // Get the URL from the URI.
+ launchingIntentUriData.toString()
+ } else if (launchingIntentStringExtra != null) { // The launching intent contains text that might be a URL.
+ // Get the URL from the string extra.
+ launchingIntentStringExtra
+ } else if (urlString != "") { // The activity has been restarted.
+ // Load the saved URL.
+ urlString
+ } else { // The is no saved URL and there is no URL in the intent.
+ // Load the homepage.
+ sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))
+ }
+
+ // Load the website if not waiting for the proxy.
+ if (waitingForProxy) { // Store the URL to be loaded in the Nested Scroll WebView.
+ nestedScrollWebView.waitingForProxyUrlString = urlToLoadString!!
+ } else { // Load the URL.
+ loadUrl(nestedScrollWebView, urlToLoadString!!)
+ }
+
+ // Reset the intent. This prevents a duplicate tab from being created on a subsequent restart if loading an link from a new intent on restart.
+ // For example, this prevents a duplicate tab if a link is loaded from the Guide after changing the theme in the guide and then changing the theme again in the main activity.
+ intent = Intent()
+ } else { // This is not the first tab.
+ // Load the URL.
+ loadUrl(nestedScrollWebView, urlString)
+
+ // Set the focus and display the keyboard if the URL is blank.
+ if (urlString == "") {
+ // Request focus for the URL text box.
+ urlEditText.requestFocus()
+
+ // Create a display keyboard handler.
+ val displayKeyboardHandler = Handler(Looper.getMainLooper())
+
+ // Create a display keyboard runnable.
+ val displayKeyboardRunnable = Runnable {
+ // Display the keyboard.
+ inputMethodManager.showSoftInput(urlEditText, 0)
+ }
+
+ // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
+ displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100)
+ }
+ }
+ }
+
+ 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://") || unformattedUrlString.startsWith("view-source:")) { // This is a content or source 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