/*
- * Copyright © 2017-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2017-2022 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
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)
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)
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)
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()
// Set the swipe refresh background color.
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
- // Get the Do Not Track status.
- val doNotTrack = sharedPreferences.getBoolean(getString(R.string.do_not_track_key), false)
-
// Populate the locale string.
val localeString = if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
// Get the list of locales.
// 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!!, doNotTrack, localeString, proxy, MainWebViewActivity.executorService)
+ val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localeString, 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]
//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 {
// 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.
// 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
}
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.
}
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.
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)
-
- // Create a base URL string.
- val baseUrl: String
-
- // Get the base URL.
- 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
+}