]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt
Bump the minimum API to 24 (Android 7).
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewSourceActivity.kt
index 173313f65da654b540f2d88af163bc59ba176aa3..a8d86ecc6a243d1e2cf28f5decdb1009ce8c23fa 100644 (file)
@@ -1,29 +1,26 @@
 /*
- * Copyright © 2017-2021 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
  *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
  *
- * Privacy Browser is free software: you can redistribute it and/or modify
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
  *
- * Privacy Browser is distributed in the hope that it will be useful,
+ * Privacy Browser Android is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
  */
 
 package com.stoutner.privacybrowser.activities
 
-import android.content.res.Configuration
-import android.os.Build
 import android.os.Bundle
 import android.text.SpannableStringBuilder
-import android.text.Spanned
 import android.text.style.ForegroundColorSpan
 import android.util.TypedValue
 import android.view.KeyEvent
@@ -50,37 +47,46 @@ import com.google.android.material.snackbar.Snackbar
 
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog
+import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
+import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
 import com.stoutner.privacybrowser.helpers.ProxyHelper
+import com.stoutner.privacybrowser.helpers.UrlHelper
 import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory
 import com.stoutner.privacybrowser.viewmodels.WebViewSource
 
-import java.util.Locale
-
-// Declare the public constants.
+// Define the public constants.
 const val CURRENT_URL = "current_url"
 const val USER_AGENT = "user_agent"
 
