2 * Copyright © 2017-2020 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 override fun onCreate(savedInstanceState: Bundle?) {
70 // Get a handle for the shared preferences.
71 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
73 // Get the screenshot preference.
74 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
76 // Disable screenshots if not allowed.
77 if (!allowScreenshots) {
78 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
82 setTheme(R.style.PrivacyBrowser)
84 // Run the default commands.
85 super.onCreate(savedInstanceState)
87 // Get the launching intent
90 // Get the information from the intent.
91 val currentUrl = intent.getStringExtra(CURRENT_URL)
92 val userAgent = intent.getStringExtra(USER_AGENT)
94 // Set the content view.
95 setContentView(R.layout.view_source_coordinatorlayout)
97 // Get a handle for the toolbar.
98 val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
100 // Set the support action bar.
101 setSupportActionBar(toolbar)
103 // Get a handle for the action bar.
104 val actionBar = supportActionBar!!
106 // Add the custom layout to the action bar.
107 actionBar.setCustomView(R.layout.view_source_app_bar)
109 // Instruct the action bar to display a custom layout.
110 actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
112 // Get handles for the views.
113 val urlEditText = findViewById<EditText>(R.id.url_edittext)
114 val requestHeadersTextView = findViewById<TextView>(R.id.request_headers)
115 val responseMessageTextView = findViewById<TextView>(R.id.response_message)
116 val responseHeadersTextView = findViewById<TextView>(R.id.response_headers)
117 val responseBodyTextView = findViewById<TextView>(R.id.response_body)
118 val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
119 val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
121 // Populate the URL text box.
122 urlEditText.setText(currentUrl)
124 // Initialize the gray foreground color spans for highlighting the URLs. The deprecated `getColor()` must be used until the minimum API >= 23.
125 @Suppress("DEPRECATION")
126 initialGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
127 @Suppress("DEPRECATION")
128 finalGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
130 // Get the current theme status.
131 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
133 // Set the red color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
134 redColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
135 @Suppress("DEPRECATION")
136 ForegroundColorSpan(resources.getColor(R.color.red_a700))
138 @Suppress("DEPRECATION")
139 ForegroundColorSpan(resources.getColor(R.color.red_900))
142 // Apply text highlighting to the URL.
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.
167 // Set the refresh color scheme according to the theme.
168 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
169 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700)
171 swipeRefreshLayout.setColorSchemeResources(R.color.violet_500)
174 // Initialize a color background typed value.
175 val colorBackgroundTypedValue = TypedValue()
177 // Get the color background from the theme.
178 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
180 // Get the color background int from the typed value.
181 val colorBackgroundInt = colorBackgroundTypedValue.data
183 // Set the swipe refresh background color.
184 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
186 // Get the Do Not Track status.
187 val doNotTrack = sharedPreferences.getBoolean(getString(R.string.do_not_track_key), false)
189 // Populate the locale string.
190 val localeString = if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
191 // Get the list of locales.
192 val localeList = resources.configuration.locales
194 // Initialize a string builder to extract the locales from the list.
195 val localesStringBuilder = StringBuilder()
197 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
200 // Populate the string builder with the contents of the locales list.
201 for (i in 0 until localeList.size()) {
202 // Append a comma if there is already an item in the string builder.
204 localesStringBuilder.append(",")
207 // Get the locale from the list.
208 val locale = localeList[i]
210 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
211 localesStringBuilder.append(locale.language)
212 localesStringBuilder.append("-")
213 localesStringBuilder.append(locale.country)
215 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
217 localesStringBuilder.append(";q=0.")
218 localesStringBuilder.append(q)
221 // Decrement `q` if it is greater than 1.
226 // Add a second entry for the language only portion of the locale.
227 localesStringBuilder.append(",")
228 localesStringBuilder.append(locale.language)
230 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
231 localesStringBuilder.append(";q=0.")
232 localesStringBuilder.append(q)
234 // Decrement `q` if it is greater than 1.
240 // Store the populated string builder in the locale string.
241 localesStringBuilder.toString()
242 } else { // SDK < 24 only has a primary locale.
243 // Store the locale in the locale string.
244 Locale.getDefault().toString()
247 // Instantiate the proxy helper.
248 val proxyHelper = ProxyHelper()
250 // Get the current proxy.
251 val proxy = proxyHelper.getCurrentProxy(this)
253 // Make the progress bar visible.
254 progressBar.visibility = View.VISIBLE
256 // Set the progress bar to be indeterminate.
257 progressBar.isIndeterminate = true
259 // Instantiate the WebView source factory.
260 val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl!!, userAgent!!, doNotTrack, localeString, proxy, MainWebViewActivity.executorService)
262 // Instantiate the WebView source view model class.
263 val webViewSource = ViewModelProvider(this, webViewSourceFactory).get(WebViewSource::class.java)
265 // Create a source observer.
266 webViewSource.observeSource().observe(this, { sourceStringArray: Array<SpannableStringBuilder> ->
267 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
268 requestHeadersTextView.text = sourceStringArray[0]
269 responseMessageTextView.text = sourceStringArray[1]
270 responseHeadersTextView.text = sourceStringArray[2]
271 responseBodyTextView.text = sourceStringArray[3]
273 // Hide the progress bar.
274 progressBar.isIndeterminate = false
275 progressBar.visibility = View.GONE
277 //Stop the swipe to refresh indicator if it is running
278 swipeRefreshLayout.isRefreshing = false
281 // Create an error observer.
282 webViewSource.observeErrors().observe(this, { errorString: String ->
283 // Display an error snackbar if the string is not `""`.
284 if (errorString != "") {
285 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
289 // Implement swipe to refresh.
290 swipeRefreshLayout.setOnRefreshListener {
291 // Make the progress bar visible.
292 progressBar.visibility = View.VISIBLE
294 // Set the progress bar to be indeterminate.
295 progressBar.isIndeterminate = true
298 val urlString = urlEditText.text.toString()
300 // Get the updated source.
301 webViewSource.updateSource(urlString)
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 // Get the updated source.
324 webViewSource.updateSource(urlString)
326 // Consume the key press.
327 return@setOnKeyListener true
329 // Do not consume the key press.
330 return@setOnKeyListener false
335 override fun onCreateOptionsMenu(menu: Menu): Boolean {
336 // Inflate the menu. This adds items to the action bar if it is present.
337 menuInflater.inflate(R.menu.view_source_options_menu, menu)
343 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
344 // Get a handle for the about alert dialog.
345 val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
347 // Show the about alert dialog.
348 aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
350 // Consume the event.
354 // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
355 fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
357 NavUtils.navigateUpFromSameTask(this)
360 private fun highlightUrlText() {
361 // Get a handle for the URL edit text.
362 val urlEditText = findViewById<EditText>(R.id.url_edittext)
364 // Get the URL string.
365 val urlString = urlEditText.text.toString()
367 // Highlight the URL according to the protocol.
368 if (urlString.startsWith("file://")) { // This is a file URL.
369 // De-emphasize only the protocol.
370 urlEditText.text.setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
371 } else if (urlString.startsWith("content://")) {
372 // De-emphasize only the protocol.
373 urlEditText.text.setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
374 } else { // This is a web URL.
375 // Get the index of the `/` immediately after the domain name.
376 val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
378 // Create a base URL string.
382 baseUrl = if (endOfDomainName > 0) { // There is at least one character after the base URL.
384 urlString.substring(0, endOfDomainName)
385 } else { // There are no characters after the base URL.
386 // Set the base URL to be the entire URL string.
390 // Get the index of the last `.` in the domain.
391 val lastDotIndex = baseUrl.lastIndexOf(".")
393 // Get the index of the penultimate `.` in the domain.
394 val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
396 // Markup the beginning of the URL.
397 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
398 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
400 // De-emphasize subdomains.
401 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
402 urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
404 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
405 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
406 // De-emphasize the protocol and the additional subdomains.
407 urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
408 } else { // There is only one subdomain in the domain name.
409 // De-emphasize only the protocol.
410 urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
414 // De-emphasize the text after the domain name.
415 if (endOfDomainName > 0) {
416 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)