2 * Copyright © 2017-2021 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser 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 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. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.activities
22 import android.content.res.Configuration
23 import android.os.Build
24 import android.os.Bundle
25 import android.text.SpannableStringBuilder
26 import android.text.Spanned
27 import android.text.style.ForegroundColorSpan
28 import android.util.TypedValue
29 import android.view.KeyEvent
30 import android.view.Menu
31 import android.view.MenuItem
32 import android.view.View
33 import android.view.View.OnFocusChangeListener
34 import android.view.WindowManager
35 import android.view.inputmethod.InputMethodManager
36 import android.widget.EditText
37 import android.widget.ProgressBar
38 import android.widget.TextView
40 import androidx.appcompat.app.ActionBar
41 import androidx.appcompat.app.AppCompatActivity
42 import androidx.appcompat.widget.Toolbar
43 import androidx.core.app.NavUtils
44 import androidx.fragment.app.DialogFragment
45 import androidx.lifecycle.ViewModelProvider
46 import androidx.preference.PreferenceManager
47 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
49 import com.google.android.material.snackbar.Snackbar
51 import com.stoutner.privacybrowser.R
52 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog
53 import com.stoutner.privacybrowser.helpers.ProxyHelper
54 import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory
55 import com.stoutner.privacybrowser.viewmodels.WebViewSource
57 import java.util.Locale
59 // Declare the public constants.
60 const val CURRENT_URL = "current_url"
61 const val USER_AGENT = "user_agent"
63 class ViewSourceActivity: AppCompatActivity() {
64 // Declare the class variables.
65 private lateinit var initialGrayColorSpan: ForegroundColorSpan
66 private lateinit var finalGrayColorSpan: ForegroundColorSpan
67 private lateinit var redColorSpan: ForegroundColorSpan
69 // Declare the class views.
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 screenshot preference.
82 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
84 // Disable screenshots if not allowed.
85 if (!allowScreenshots) {
86 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
90 setTheme(R.style.PrivacyBrowser)
92 // Run the default commands.
93 super.onCreate(savedInstanceState)
95 // Get the launching intent
98 // Get the information from the intent.
99 val currentUrl = intent.getStringExtra(CURRENT_URL)!!
100 val userAgent = intent.getStringExtra(USER_AGENT)!!
102 // Set the content view.
103 setContentView(R.layout.view_source_coordinatorlayout)
105 // Get a handle for the toolbar.
106 val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
108 // Set the support action bar.
109 setSupportActionBar(toolbar)
111 // Get a handle for the action bar.
112 val actionBar = supportActionBar!!
114 // Add the custom layout to the action bar.
115 actionBar.setCustomView(R.layout.view_source_app_bar)
117 // Instruct the action bar to display a custom layout.
118 actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
120 // Get handles for the views.
121 val urlEditText = findViewById<EditText>(R.id.url_edittext)
122 val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
123 val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
124 requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
125 requestHeadersTextView = findViewById(R.id.request_headers_textview)
126 responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
127 responseMessageTextView = findViewById(R.id.response_message_textview)
128 responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textivew)
129 val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
130 responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
131 val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
133 // Populate the URL text box.
134 urlEditText.setText(currentUrl)
136 // Initialize the gray foreground color spans for highlighting the URLs. The deprecated `getColor()` must be used until the minimum API >= 23.
137 @Suppress("DEPRECATION")
138 initialGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
139 @Suppress("DEPRECATION")
140 finalGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
142 // Get the current theme status.
143 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
145 // Set the red color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
146 redColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
147 @Suppress("DEPRECATION")
148 ForegroundColorSpan(resources.getColor(R.color.red_a700))
150 @Suppress("DEPRECATION")
151 ForegroundColorSpan(resources.getColor(R.color.red_900))
154 // Apply text highlighting to the URL.
157 // Get a handle for the input method manager, which is used to hide the keyboard.
158 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
160 // Remove the formatting from the URL when the user is editing the text.
161 urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
162 if (hasFocus) { // The user is editing the URL text box.
163 // Remove the highlighting.
164 urlEditText.text.removeSpan(redColorSpan)
165 urlEditText.text.removeSpan(initialGrayColorSpan)
166 urlEditText.text.removeSpan(finalGrayColorSpan)
167 } else { // The user has stopped editing the URL text box.
168 // Hide the soft keyboard.
169 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
171 // Move to the beginning of the string.
172 urlEditText.setSelection(0)
174 // Reapply the highlighting.
179 // Set the refresh color scheme according to the theme.
180 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
181 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700)
183 swipeRefreshLayout.setColorSchemeResources(R.color.violet_500)
186 // Initialize a color background typed value.
187 val colorBackgroundTypedValue = TypedValue()
189 // Get the color background from the theme.
190 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
192 // Get the color background int from the typed value.
193 val colorBackgroundInt = colorBackgroundTypedValue.data
195 // Set the swipe refresh background color.
196 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
198 // Populate the locale string.
199 val localeString = if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
200 // Get the list of locales.
201 val localeList = resources.configuration.locales
203 // Initialize a string builder to extract the locales from the list.
204 val localesStringBuilder = StringBuilder()
206 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
209 // Populate the string builder with the contents of the locales list.
210 for (i in 0 until localeList.size()) {
211 // Append a comma if there is already an item in the string builder.
213 localesStringBuilder.append(",")
216 // Get the locale from the list.
217 val locale = localeList[i]
219 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
220 localesStringBuilder.append(locale.language)
221 localesStringBuilder.append("-")
222 localesStringBuilder.append(locale.country)
224 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
226 localesStringBuilder.append(";q=0.")
227 localesStringBuilder.append(q)
230 // Decrement `q` if it is greater than 1.
235 // Add a second entry for the language only portion of the locale.
236 localesStringBuilder.append(",")
237 localesStringBuilder.append(locale.language)
239 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
240 localesStringBuilder.append(";q=0.")
241 localesStringBuilder.append(q)
243 // Decrement `q` if it is greater than 1.
249 // Store the populated string builder in the locale string.
250 localesStringBuilder.toString()
251 } else { // SDK < 24 only has a primary locale.
252 // Store the locale in the locale string.
253 Locale.getDefault().toString()
256 // Instantiate the proxy helper.
257 val proxyHelper = ProxyHelper()
259 // Get the current proxy.
260 val proxy = proxyHelper.getCurrentProxy(this)
262 // Make the progress bar visible.
263 progressBar.visibility = View.VISIBLE
265 // Set the progress bar to be indeterminate.
266 progressBar.isIndeterminate = true
268 // Update the layout.
269 updateLayout(currentUrl)
271 // Instantiate the WebView source factory.
272 val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localeString, proxy, contentResolver, MainWebViewActivity.executorService)
274 // Instantiate the WebView source view model class.
275 val webViewSource = ViewModelProvider(this, webViewSourceFactory).get(WebViewSource::class.java)
277 // Create a source observer.
278 webViewSource.observeSource().observe(this, { sourceStringArray: Array<SpannableStringBuilder> ->
279 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
280 requestHeadersTextView.text = sourceStringArray[0]
281 responseMessageTextView.text = sourceStringArray[1]
282 responseHeadersTextView.text = sourceStringArray[2]
283 responseBodyTextView.text = sourceStringArray[3]
285 // Hide the progress bar.
286 progressBar.isIndeterminate = false
287 progressBar.visibility = View.GONE
289 //Stop the swipe to refresh indicator if it is running
290 swipeRefreshLayout.isRefreshing = false
293 // Create an error observer.
294 webViewSource.observeErrors().observe(this, { errorString: String ->
295 // Display an error snackbar if the string is not `""`.
296 if (errorString != "") {
297 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
301 // Implement swipe to refresh.
302 swipeRefreshLayout.setOnRefreshListener {
303 // Make the progress bar visible.
304 progressBar.visibility = View.VISIBLE
306 // Set the progress bar to be indeterminate.
307 progressBar.isIndeterminate = true
310 val urlString = urlEditText.text.toString()
312 // Update the layout.
313 updateLayout(urlString)
315 // Get the updated source.
316 webViewSource.updateSource(urlString)
319 // Set the go button on the keyboard to request new source data.
320 urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
321 // Request new source data if the enter key was pressed.
322 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
323 // Hide the soft keyboard.
324 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
326 // Remove the focus from the URL box.
327 urlEditText.clearFocus()
329 // Make the progress bar visible.
330 progressBar.visibility = View.VISIBLE
332 // Set the progress bar to be indeterminate.
333 progressBar.isIndeterminate = true
336 val urlString = urlEditText.text.toString()
338 // Update the layout.
339 updateLayout(urlString)
341 // Get the updated source.
342 webViewSource.updateSource(urlString)
344 // Consume the key press.
345 return@setOnKeyListener true
347 // Do not consume the key press.
348 return@setOnKeyListener false
353 override fun onCreateOptionsMenu(menu: Menu): Boolean {
354 // Inflate the menu. This adds items to the action bar if it is present.
355 menuInflater.inflate(R.menu.view_source_options_menu, menu)
361 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
362 // Get a handle for the about alert dialog.
363 val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
365 // Show the about alert dialog.
366 aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
368 // Consume the event.
372 // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
373 fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
375 NavUtils.navigateUpFromSameTask(this)
378 private fun highlightUrlText() {
379 // Get a handle for the URL edit text.
380 val urlEditText = findViewById<EditText>(R.id.url_edittext)
382 // Get the URL string.
383 val urlString = urlEditText.text.toString()
385 // Highlight the URL according to the protocol.
386 if (urlString.startsWith("file://")) { // This is a file URL.
387 // De-emphasize only the protocol.
388 urlEditText.text.setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
389 } else if (urlString.startsWith("content://")) {
390 // De-emphasize only the protocol.
391 urlEditText.text.setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
392 } else { // This is a web URL.
393 // Get the index of the `/` immediately after the domain name.
394 val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
397 val baseUrl = if (endOfDomainName > 0) { // There is at least one character after the base URL.
399 urlString.substring(0, endOfDomainName)
400 } else { // There are no characters after the base URL.
401 // Set the base URL to be the entire URL string.
405 // Get the index of the last `.` in the domain.
406 val lastDotIndex = baseUrl.lastIndexOf(".")
408 // Get the index of the penultimate `.` in the domain.
409 val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
411 // Markup the beginning of the URL.
412 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
413 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
415 // De-emphasize subdomains.
416 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
417 urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
419 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
420 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
421 // De-emphasize the protocol and the additional subdomains.
422 urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
423 } else { // There is only one subdomain in the domain name.
424 // De-emphasize only the protocol.
425 urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
429 // De-emphasize the text after the domain name.
430 if (endOfDomainName > 0) {
431 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
436 private fun updateLayout(urlString: String) {
437 if (urlString.startsWith("content://")) { // This is a content URL.
438 // Hide the unused text views.
439 requestHeadersTitleTextView.visibility = View.GONE
440 requestHeadersTextView.visibility = View.GONE
441 responseMessageTitleTextView.visibility = View.GONE
442 responseMessageTextView.visibility = View.GONE
444 // Change the text of the remaining title text views.
445 responseHeadersTitleTextView.setText(R.string.content_metadata)
446 responseBodyTitleTextView.setText(R.string.content_data)
447 } else { // This is not a content URL.
449 requestHeadersTitleTextView.visibility = View.VISIBLE
450 requestHeadersTextView.visibility = View.VISIBLE
451 responseMessageTitleTextView.visibility = View.VISIBLE
452 responseMessageTextView.visibility = View.VISIBLE
454 // Restore the text of the other title text views.
455 responseHeadersTitleTextView.setText(R.string.response_headers)
456 responseBodyTitleTextView.setText(R.string.response_body)