X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FViewSourceActivity.kt;fp=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FViewSourceActivity.kt;h=64e4ee98643d9d244b7e83fa648becc5396d93ad;hp=0000000000000000000000000000000000000000;hb=606b5659cfd41c546fcbf248dcde72fd3c60d1c3;hpb=8a06558b0071fa94e2a7d1093b3118417ac5cc8f diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt new file mode 100644 index 00000000..64e4ee98 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt @@ -0,0 +1,420 @@ +/* + * Copyright © 2017-2020 Soren Stoutner . + * + * This file is part of Privacy Browser . + * + * Privacy Browser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser. If not, see . + */ + +package com.stoutner.privacybrowser.activities + +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.util.TypedValue +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.ProgressBar +import android.widget.TextView + +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.app.NavUtils +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +import com.google.android.material.snackbar.Snackbar + +import com.stoutner.privacybrowser.R +import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog +import com.stoutner.privacybrowser.helpers.ProxyHelper +import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory +import com.stoutner.privacybrowser.viewmodels.WebViewSource + +import java.util.Locale + +// Declare the public constants. +const val CURRENT_URL = "current_url" +const val USER_AGENT = "user_agent" + +class ViewSourceActivity: AppCompatActivity() { + // Declare the class variables. + private lateinit var initialGrayColorSpan: ForegroundColorSpan + private lateinit var finalGrayColorSpan: ForegroundColorSpan + private lateinit var redColorSpan: ForegroundColorSpan + + override fun onCreate(savedInstanceState: Bundle?) { + // Get a handle for the shared preferences. + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + + // Get the screenshot preference. + val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false) + + // Disable screenshots if not allowed. + if (!allowScreenshots) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + + // Set the theme. + setTheme(R.style.PrivacyBrowser) + + // Run the default commands. + super.onCreate(savedInstanceState) + + // Get the launching intent + val intent = intent + + // Get the information from the intent. + val currentUrl = intent.getStringExtra(CURRENT_URL) + val userAgent = intent.getStringExtra(USER_AGENT) + + // Set the content view. + setContentView(R.layout.view_source_coordinatorlayout) + + // Get a handle for the toolbar. + val toolbar = findViewById(R.id.view_source_toolbar) + + // Set the support action bar. + setSupportActionBar(toolbar) + + // Get a handle for the action bar. + val actionBar = supportActionBar!! + + // Add the custom layout to the action bar. + actionBar.setCustomView(R.layout.view_source_app_bar) + + // Instruct the action bar to display a custom layout. + actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM + + // Get handles for the views. + val urlEditText = findViewById(R.id.url_edittext) + val requestHeadersTextView = findViewById(R.id.request_headers) + val responseMessageTextView = findViewById(R.id.response_message) + val responseHeadersTextView = findViewById(R.id.response_headers) + val responseBodyTextView = findViewById(R.id.response_body) + val progressBar = findViewById(R.id.progress_bar) + val swipeRefreshLayout = findViewById(R.id.view_source_swiperefreshlayout) + + // Populate the URL text box. + urlEditText.setText(currentUrl) + + // Initialize the gray foreground color spans for highlighting the URLs. The deprecated `getColor()` must be used until the minimum API >= 23. + @Suppress("DEPRECATION") + initialGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500)) + @Suppress("DEPRECATION") + finalGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500)) + + // Get the current theme status. + val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + + // Set the red color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23. + redColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + @Suppress("DEPRECATION") + ForegroundColorSpan(resources.getColor(R.color.red_a700)) + } else { + @Suppress("DEPRECATION") + ForegroundColorSpan(resources.getColor(R.color.red_900)) + } + + // Apply text highlighting to the URL. + highlightUrlText() + + // Get a handle for the input method manager, which is used to hide the keyboard. + val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager) + + // Remove the formatting from the URL when the user is editing the text. + urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> + if (hasFocus) { // The user is editing the URL text box. + // Remove the highlighting. + urlEditText.text.removeSpan(redColorSpan) + urlEditText.text.removeSpan(initialGrayColorSpan) + urlEditText.text.removeSpan(finalGrayColorSpan) + } else { // The user has stopped editing the URL text box. + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0) + + // Move to the beginning of the string. + urlEditText.setSelection(0) + + // Reapply the highlighting. + highlightUrlText() + } + } + + // Set the refresh color scheme according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + swipeRefreshLayout.setColorSchemeResources(R.color.blue_700) + } else { + swipeRefreshLayout.setColorSchemeResources(R.color.violet_500) + } + + // Initialize a color background typed value. + val colorBackgroundTypedValue = TypedValue() + + // Get the color background from the theme. + theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true) + + // Get the color background int from the typed value. + val colorBackgroundInt = colorBackgroundTypedValue.data + + // Set the swipe refresh background color. + swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt) + + // Get the Do Not Track status. + val doNotTrack = sharedPreferences.getBoolean(getString(R.string.do_not_track_key), false) + + // Populate the locale string. + val localeString = if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales. + // Get the list of locales. + val localeList = resources.configuration.locales + + // Initialize a string builder to extract the locales from the list. + val localesStringBuilder = StringBuilder() + + // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages. + var q = 10 + + // Populate the string builder with the contents of the locales list. + for (i in 0 until localeList.size()) { + // Append a comma if there is already an item in the string builder. + if (i > 0) { + localesStringBuilder.append(",") + } + + // Get the locale from the list. + val locale = localeList[i] + + // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format. + localesStringBuilder.append(locale.language) + localesStringBuilder.append("-") + localesStringBuilder.append(locale.country) + + // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1. + if (q < 10) { + localesStringBuilder.append(";q=0.") + localesStringBuilder.append(q) + } + + // Decrement `q` if it is greater than 1. + if (q > 1) { + q-- + } + + // Add a second entry for the language only portion of the locale. + localesStringBuilder.append(",") + localesStringBuilder.append(locale.language) + + // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1. + localesStringBuilder.append(";q=0.") + localesStringBuilder.append(q) + + // Decrement `q` if it is greater than 1. + if (q > 1) { + q-- + } + } + + // Store the populated string builder in the locale string. + localesStringBuilder.toString() + } else { // SDK < 24 only has a primary locale. + // Store the locale in the locale string. + Locale.getDefault().toString() + } + + // Instantiate the proxy helper. + val proxyHelper = ProxyHelper() + + // Get the current proxy. + val proxy = proxyHelper.getCurrentProxy(this) + + // Make the progress bar visible. + progressBar.visibility = View.VISIBLE + + // Set the progress bar to be indeterminate. + progressBar.isIndeterminate = true + + // Instantiate the WebView source factory. + val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl!!, userAgent!!, doNotTrack, localeString, proxy, MainWebViewActivity.executorService) + + // Instantiate the WebView source view model class. + val webViewSource = ViewModelProvider(this, webViewSourceFactory).get(WebViewSource::class.java) + + // Create a source observer. + webViewSource.observeSource().observe(this, { sourceStringArray: Array -> + // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large. + requestHeadersTextView.text = sourceStringArray[0] + responseMessageTextView.text = sourceStringArray[1] + responseHeadersTextView.text = sourceStringArray[2] + responseBodyTextView.text = sourceStringArray[3] + + // Hide the progress bar. + progressBar.isIndeterminate = false + progressBar.visibility = View.GONE + + //Stop the swipe to refresh indicator if it is running + swipeRefreshLayout.isRefreshing = false + }) + + // Create an error observer. + webViewSource.observeErrors().observe(this, { errorString: String -> + // Display an error snackbar if the string is not `""`. + if (errorString != "") { + Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show() + } + }) + + // Implement swipe to refresh. + swipeRefreshLayout.setOnRefreshListener { + // Make the progress bar visible. + progressBar.visibility = View.VISIBLE + + // Set the progress bar to be indeterminate. + progressBar.isIndeterminate = true + + // Get the URL. + val urlString = urlEditText.text.toString() + + // Get the updated source. + webViewSource.updateSource(urlString) + } + + // Set the go button on the keyboard to request new source data. + urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent -> + // Request new source data if the enter key was pressed. + if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0) + + // Remove the focus from the URL box. + urlEditText.clearFocus() + + // Make the progress bar visible. + progressBar.visibility = View.VISIBLE + + // Set the progress bar to be indeterminate. + progressBar.isIndeterminate = true + + // Get the URL. + val urlString = urlEditText.text.toString() + + // Get the updated source. + webViewSource.updateSource(urlString) + + // Consume the key press. + return@setOnKeyListener true + } else { + // Do not consume the key press. + return@setOnKeyListener false + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu. This adds items to the action bar if it is present. + menuInflater.inflate(R.menu.view_source_options_menu, menu) + + // Display the menu. + return true + } + + override fun onOptionsItemSelected(menuItem: MenuItem): Boolean { + // Get a handle for the about alert dialog. + val aboutDialogFragment: DialogFragment = AboutViewSourceDialog() + + // Show the about alert dialog. + aboutDialogFragment.show(supportFragmentManager, getString(R.string.about)) + + // Consume the event. + return true + } + + // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar. + fun goBack(@Suppress("UNUSED_PARAMETER") view: View) { + // Go home. + NavUtils.navigateUpFromSameTask(this) + } + + private fun highlightUrlText() { + // Get a handle for the URL edit text. + val urlEditText = findViewById(R.id.url_edittext) + + // Get the URL string. + val urlString = urlEditText.text.toString() + + // Highlight the URL according to the protocol. + if (urlString.startsWith("file://")) { // This is a file URL. + // De-emphasize only the protocol. + urlEditText.text.setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } else if (urlString.startsWith("content://")) { + // De-emphasize only the protocol. + urlEditText.text.setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } else { // This is a web URL. + // Get the index of the `/` immediately after the domain name. + val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2) + + // Create a base URL string. + val baseUrl: String + + // Get the base URL. + baseUrl = if (endOfDomainName > 0) { // There is at least one character after the base URL. + // Get the base URL. + urlString.substring(0, endOfDomainName) + } else { // There are no characters after the base URL. + // Set the base URL to be the entire URL string. + urlString + } + + // Get the index of the last `.` in the domain. + val lastDotIndex = baseUrl.lastIndexOf(".") + + // Get the index of the penultimate `.` in the domain. + val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1) + + // Markup the beginning of the URL. + if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted. + urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // De-emphasize subdomains. + if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. + urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } + } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted. + if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. + // De-emphasize the protocol and the additional subdomains. + urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } else { // There is only one subdomain in the domain name. + // De-emphasize only the protocol. + urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } + } + + // De-emphasize the text after the domain name. + if (endOfDomainName > 0) { + urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } + } + } +} \ No newline at end of file