e4899ea329669f741c3b6355558b7e3f4bceba6e
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewSourceActivity.java
1 /*
2  * Copyright © 2017-2020 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.activities;
21
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;
43
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;
52
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;
59
60 import java.net.Proxy;
61 import java.util.Locale;
62
63 public class ViewSourceActivity extends AppCompatActivity {
64     // `activity` is used in `onCreate()` and `goBack()`.
65     private Activity activity;
66
67     // The color spans are used in `onCreate()` and `highlightUrlText()`.
68     private ForegroundColorSpan redColorSpan;
69     private ForegroundColorSpan initialGrayColorSpan;
70     private ForegroundColorSpan finalGrayColorSpan;
71
72     @Override
73     protected void onCreate(Bundle savedInstanceState) {
74         // Get a handle for the shared preferences.
75         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
76
77         // Get the screenshot preference.
78         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
79
80         // Disable screenshots if not allowed.
81         if (!allowScreenshots) {
82             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
83         }
84
85         // Set the theme.
86         setTheme(R.style.PrivacyBrowser);
87
88         // Run the default commands.
89         super.onCreate(savedInstanceState);
90
91         // Get the launching intent
92         Intent intent = getIntent();
93
94         // Get the information from the intent.
95         String userAgent = intent.getStringExtra("user_agent");
96         String currentUrl = intent.getStringExtra("current_url");
97
98         // Remove the incorrect lint warning below that the user agent might be null.
99         assert userAgent != null;
100
101         // Store a handle for the current activity.
102         activity = this;
103
104         // Set the content view.
105         setContentView(R.layout.view_source_coordinatorlayout);
106
107         // Get a handle for the toolbar.
108         Toolbar toolbar = findViewById(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         final ActionBar actionBar = getSupportActionBar();
115
116         // Remove the incorrect lint warning that the action bar might be null.
117         assert actionBar != null;
118
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);
122
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);
131
132         // Populate the URL text box.
133         urlEditText.setText(currentUrl);
134
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));
138
139         // Get the current theme status.
140         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
141
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));
145         } else {
146             redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
147         }
148
149         // Apply text highlighting to the URL.
150         highlightUrlText();
151
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);
154
155         // Remove the lint warning that the input method manager might be null.
156         assert inputMethodManager != null;
157
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);
168
169                 // Move to the beginning of the string.
170                 urlEditText.setSelection(0);
171
172                 // Reapply the highlighting.
173                 highlightUrlText();
174             }
175         });
176
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);
180         } else {
181             swipeRefreshLayout.setColorSchemeResources(R.color.violet_500);
182         }
183
184         // Initialize a color background typed value.
185         TypedValue colorBackgroundTypedValue = new TypedValue();
186
187         // Get the color background from the theme.
188         getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
189
190         // Get the color background int from the typed value.
191         int colorBackgroundInt = colorBackgroundTypedValue.data;
192
193         // Set the swipe refresh background color.
194         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
195
196         // Get the Do Not Track status.
197         boolean doNotTrack = sharedPreferences.getBoolean("do_not_track", false);
198
199         // Instantiate a locale string.
200         String localeString;
201
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();
206
207             // Initialize a string builder to extract the locales from the list.
208             StringBuilder localesStringBuilder = new StringBuilder();
209
210             // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
211             int q = 10;
212
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.
216                 if (i > 0) {
217                     localesStringBuilder.append(",");
218                 }
219
220                 // Get the locale from the list.
221                 Locale locale = localeList.get(i);
222
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());
227
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.
229                 if (q < 10) {
230                     localesStringBuilder.append(";q=0.");
231                     localesStringBuilder.append(q);
232                 }
233
234                 // Decrement `q` if it is greater than 1.
235                 if (q > 1) {
236                     q--;
237                 }
238
239                 // Add a second entry for the language only portion of the locale.
240                 localesStringBuilder.append(",");
241                 localesStringBuilder.append(locale.getLanguage());
242
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);
246
247                 // Decrement `q` if it is greater than 1.
248                 if (q > 1) {
249                     q--;
250                 }
251             }
252
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();
258         }
259
260         // Instantiate the proxy helper.
261         ProxyHelper proxyHelper = new ProxyHelper();
262
263         // Get the current proxy.
264         Proxy proxy = proxyHelper.getCurrentProxy(this);
265
266         // Make the progress bar visible.
267         progressBar.setVisibility(View.VISIBLE);
268
269         // Set the progress bar to be indeterminate.
270         progressBar.setIndeterminate(true);
271
272         // Instantiate the WebView source factory.
273         ViewModelProvider.Factory webViewSourceFactory = new WebViewSourceFactory(currentUrl, userAgent, doNotTrack, localeString, proxy, MainWebViewActivity.executorService);
274
275         // Instantiate the WebView source view model class.
276         final WebViewSource webViewSource = new ViewModelProvider(this, webViewSourceFactory).get(WebViewSource.class);
277
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]);
285
286             // Hide the progress bar.
287             progressBar.setIndeterminate(false);
288             progressBar.setVisibility(View.GONE);
289
290             //Stop the swipe to refresh indicator if it is running
291             swipeRefreshLayout.setRefreshing(false);
292         });
293
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();
299             }
300         });
301
302         // Implement swipe to refresh.
303         swipeRefreshLayout.setOnRefreshListener(() -> {
304             // Make the progress bar visible.
305             progressBar.setVisibility(View.VISIBLE);
306
307             // Set the progress bar to be indeterminate.
308             progressBar.setIndeterminate(true);
309
310             // Get the URL.
311             String urlString = urlEditText.getText().toString();
312
313             // Get the updated source.
314             webViewSource.updateSource(urlString);
315         });
316
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);
323
324                 // Remove the focus from the URL box.
325                 urlEditText.clearFocus();
326
327                 // Make the progress bar visible.
328                 progressBar.setVisibility(View.VISIBLE);
329
330                 // Set the progress bar to be indeterminate.
331                 progressBar.setIndeterminate(true);
332
333                 // Get the URL.
334                 String urlString = urlEditText.getText().toString();
335
336                 // Get the updated source.
337                 webViewSource.updateSource(urlString);
338
339                 // Consume the key press.
340                 return true;
341             } else {
342                 // Do not consume the key press.
343                 return false;
344             }
345         });
346     }
347
348     @Override
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);
352
353         // Display the menu.
354         return true;
355     }
356
357     @Override
358     public boolean onOptionsItemSelected(@NonNull MenuItem menuItem) {
359         // Get a handle for the about alert dialog.
360         DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
361
362         // Show the about alert dialog.
363         aboutDialogFragment.show(getSupportFragmentManager(), getString(R.string.about));
364
365         // Consume the event.
366         return true;
367     }
368
369     public void goBack(View view) {
370         // Go home.
371         NavUtils.navigateUpFromSameTask(activity);
372     }
373
374     private void highlightUrlText() {
375         // Get a handle for the URL EditText.
376         EditText urlEditText = findViewById(R.id.url_edittext);
377
378         // Get the URL string.
379         String urlString = urlEditText.getText().toString();
380
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));
391
392             // Create a base URL string.
393             String baseUrl;
394
395             // Get the base URL.
396             if (endOfDomainName > 0) {  // There is at least one character after the base URL.
397                 // Get 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.
401                 baseUrl = urlString;
402             }
403
404             // Get the index of the last `.` in the domain.
405             int lastDotIndex = baseUrl.lastIndexOf(".");
406
407             // Get the index of the penultimate `.` in the domain.
408             int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
409
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);
413
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);
417                 }
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);
425                 }
426             }
427
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);
431             }
432         }
433     }
434 }