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 CURRENT_URL = "current_url"
59 const val USER_AGENT = "user_agent"
61 class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener {
62 // Declare the class variables.
63 private lateinit var initialGrayColorSpan: ForegroundColorSpan
64 private lateinit var finalGrayColorSpan: ForegroundColorSpan
65 private lateinit var redColorSpan: ForegroundColorSpan
66 private lateinit var webViewSource: WebViewSource
68 // Declare the class views.
69 private lateinit var urlEditText: EditText
70 private lateinit var requestHeadersTitleTextView: TextView
71 private lateinit var requestHeadersTextView: TextView
72 private lateinit var responseMessageTitleTextView: TextView
73 private lateinit var responseMessageTextView: TextView
74 private lateinit var responseHeadersTitleTextView: TextView
75 private lateinit var responseBodyTitleTextView: TextView
77 override fun onCreate(savedInstanceState: Bundle?) {
78 // Get a handle for the shared preferences.
79 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
81 // Get the preferences.
82 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
83 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
85 // Disable screenshots if not allowed.
86 if (!allowScreenshots) {
87 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
90 // Run the default commands.
91 super.onCreate(savedInstanceState)
93 // Get the launching intent
96 // Get the information from the intent.
97 val currentUrl = intent.getStringExtra(CURRENT_URL)!!
98 val userAgent = intent.getStringExtra(USER_AGENT)!!
100 // Set the content view.
102 setContentView(R.layout.view_source_bottom_appbar)
104 setContentView(R.layout.view_source_top_appbar)
107 // Get a handle for the toolbar.
108 val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
110 // Set the support action bar.
111 setSupportActionBar(toolbar)
113 // Get a handle for the action bar.
114 val actionBar = supportActionBar!!
116 // Add the custom layout to the action bar.
117 actionBar.setCustomView(R.layout.view_source_appbar_custom_view)
119 // Instruct the action bar to display a custom layout.
120 actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
122 // Get handles for the views.
123 urlEditText = findViewById(R.id.url_edittext)
124 val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
125 val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
126 requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
127 requestHeadersTextView = findViewById(R.id.request_headers_textview)
128 responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
129 responseMessageTextView = findViewById(R.id.response_message_textview)
130 responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
131 val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
132 responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
133 val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
135 // Populate the URL text box.
136 urlEditText.setText(currentUrl)
138 // Initialize the gray foreground color spans for highlighting the URLs.
139 initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
140 finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
141 redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
143 // Apply text highlighting to the URL.
144 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
146 // Get a handle for the input method manager, which is used to hide the keyboard.
147 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
149 // Remove the formatting from the URL when the user is editing the text.
150 urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
151 if (hasFocus) { // The user is editing the URL text box.
152 // Remove the highlighting.
153 urlEditText.text.removeSpan(redColorSpan)
154 urlEditText.text.removeSpan(initialGrayColorSpan)
155 urlEditText.text.removeSpan(finalGrayColorSpan)
156 } else { // The user has stopped editing the URL text box.
157 // Hide the soft keyboard.
158 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
160 // Move to the beginning of the string.
161 urlEditText.setSelection(0)
163 // Reapply the highlighting.
164 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
168 // Set the refresh color scheme according to the theme.
169 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
171 // Initialize a color background typed value.
172 val colorBackgroundTypedValue = TypedValue()
174 // Get the color background from the theme.
175 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
177 // Get the color background int from the typed value.
178 val colorBackgroundInt = colorBackgroundTypedValue.data
180 // Set the swipe refresh background color.
181 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
183 // Get the list of locales.
184 val localeList = resources.configuration.locales
186 // Initialize a string builder to extract the locales from the list.
187 val localesStringBuilder = StringBuilder()
189 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
192 // Populate the string builder with the contents of the locales list.
193 for (i in 0 until localeList.size()) {
194 // Append a comma if there is already an item in the string builder.
196 localesStringBuilder.append(",")
199 // Get the locale from the list.
200 val locale = localeList[i]
202 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
203 localesStringBuilder.append(locale.language)
204 localesStringBuilder.append("-")
205 localesStringBuilder.append(locale.country)
207 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
209 localesStringBuilder.append(";q=0.")
210 localesStringBuilder.append(q)
213 // Decrement `q` if it is greater than 1.
218 // Add a second entry for the language only portion of the locale.
219 localesStringBuilder.append(",")
220 localesStringBuilder.append(locale.language)
222 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
223 localesStringBuilder.append(";q=0.")
224 localesStringBuilder.append(q)
226 // Decrement `q` if it is greater than 1.
232 // Instantiate the proxy helper.
233 val proxyHelper = ProxyHelper()
235 // Get the current proxy.
236 val proxy = proxyHelper.getCurrentProxy(this)
238 // Make the progress bar visible.
239 progressBar.visibility = View.VISIBLE
241 // Set the progress bar to be indeterminate.
242 progressBar.isIndeterminate = true
244 // Update the layout.
245 updateLayout(currentUrl)
247 // Instantiate the WebView source factory.
248 val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
250 // Instantiate the WebView source view model class.
251 webViewSource = ViewModelProvider(this, webViewSourceFactory)[WebViewSource::class.java]
253 // Create a source observer.
254 webViewSource.observeSource().observe(this) { sourceStringArray: Array<SpannableStringBuilder> ->
255 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
256 requestHeadersTextView.text = sourceStringArray[0]
257 responseMessageTextView.text = sourceStringArray[1]
258 responseHeadersTextView.text = sourceStringArray[2]
259 responseBodyTextView.text = sourceStringArray[3]
261 // Hide the progress bar.
262 progressBar.isIndeterminate = false
263 progressBar.visibility = View.GONE
265 //Stop the swipe to refresh indicator if it is running
266 swipeRefreshLayout.isRefreshing = false
269 // Create an error observer.
270 webViewSource.observeErrors().observe(this) { errorString: String ->
271 // Display an error snackbar if the string is not `""`.
272 if (errorString != "") {
273 if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
274 // Instantiate the untrusted SSL certificate dialog.
275 val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
277 // Show the untrusted SSL certificate dialog.
278 untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
280 // Display a snackbar with the error message.
281 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
286 // Implement swipe to refresh.
287 swipeRefreshLayout.setOnRefreshListener {
288 // Make the progress bar visible.
289 progressBar.visibility = View.VISIBLE
291 // Set the progress bar to be indeterminate.
292 progressBar.isIndeterminate = true
295 val urlString = urlEditText.text.toString()
297 // Update the layout.
298 updateLayout(urlString)
300 // Get the updated source.
301 webViewSource.updateSource(urlString, false)
304 // Set the go button on the keyboard to request new source data.
305 urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
306 // Request new source data if the enter key was pressed.
307 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
308 // Hide the soft keyboard.
309 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
311 // Remove the focus from the URL box.
312 urlEditText.clearFocus()
314 // Make the progress bar visible.
315 progressBar.visibility = View.VISIBLE
317 // Set the progress bar to be indeterminate.
318 progressBar.isIndeterminate = true
321 val urlString = urlEditText.text.toString()
323 // Update the layout.
324 updateLayout(urlString)
326 // Get the updated source.
327 webViewSource.updateSource(urlString, false)
329 // Consume the key press.
330 return@setOnKeyListener true
332 // Do not consume the key press.
333 return@setOnKeyListener false
338 override fun onCreateOptionsMenu(menu: Menu): Boolean {
340 menuInflater.inflate(R.menu.view_source_options_menu, menu)
346 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
347 // Instantiate the about dialog fragment.
348 val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
350 // Show the about alert dialog.
351 aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
353 // Consume the event.
357 // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
358 fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
360 NavUtils.navigateUpFromSameTask(this)
363 override fun loadAnyway() {
364 // Load the URL anyway.
365 webViewSource.updateSource(urlEditText.text.toString(), true)
368 private fun updateLayout(urlString: String) {
369 if (urlString.startsWith("content://")) { // This is a content URL.
370 // Hide the unused text views.
371 requestHeadersTitleTextView.visibility = View.GONE
372 requestHeadersTextView.visibility = View.GONE
373 responseMessageTitleTextView.visibility = View.GONE
374 responseMessageTextView.visibility = View.GONE
376 // Change the text of the remaining title text views.
377 responseHeadersTitleTextView.setText(R.string.content_metadata)
378 responseBodyTitleTextView.setText(R.string.content_data)
379 } else { // This is not a content URL.
381 requestHeadersTitleTextView.visibility = View.VISIBLE
382 requestHeadersTextView.visibility = View.VISIBLE
383 responseMessageTitleTextView.visibility = View.VISIBLE
384 responseMessageTextView.visibility = View.VISIBLE
386 // Restore the text of the other title text views.
387 responseHeadersTitleTextView.setText(R.string.response_headers)
388 responseBodyTitleTextView.setText(R.string.response_body)