]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt
Bump the minimum API to 24 (Android 7).
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewSourceActivity.kt
1 /*
2  * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.activities
21
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
36
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
45
46 import com.google.android.material.snackbar.Snackbar
47
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
56
57 // Define the public constants.
58 const val CURRENT_URL = "current_url"
59 const val USER_AGENT = "user_agent"
60
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
67
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
76
77     override fun onCreate(savedInstanceState: Bundle?) {
78         // Get a handle for the shared preferences.
79         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
80
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)
84
85         // Disable screenshots if not allowed.
86         if (!allowScreenshots) {
87             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
88         }
89
90         // Run the default commands.
91         super.onCreate(savedInstanceState)
92
93         // Get the launching intent
94         val intent = intent
95
96         // Get the information from the intent.
97         val currentUrl = intent.getStringExtra(CURRENT_URL)!!
98         val userAgent = intent.getStringExtra(USER_AGENT)!!
99
100         // Set the content view.
101         if (bottomAppBar) {
102             setContentView(R.layout.view_source_bottom_appbar)
103         } else {
104             setContentView(R.layout.view_source_top_appbar)
105         }
106
107         // Get a handle for the toolbar.
108         val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
109
110         // Set the support action bar.
111         setSupportActionBar(toolbar)
112
113         // Get a handle for the action bar.
114         val actionBar = supportActionBar!!
115
116         // Add the custom layout to the action bar.
117         actionBar.setCustomView(R.layout.view_source_appbar_custom_view)
118
119         // Instruct the action bar to display a custom layout.
120         actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
121
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)
134
135         // Populate the URL text box.
136         urlEditText.setText(currentUrl)
137
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))
142
143         // Apply text highlighting to the URL.
144         UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
145
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)
148
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)
159
160                 // Move to the beginning of the string.
161                 urlEditText.setSelection(0)
162
163                 // Reapply the highlighting.
164                 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
165             }
166         }
167
168         // Set the refresh color scheme according to the theme.
169         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
170
171         // Initialize a color background typed value.
172         val colorBackgroundTypedValue = TypedValue()
173
174         // Get the color background from the theme.
175         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
176
177         // Get the color background int from the typed value.
178         val colorBackgroundInt = colorBackgroundTypedValue.data
179
180         // Set the swipe refresh background color.
181         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
182
183         // Get the list of locales.
184         val localeList = resources.configuration.locales
185
186         // Initialize a string builder to extract the locales from the list.
187         val localesStringBuilder = StringBuilder()
188
189         // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
190         var q = 10
191
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.
195             if (i > 0) {
196                 localesStringBuilder.append(",")
197             }
198
199             // Get the locale from the list.
200             val locale = localeList[i]
201
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)
206
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.
208             if (q < 10) {
209                 localesStringBuilder.append(";q=0.")
210                 localesStringBuilder.append(q)
211             }
212
213             // Decrement `q` if it is greater than 1.
214             if (q > 1) {
215                 q--
216             }
217
218             // Add a second entry for the language only portion of the locale.
219             localesStringBuilder.append(",")
220             localesStringBuilder.append(locale.language)
221
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)
225
226             // Decrement `q` if it is greater than 1.
227             if (q > 1) {
228                 q--
229             }
230         }
231
232         // Instantiate the proxy helper.
233         val proxyHelper = ProxyHelper()
234
235         // Get the current proxy.
236         val proxy = proxyHelper.getCurrentProxy(this)
237
238         // Make the progress bar visible.
239         progressBar.visibility = View.VISIBLE
240
241         // Set the progress bar to be indeterminate.
242         progressBar.isIndeterminate = true
243
244         // Update the layout.
245         updateLayout(currentUrl)
246
247         // Instantiate the WebView source factory.
248         val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
249
250         // Instantiate the WebView source view model class.
251         webViewSource = ViewModelProvider(this, webViewSourceFactory)[WebViewSource::class.java]
252
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]
260
261             // Hide the progress bar.
262             progressBar.isIndeterminate = false
263             progressBar.visibility = View.GONE
264
265             //Stop the swipe to refresh indicator if it is running
266             swipeRefreshLayout.isRefreshing = false
267         }
268
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()
276
277                     // Show the untrusted SSL certificate dialog.
278                     untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
279                 } else {
280                     // Display a snackbar with the error message.
281                     Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
282                 }
283             }
284         }
285
286         // Implement swipe to refresh.
287         swipeRefreshLayout.setOnRefreshListener {
288             // Make the progress bar visible.
289             progressBar.visibility = View.VISIBLE
290
291             // Set the progress bar to be indeterminate.
292             progressBar.isIndeterminate = true
293
294             // Get the URL.
295             val urlString = urlEditText.text.toString()
296
297             // Update the layout.
298             updateLayout(urlString)
299
300             // Get the updated source.
301             webViewSource.updateSource(urlString, false)
302         }
303
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)
310
311                 // Remove the focus from the URL box.
312                 urlEditText.clearFocus()
313
314                 // Make the progress bar visible.
315                 progressBar.visibility = View.VISIBLE
316
317                 // Set the progress bar to be indeterminate.
318                 progressBar.isIndeterminate = true
319
320                 // Get the URL.
321                 val urlString = urlEditText.text.toString()
322
323                 // Update the layout.
324                 updateLayout(urlString)
325
326                 // Get the updated source.
327                 webViewSource.updateSource(urlString, false)
328
329                 // Consume the key press.
330                 return@setOnKeyListener true
331             } else {
332                 // Do not consume the key press.
333                 return@setOnKeyListener false
334             }
335         }
336     }
337
338     override fun onCreateOptionsMenu(menu: Menu): Boolean {
339         // Inflate the menu.
340         menuInflater.inflate(R.menu.view_source_options_menu, menu)
341
342         // Display the menu.
343         return true
344     }
345
346     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
347         // Instantiate the about dialog fragment.
348         val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
349
350         // Show the about alert dialog.
351         aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
352
353         // Consume the event.
354         return true
355     }
356
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) {
359         // Go home.
360         NavUtils.navigateUpFromSameTask(this)
361     }
362
363     override fun loadAnyway() {
364         // Load the URL anyway.
365         webViewSource.updateSource(urlEditText.text.toString(), true)
366     }
367
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
375
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.
380             // Show the views.
381             requestHeadersTitleTextView.visibility = View.VISIBLE
382             requestHeadersTextView.visibility = View.VISIBLE
383             responseMessageTitleTextView.visibility = View.VISIBLE
384             responseMessageTextView.visibility = View.VISIBLE
385
386             // Restore the text of the other title text views.
387             responseHeadersTitleTextView.setText(R.string.response_headers)
388             responseBodyTitleTextView.setText(R.string.response_body)
389         }
390     }
391 }