-class ViewSourceActivity: AppCompatActivity() {
+class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener {
     // Declare the class variables.
     private lateinit var initialGrayColorSpan: ForegroundColorSpan
     private lateinit var finalGrayColorSpan: ForegroundColorSpan
     private lateinit var redColorSpan: ForegroundColorSpan
+    private lateinit var webViewSource: WebViewSource
+
+    // Declare the class views.
+    private lateinit var urlEditText: EditText
+    private lateinit var requestHeadersTitleTextView: TextView
+    private lateinit var requestHeadersTextView: TextView
+    private lateinit var responseMessageTitleTextView: TextView
+    private lateinit var responseMessageTextView: TextView
+    private lateinit var responseHeadersTitleTextView: TextView
+    private lateinit var responseBodyTitleTextView: TextView
 
     override fun onCreate(savedInstanceState: Bundle?) {
         // Get a handle for the shared preferences.
         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
 
-        // Get the screenshot preference.
+        // Get the preferences.
         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+        val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
 
         // Disable screenshots if not allowed.
         if (!allowScreenshots) {
             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
         }
 
-        // Set the theme.
-        setTheme(R.style.PrivacyBrowser)
-
         // Run the default commands.
         super.onCreate(savedInstanceState)
 
@@ -88,11 +94,15 @@ class ViewSourceActivity: AppCompatActivity() {
         val intent = intent
 
         // Get the information from the intent.
-        val currentUrl = intent.getStringExtra(CURRENT_URL)
-        val userAgent = intent.getStringExtra(USER_AGENT)
+        val currentUrl = intent.getStringExtra(CURRENT_URL)!!
+        val userAgent = intent.getStringExtra(USER_AGENT)!!
 
         // Set the content view.
-        setContentView(R.layout.view_source_coordinatorlayout)
+        if (bottomAppBar) {
+            setContentView(R.layout.view_source_bottom_appbar)
+        } else {
+            setContentView(R.layout.view_source_top_appbar)
+        }
 
         // Get a handle for the toolbar.
         val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
@@ -104,43 +114,34 @@ class ViewSourceActivity: AppCompatActivity() {
         val actionBar = supportActionBar!!
 
         // Add the custom layout to the action bar.
-        actionBar.setCustomView(R.layout.view_source_app_bar)
+        actionBar.setCustomView(R.layout.view_source_appbar_custom_view)
 
         // Instruct the action bar to display a custom layout.
         actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
 
         // Get handles for the views.
-        val urlEditText = findViewById<EditText>(R.id.url_edittext)
-        val requestHeadersTextView = findViewById<TextView>(R.id.request_headers)
-        val responseMessageTextView = findViewById<TextView>(R.id.response_message)
-        val responseHeadersTextView = findViewById<TextView>(R.id.response_headers)
-        val responseBodyTextView = findViewById<TextView>(R.id.response_body)
+        urlEditText = findViewById(R.id.url_edittext)
         val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
         val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
+        requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
+        requestHeadersTextView = findViewById(R.id.request_headers_textview)
+        responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
+        responseMessageTextView = findViewById(R.id.response_message_textview)
+        responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
+        val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
+        responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
+        val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
 
         // Populate the URL text box.
         urlEditText.setText(currentUrl)
 
-        // Initialize the gray foreground color spans for highlighting the URLs.  The deprecated `getColor()` must be used until the minimum API >= 23.
-        @Suppress("DEPRECATION")
-        initialGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
-        @Suppress("DEPRECATION")
-        finalGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
-
-        // Get the current theme status.
-        val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
-
-        // Set the red color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
-        redColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-            @Suppress("DEPRECATION")
-            ForegroundColorSpan(resources.getColor(R.color.red_a700))
-        } else {
-            @Suppress("DEPRECATION")
-            ForegroundColorSpan(resources.getColor(R.color.red_900))
-        }
+        // Initialize the gray foreground color spans for highlighting the URLs.
+        initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
+        finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
+        redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
 
         // Apply text highlighting to the URL.
-        highlightUrlText()
+        UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
 
         // Get a handle for the input method manager, which is used to hide the keyboard.
         val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
@@ -160,16 +161,12 @@ class ViewSourceActivity: AppCompatActivity() {
                 urlEditText.setSelection(0)
 
                 // Reapply the highlighting.
-                highlightUrlText()
+                UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
             }
         }
 
         // Set the refresh color scheme according to the theme.
-        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-            swipeRefreshLayout.setColorSchemeResources(R.color.blue_700)
-        } else {
-            swipeRefreshLayout.setColorSchemeResources(R.color.violet_500)
-        }
+        swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
 
         // Initialize a color background typed value.
         val colorBackgroundTypedValue = TypedValue()
@@ -183,62 +180,53 @@ class ViewSourceActivity: AppCompatActivity() {
         // Set the swipe refresh background color.
         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
 
-        // Populate the locale string.
-        val localeString = if (Build.VERSION.SDK_INT >= 24) {  // SDK >= 24 has a list of locales.
-            // Get the list of locales.
-            val localeList = resources.configuration.locales
+        // Get the list of locales.
+        val localeList = resources.configuration.locales
 
-            // Initialize a string builder to extract the locales from the list.
-            val localesStringBuilder = StringBuilder()
+        // Initialize a string builder to extract the locales from the list.
+        val localesStringBuilder = StringBuilder()
 
-            // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
-            var q = 10
+        // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
+        var q = 10
 
-            // Populate the string builder with the contents of the locales list.
-            for (i in 0 until localeList.size()) {
-                // Append a comma if there is already an item in the string builder.
-                if (i > 0) {
-                    localesStringBuilder.append(",")
-                }
-
-                // Get the locale from the list.
-                val locale = localeList[i]
-
-                // Add the locale to the string.  `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
-                localesStringBuilder.append(locale.language)
-                localesStringBuilder.append("-")
-                localesStringBuilder.append(locale.country)
-
-                // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
-                if (q < 10) {
-                    localesStringBuilder.append(";q=0.")
-                    localesStringBuilder.append(q)
-                }
+        // Populate the string builder with the contents of the locales list.
+        for (i in 0 until localeList.size()) {
+            // Append a comma if there is already an item in the string builder.
+            if (i > 0) {
+                localesStringBuilder.append(",")
+            }
 
-                // Decrement `q` if it is greater than 1.
-                if (q > 1) {
-                    q--
-                }
+            // Get the locale from the list.
+            val locale = localeList[i]
 
-                // Add a second entry for the language only portion of the locale.
-                localesStringBuilder.append(",")
-                localesStringBuilder.append(locale.language)
+            // Add the locale to the string.  `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
+            localesStringBuilder.append(locale.language)
+            localesStringBuilder.append("-")
+            localesStringBuilder.append(locale.country)
 
-                // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
+            // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
+            if (q < 10) {
                 localesStringBuilder.append(";q=0.")
                 localesStringBuilder.append(q)
+            }
 
-                // Decrement `q` if it is greater than 1.
-                if (q > 1) {
-                    q--
-                }
+            // Decrement `q` if it is greater than 1.
+            if (q > 1) {
+                q--
             }
 
-            // Store the populated string builder in the locale string.
-            localesStringBuilder.toString()
-        } else {  // SDK < 24 only has a primary locale.
-            // Store the locale in the locale string.
-            Locale.getDefault().toString()
+            // Add a second entry for the language only portion of the locale.
+            localesStringBuilder.append(",")
+            localesStringBuilder.append(locale.language)
+
+            // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
+            localesStringBuilder.append(";q=0.")
+            localesStringBuilder.append(q)
+
+            // Decrement `q` if it is greater than 1.
+            if (q > 1) {
+                q--
+            }
         }
 
         // Instantiate the proxy helper.
@@ -253,14 +241,17 @@ class ViewSourceActivity: AppCompatActivity() {
         // Set the progress bar to be indeterminate.
         progressBar.isIndeterminate = true
 
+        // Update the layout.
+        updateLayout(currentUrl)
+
         // Instantiate the WebView source factory.
-        val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl!!, userAgent!!, localeString, proxy, MainWebViewActivity.executorService)
+        val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
 
         // Instantiate the WebView source view model class.
-        val webViewSource = ViewModelProvider(this, webViewSourceFactory).get(WebViewSource::class.java)
+        webViewSource = ViewModelProvider(this, webViewSourceFactory)[WebViewSource::class.java]
 
         // Create a source observer.
-        webViewSource.observeSource().observe(this, { sourceStringArray: Array<SpannableStringBuilder> ->
+        webViewSource.observeSource().observe(this) { sourceStringArray: Array<SpannableStringBuilder> ->
             // Populate the text views.  This can take a long time, and freezes the user interface, if the response body is particularly large.
             requestHeadersTextView.text = sourceStringArray[0]
             responseMessageTextView.text = sourceStringArray[1]
@@ -273,15 +264,24 @@ class ViewSourceActivity: AppCompatActivity() {
 
             //Stop the swipe to refresh indicator if it is running
             swipeRefreshLayout.isRefreshing = false
-        })
+        }
 
         // Create an error observer.
-        webViewSource.observeErrors().observe(this, { errorString: String ->
+        webViewSource.observeErrors().observe(this) { errorString: String ->
             // Display an error snackbar if the string is not `""`.
             if (errorString != "") {
-                Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
+                if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
+                    // Instantiate the untrusted SSL certificate dialog.
+                    val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
+
+                    // Show the untrusted SSL certificate dialog.
+                    untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
+                } else {
+                    // Display a snackbar with the error message.
+                    Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
+                }
             }
-        })
+        }
 
         // Implement swipe to refresh.
         swipeRefreshLayout.setOnRefreshListener {
@@ -294,8 +294,11 @@ class ViewSourceActivity: AppCompatActivity() {
             // Get the URL.
             val urlString = urlEditText.text.toString()
 
+            // Update the layout.
+            updateLayout(urlString)
+
             // Get the updated source.
-            webViewSource.updateSource(urlString)
+            webViewSource.updateSource(urlString, false)
         }
 
         // Set the go button on the keyboard to request new source data.
@@ -317,8 +320,11 @@ class ViewSourceActivity: AppCompatActivity() {
                 // Get the URL.
                 val urlString = urlEditText.text.toString()
 
+                // Update the layout.
+                updateLayout(urlString)
+
                 // Get the updated source.
-                webViewSource.updateSource(urlString)
+                webViewSource.updateSource(urlString, false)
 
                 // Consume the key press.
                 return@setOnKeyListener true
@@ -330,7 +336,7 @@ class ViewSourceActivity: AppCompatActivity() {
     }
 
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
-        // Inflate the menu.  This adds items to the action bar if it is present.
+        // Inflate the menu.
         menuInflater.inflate(R.menu.view_source_options_menu, menu)
 
         // Display the menu.
@@ -338,7 +344,7 @@ class ViewSourceActivity: AppCompatActivity() {
     }
 
     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
-        // Get a handle for the about alert dialog.
+        // Instantiate the about dialog fragment.
         val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
 
         // Show the about alert dialog.
@@ -354,61 +360,32 @@ class ViewSourceActivity: AppCompatActivity() {
         NavUtils.navigateUpFromSameTask(this)
     }
 
-    private fun highlightUrlText() {
-        // Get a handle for the URL edit text.
-        val urlEditText = findViewById<EditText>(R.id.url_edittext)
-
-        // Get the URL string.
-        val urlString = urlEditText.text.toString()
-
-        // Highlight the URL according to the protocol.
-        if (urlString.startsWith("file://")) {  // This is a file URL.
-            // De-emphasize only the protocol.
-            urlEditText.text.setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-        } else if (urlString.startsWith("content://")) {
-            // De-emphasize only the protocol.
-            urlEditText.text.setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-        } else {  // This is a web URL.
-            // Get the index of the `/` immediately after the domain name.
-            val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
-
-            // Get the base URL.
-            val baseUrl = if (endOfDomainName > 0) {  // There is at least one character after the base URL.
-                // Get the base URL.
-                urlString.substring(0, endOfDomainName)
-            } else {  // There are no characters after the base URL.
-                // Set the base URL to be the entire URL string.
-                urlString
-            }
-
-            // Get the index of the last `.` in the domain.
-            val lastDotIndex = baseUrl.lastIndexOf(".")
-
-            // Get the index of the penultimate `.` in the domain.
-            val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
-
-            // Markup the beginning of the URL.
-            if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
-                urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-
-                // De-emphasize subdomains.
-                if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
-                    urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-                }
-            } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
-                if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
-                    // De-emphasize the protocol and the additional subdomains.
-                    urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-                } else {  // There is only one subdomain in the domain name.
-                    // De-emphasize only the protocol.
-                    urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-                }
-            }
+    override fun loadAnyway() {
+        // Load the URL anyway.
+        webViewSource.updateSource(urlEditText.text.toString(), true)
+    }
 
-            // De-emphasize the text after the domain name.
-            if (endOfDomainName > 0) {
-                urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-            }
+    private fun updateLayout(urlString: String) {
+        if (urlString.startsWith("content://")) {  // This is a content URL.
+            // Hide the unused text views.
+            requestHeadersTitleTextView.visibility = View.GONE
+            requestHeadersTextView.visibility = View.GONE
+            responseMessageTitleTextView.visibility = View.GONE
+            responseMessageTextView.visibility = View.GONE
+
+            // Change the text of the remaining title text views.
+            responseHeadersTitleTextView.setText(R.string.content_metadata)
+            responseBodyTitleTextView.setText(R.string.content_data)
+        } else {  // This is not a content URL.
+            // Show the views.
+            requestHeadersTitleTextView.visibility = View.VISIBLE
+            requestHeadersTextView.visibility = View.VISIBLE
+            responseMessageTitleTextView.visibility = View.VISIBLE
+            responseMessageTextView.visibility = View.VISIBLE
+
+            // Restore the text of the other title text views.
+            responseHeadersTitleTextView.setText(R.string.response_headers)
+            responseBodyTitleTextView.setText(R.string.response_body)
         }
     }
-}
\ No newline at end of file
+}