2 * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.activities
22 import android.os.Bundle
23 import android.text.SpannableStringBuilder
24 import android.text.style.ForegroundColorSpan
25 import android.util.TypedValue
26 import android.view.KeyEvent
27 import android.view.Menu
28 import android.view.MenuItem
29 import android.view.View
30 import android.view.View.OnFocusChangeListener
31 import android.view.WindowManager
32 import android.view.inputmethod.InputMethodManager
33 import android.widget.EditText
34 import android.widget.ProgressBar
35 import android.widget.TextView
37 import androidx.appcompat.app.ActionBar
38 import androidx.appcompat.app.AppCompatActivity
39 import androidx.appcompat.widget.Toolbar
40 import androidx.core.app.NavUtils
41 import androidx.fragment.app.DialogFragment
42 import androidx.lifecycle.ViewModelProvider
43 import androidx.preference.PreferenceManager
44 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
46 import com.google.android.material.snackbar.Snackbar
48 import com.stoutner.privacybrowser.R
49 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog
50 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
51 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
52 import com.stoutner.privacybrowser.helpers.ProxyHelper
53 import com.stoutner.privacybrowser.helpers.UrlHelper
54 import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory
55 import com.stoutner.privacybrowser.viewmodels.WebViewSource
57 // Define the public constants.
58 const val USER_AGENT = "user_agent"
60 class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener {
61 // Declare the class variables.
62 private lateinit var initialGrayColorSpan: ForegroundColorSpan
63 private lateinit var finalGrayColorSpan: ForegroundColorSpan
64 private lateinit var redColorSpan: ForegroundColorSpan
65 private lateinit var webViewSource: WebViewSource
67 // Declare the class views.
68 private lateinit var urlEditText: EditText
69 private lateinit var requestHeadersTitleTextView: TextView
70 private lateinit var requestHeadersTextView: TextView
71 private lateinit var responseMessageTitleTextView: TextView
72 private lateinit var responseMessageTextView: TextView
73 private lateinit var responseHeadersTitleTextView: TextView
74 private lateinit var responseBodyTitleTextView: TextView
76 override fun onCreate(savedInstanceState: Bundle?) {
77 // Get a handle for the shared preferences.
78 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
80 // Get the preferences.
81 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
82 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
84 // Disable screenshots if not allowed.
85 if (!allowScreenshots) {
86 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
89 // Run the default commands.
90 super.onCreate(savedInstanceState)
92 // Get the launching intent
95 // Get the information from the intent.
96 val currentUrl = intent.getStringExtra(CURRENT_URL)!!
97 val userAgent = intent.getStringExtra(USER_AGENT)!!
99 // Set the content view.
101 setContentView(R.layout.view_source_bottom_appbar)
103 setContentView(R.layout.view_source_top_appbar)
106 // Get a handle for the toolbar.
107 val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
109 // Set the support action bar.
110 setSupportActionBar(toolbar)
112 // Get a handle for the action bar.
113 val actionBar = supportActionBar!!
115 // Add the custom layout to the action bar.
116 actionBar.setCustomView(R.layout.view_source_appbar_custom_view)
118 // Instruct the action bar to display a custom layout.
119 actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
121 // Get handles for the views.
122 urlEditText = findViewById(R.id.url_edittext)
123 val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
124 val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
125 requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
126 requestHeadersTextView = findViewById(R.id.request_headers_textview)
127 responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
128 responseMessageTextView = findViewById(R.id.response_message_textview)
129 responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
130 val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
131 responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
132 val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
134 // Populate the URL text box.
135 urlEditText.setText(currentUrl)
137 // Initialize the gray foreground color spans for highlighting the URLs.
138 initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
139 finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
140 redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
142 // Apply text highlighting to the URL.
143 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
145 // Get a handle for the input method manager, which is used to hide the keyboard.
146 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
148 // Remove the formatting from the URL when the user is editing the text.
149 urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
150 if (hasFocus) { // The user is editing the URL text box.
151 // Remove the highlighting.
152 urlEditText.text.removeSpan(redColorSpan)
153 urlEditText.text.removeSpan(initialGrayColorSpan)
154 urlEditText.text.removeSpan(finalGrayColorSpan)
155 } else { // The user has stopped editing the URL text box.
156 // Hide the soft keyboard.
157 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
159 // Move to the beginning of the string.
160 urlEditText.setSelection(0)
162 // Reapply the highlighting.
163 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
167 // Set the refresh color scheme according to the theme.
168 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
170 // Initialize a color background typed value.
171 val colorBackgroundTypedValue = TypedValue()
173 // Get the color background from the theme.
174 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
176 // Get the color background int from the typed value.
177 val colorBackgroundInt = colorBackgroundTypedValue.data
179 // Set the swipe refresh background color.
180 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
182 // Get the list of locales.
183 val localeList = resources.configuration.locales
185 // Initialize a string builder to extract the locales from the list.
186 val localesStringBuilder = StringBuilder()
188 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
191 // Populate the string builder with the contents of the locales list.
192 for (i in 0 until localeList.size()) {
193 // Append a comma if there is already an item in the string builder.
195 localesStringBuilder.append(",")
198 // Get the locale from the list.
199 val locale = localeList[i]
201 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
202 localesStringBuilder.append(locale.language)
203 localesStringBuilder.append("-")
204 localesStringBuilder.append(locale.country)
206 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
208 localesStringBuilder.append(";q=0.")
209 localesStringBuilder.append(q)
212 // Decrement `q` if it is greater than 1.
217 // Add a second entry for the language only portion of the locale.
218 localesStringBuilder.append(",")
219 localesStringBuilder.append(locale.language)
221 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
222 localesStringBuilder.append(";q=0.")
223 localesStringBuilder.append(q)
225 // Decrement `q` if it is greater than 1.
231 // Instantiate the proxy helper.
232 val proxyHelper = ProxyHelper()
234 // Get the current proxy.
235 val proxy = proxyHelper.getCurrentProxy(this)
237 // Make the progress bar visible.
238 progressBar.visibility = View.VISIBLE
240 // Set the progress bar to be indeterminate.
241 progressBar.isIndeterminate = true
243 // Update the layout.
244 updateLayout(currentUrl)
246 // Instantiate the WebView source factory.
247 val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
249 // Instantiate the WebView source view model class.
250 webViewSource = ViewModelProvider(this, webViewSourceFactory)[WebViewSource::class.java]
252 // Create a source observer.
253 webViewSource.observeSource().observe(this) { sourceStringArray: Array<SpannableStringBuilder> ->
254 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
255 requestHeadersTextView.text = sourceStringArray[0]
256 responseMessageTextView.text = sourceStringArray[1]
257 responseHeadersTextView.text = sourceStringArray[2]
258 responseBodyTextView.text = sourceStringArray[3]
260 // Hide the progress bar.
261 progressBar.isIndeterminate = false
262 progressBar.visibility = View.GONE
264 //Stop the swipe to refresh indicator if it is running
265 swipeRefreshLayout.isRefreshing = false
268 // Create an error observer.
269 webViewSource.observeErrors().observe(this) { errorString: String ->
270 // Display an error snackbar if the string is not `""`.
271 if (errorString != "") {
272 if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
273 // Instantiate the untrusted SSL certificate dialog.
274 val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
276 // Show the untrusted SSL certificate dialog.
277 untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
279 // Display a snackbar with the error message.
280 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
285 // Implement swipe to refresh.
286 swipeRefreshLayout.setOnRefreshListener {
287 // Make the progress bar visible.
288 progressBar.visibility = View.VISIBLE
290 // Set the progress bar to be indeterminate.
291 progressBar.isIndeterminate = true
294 val urlString = urlEditText.text.toString()
296 // Update the layout.
297 updateLayout(urlString)
299 // Get the updated source.
300 webViewSource.updateSource(urlString, false)
303 // Set the go button on the keyboard to request new source data.
304 urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
305 // Request new source data if the enter key was pressed.
306 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
307 // Hide the soft keyboard.
308 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
310 // Remove the focus from the URL box.
311 urlEditText.clearFocus()
313 // Make the progress bar visible.
314 progressBar.visibility = View.VISIBLE
316 // Set the progress bar to be indeterminate.
317 progressBar.isIndeterminate = true
320 val urlString = urlEditText.text.toString()
322 // Update the layout.
323 updateLayout(urlString)
325 // Get the updated source.
326 webViewSource.updateSource(urlString, false)
328 // Consume the key press.
329 return@setOnKeyListener true
331 // Do not consume the key press.
332 return@setOnKeyListener false
337 override fun onCreateOptionsMenu(menu: Menu): Boolean {
339 menuInflater.inflate(R.menu.view_source_options_menu, menu)
345 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
346 // Instantiate the about dialog fragment.
347 val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
349 // Show the about alert dialog.
350 aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
352 // Consume the event.
356 // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
357 fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
359 NavUtils.navigateUpFromSameTask(this)
362 override fun loadAnyway() {
363 // Load the URL anyway.
364 webViewSource.updateSource(urlEditText.text.toString(), true)
367 private fun updateLayout(urlString: String) {
368 if (urlString.startsWith("content://")) { // This is a content URL.
369 // Hide the unused text views.
370 requestHeadersTitleTextView.visibility = View.GONE
371 requestHeadersTextView.visibility = View.GONE
372 responseMessageTitleTextView.visibility = View.GONE
373 responseMessageTextView.visibility = View.GONE
375 // Change the text of the remaining title text views.
376 responseHeadersTitleTextView.setText(R.string.content_metadata)
377 responseBodyTitleTextView.setText(R.string.content_data)
378 } else { // This is not a content URL.
380 requestHeadersTitleTextView.visibility = View.VISIBLE
381 requestHeadersTextView.visibility = View.VISIBLE
382 responseMessageTitleTextView.visibility = View.VISIBLE
383 responseMessageTextView.visibility = View.VISIBLE
385 // Restore the text of the other title text views.
386 responseHeadersTitleTextView.setText(R.string.response_headers)
387 responseBodyTitleTextView.setText(R.string.response_body)