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.app.Activity;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.content.res.Configuration;
27 import android.os.Build;
28 import android.os.Bundle;
29 import android.os.LocaleList;
30 import android.preference.PreferenceManager;
31 import android.text.Spanned;
32 import android.text.style.ForegroundColorSpan;
33 import android.util.TypedValue;
34 import android.view.KeyEvent;
35 import android.view.Menu;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.WindowManager;
39 import android.view.inputmethod.InputMethodManager;
40 import android.widget.EditText;
41 import android.widget.ProgressBar;
42 import android.widget.TextView;
44 import androidx.annotation.NonNull;
45 import androidx.appcompat.app.ActionBar;
46 import androidx.appcompat.app.AppCompatActivity;
47 import androidx.appcompat.widget.Toolbar;
48 import androidx.core.app.NavUtils;
49 import androidx.fragment.app.DialogFragment;
50 import androidx.lifecycle.ViewModelProvider;
51 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
53 import com.google.android.material.snackbar.Snackbar;
54 import com.stoutner.privacybrowser.R;
55 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
56 import com.stoutner.privacybrowser.helpers.ProxyHelper;
57 import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory;
58 import com.stoutner.privacybrowser.viewmodels.WebViewSource;
60 import java.net.Proxy;
61 import java.util.Locale;
63 public class ViewSourceActivity extends AppCompatActivity {
64 // `activity` is used in `onCreate()` and `goBack()`.
65 private Activity activity;
67 // The color spans are used in `onCreate()` and `highlightUrlText()`.
68 private ForegroundColorSpan redColorSpan;
69 private ForegroundColorSpan initialGrayColorSpan;
70 private ForegroundColorSpan finalGrayColorSpan;
73 protected void onCreate(Bundle savedInstanceState) {
74 // Get a handle for the shared preferences.
75 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
77 // Get the screenshot preference.
78 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
80 // Disable screenshots if not allowed.
81 if (!allowScreenshots) {
82 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
86 setTheme(R.style.PrivacyBrowser);
88 // Run the default commands.
89 super.onCreate(savedInstanceState);
91 // Get the launching intent
92 Intent intent = getIntent();
94 // Get the information from the intent.
95 String userAgent = intent.getStringExtra("user_agent");
96 String currentUrl = intent.getStringExtra("current_url");
98 // Remove the incorrect lint warning below that the user agent might be null.
99 assert userAgent != null;
101 // Store a handle for the current activity.
104 // Set the content view.
105 setContentView(R.layout.view_source_coordinatorlayout);
107 // Get a handle for the toolbar.
108 Toolbar toolbar = findViewById(R.id.view_source_toolbar);
110 // Set the support action bar.
111 setSupportActionBar(toolbar);
113 // Get a handle for the action bar.
114 final ActionBar actionBar = getSupportActionBar();
116 // Remove the incorrect lint warning that the action bar might be null.
117 assert actionBar != null;
119 // Add the custom layout to the action bar.
120 actionBar.setCustomView(R.layout.view_source_app_bar);
121 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
123 // Get handles for the views.
124 EditText urlEditText = findViewById(R.id.url_edittext);
125 TextView requestHeadersTextView = findViewById(R.id.request_headers);
126 TextView responseMessageTextView = findViewById(R.id.response_message);
127 TextView responseHeadersTextView = findViewById(R.id.response_headers);
128 TextView responseBodyTextView = findViewById(R.id.response_body);
129 ProgressBar progressBar = findViewById(R.id.progress_bar);
130 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.view_source_swiperefreshlayout);
132 // Populate the URL text box.
133 urlEditText.setText(currentUrl);
135 // Initialize the gray foreground color spans for highlighting the URLs. The deprecated `getResources()` must be used until API >= 23.
136 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
137 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
139 // Get the current theme status.
140 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
142 // Set the red color span according to the theme.
143 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
144 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
146 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
149 // Apply text highlighting to the URL.
152 // Get a handle for the input method manager, which is used to hide the keyboard.
153 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
155 // Remove the lint warning that the input method manager might be null.
156 assert inputMethodManager != null;
158 // Remove the formatting from the URL when the user is editing the text.
159 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
160 if (hasFocus) { // The user is editing `urlTextBox`.
161 // Remove the highlighting.
162 urlEditText.getText().removeSpan(redColorSpan);
163 urlEditText.getText().removeSpan(initialGrayColorSpan);
164 urlEditText.getText().removeSpan(finalGrayColorSpan);
165 } else { // The user has stopped editing `urlTextBox`.
166 // Hide the soft keyboard.
167 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
169 // Move to the beginning of the string.
170 urlEditText.setSelection(0);
172 // Reapply the highlighting.
177 // Set the refresh color scheme according to the theme.
178 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
179 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
181 swipeRefreshLayout.setColorSchemeResources(R.color.violet_500);
184 // Initialize a color background typed value.
185 TypedValue colorBackgroundTypedValue = new TypedValue();
187 // Get the color background from the theme.
188 getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
190 // Get the color background int from the typed value.
191 int colorBackgroundInt = colorBackgroundTypedValue.data;
193 // Set the swipe refresh background color.
194 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
196 // Get the Do Not Track status.
197 boolean doNotTrack = sharedPreferences.getBoolean("do_not_track", false);
199 // Instantiate a locale string.
202 // Populate the locale string.
203 if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
204 // Get the list of locales.
205 LocaleList localeList = getResources().getConfiguration().getLocales();
207 // Initialize a string builder to extract the locales from the list.
208 StringBuilder localesStringBuilder = new StringBuilder();
210 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
213 // Populate the string builder with the contents of the locales list.
214 for (int i = 0; i < localeList.size(); i++) {
215 // Append a comma if there is already an item in the string builder.
217 localesStringBuilder.append(",");
220 // Get the locale from the list.
221 Locale locale = localeList.get(i);
223 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
224 localesStringBuilder.append(locale.getLanguage());
225 localesStringBuilder.append("-");
226 localesStringBuilder.append(locale.getCountry());
228 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
230 localesStringBuilder.append(";q=0.");
231 localesStringBuilder.append(q);
234 // Decrement `q` if it is greater than 1.
239 // Add a second entry for the language only portion of the locale.
240 localesStringBuilder.append(",");
241 localesStringBuilder.append(locale.getLanguage());
243 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
244 localesStringBuilder.append(";q=0.");
245 localesStringBuilder.append(q);
247 // Decrement `q` if it is greater than 1.
253 // Store the populated string builder in the locale string.
254 localeString = localesStringBuilder.toString();
255 } else { // SDK < 24 only has a primary locale.
256 // Store the locale in the locale string.
257 localeString = Locale.getDefault().toString();
260 // Instantiate the proxy helper.
261 ProxyHelper proxyHelper = new ProxyHelper();
263 // Get the current proxy.
264 Proxy proxy = proxyHelper.getCurrentProxy(this);
266 // Make the progress bar visible.
267 progressBar.setVisibility(View.VISIBLE);
269 // Set the progress bar to be indeterminate.
270 progressBar.setIndeterminate(true);
272 // Instantiate the WebView source factory.
273 ViewModelProvider.Factory webViewSourceFactory = new WebViewSourceFactory(currentUrl, userAgent, doNotTrack, localeString, proxy, MainWebViewActivity.executorService);
275 // Instantiate the WebView source view model class.
276 final WebViewSource webViewSource = new ViewModelProvider(this, webViewSourceFactory).get(WebViewSource.class);
278 // Create a source observer.
279 webViewSource.observeSource().observe(this, sourceStringArray -> {
280 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
281 requestHeadersTextView.setText(sourceStringArray[0]);
282 responseMessageTextView.setText(sourceStringArray[1]);
283 responseHeadersTextView.setText(sourceStringArray[2]);
284 responseBodyTextView.setText(sourceStringArray[3]);
286 // Hide the progress bar.
287 progressBar.setIndeterminate(false);
288 progressBar.setVisibility(View.GONE);
290 //Stop the swipe to refresh indicator if it is running
291 swipeRefreshLayout.setRefreshing(false);
294 // Create an error observer.
295 webViewSource.observeErrors().observe(this, errorString -> {
296 // Display an error snackbar if the string is not `""`.
297 if (!errorString.equals("")) {
298 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show();
302 // Implement swipe to refresh.
303 swipeRefreshLayout.setOnRefreshListener(() -> {
304 // Make the progress bar visible.
305 progressBar.setVisibility(View.VISIBLE);
307 // Set the progress bar to be indeterminate.
308 progressBar.setIndeterminate(true);
311 String urlString = urlEditText.getText().toString();
313 // Get the updated source.
314 webViewSource.updateSource(urlString);
317 // Set the go button on the keyboard to request new source data.
318 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
319 // Request new source data if the enter key was pressed.
320 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
321 // Hide the soft keyboard.
322 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
324 // Remove the focus from the URL box.
325 urlEditText.clearFocus();
327 // Make the progress bar visible.
328 progressBar.setVisibility(View.VISIBLE);
330 // Set the progress bar to be indeterminate.
331 progressBar.setIndeterminate(true);
334 String urlString = urlEditText.getText().toString();
336 // Get the updated source.
337 webViewSource.updateSource(urlString);
339 // Consume the key press.
342 // Do not consume the key press.
349 public boolean onCreateOptionsMenu(Menu menu) {
350 // Inflate the menu. This adds items to the action bar if it is present.
351 getMenuInflater().inflate(R.menu.view_source_options_menu, menu);
358 public boolean onOptionsItemSelected(@NonNull MenuItem menuItem) {
359 // Get a handle for the about alert dialog.
360 DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
362 // Show the about alert dialog.
363 aboutDialogFragment.show(getSupportFragmentManager(), getString(R.string.about));
365 // Consume the event.
369 public void goBack(View view) {
371 NavUtils.navigateUpFromSameTask(activity);
374 private void highlightUrlText() {
375 // Get a handle for the URL EditText.
376 EditText urlEditText = findViewById(R.id.url_edittext);
378 // Get the URL string.
379 String urlString = urlEditText.getText().toString();
381 // Highlight the URL according to the protocol.
382 if (urlString.startsWith("file://")) { // This is a file URL.
383 // De-emphasize only the protocol.
384 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
385 } else if (urlString.startsWith("content://")) {
386 // De-emphasize only the protocol.
387 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
388 } else { // This is a web URL.
389 // Get the index of the `/` immediately after the domain name.
390 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
392 // Create a base URL string.
396 if (endOfDomainName > 0) { // There is at least one character after the base URL.
398 baseUrl = urlString.substring(0, endOfDomainName);
399 } else { // There are no characters after the base URL.
400 // Set the base URL to be the entire URL string.
404 // Get the index of the last `.` in the domain.
405 int lastDotIndex = baseUrl.lastIndexOf(".");
407 // Get the index of the penultimate `.` in the domain.
408 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
410 // Markup the beginning of the URL.
411 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
412 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
414 // De-emphasize subdomains.
415 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
416 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
418 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
419 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
420 // De-emphasize the protocol and the additional subdomains.
421 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
422 } else { // There is only one subdomain in the domain name.
423 // De-emphasize only the protocol.
424 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
428 // De-emphasize the text after the domain name.
429 if (endOfDomainName > 0) {
430 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);