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.Bundle;
28 import android.preference.PreferenceManager;
29 import android.text.Spanned;
30 import android.text.style.ForegroundColorSpan;
31 import android.util.TypedValue;
32 import android.view.KeyEvent;
33 import android.view.Menu;
34 import android.view.MenuItem;
35 import android.view.View;
36 import android.view.WindowManager;
37 import android.view.inputmethod.InputMethodManager;
38 import android.widget.EditText;
40 import androidx.annotation.NonNull;
41 import androidx.appcompat.app.ActionBar;
42 import androidx.appcompat.app.AppCompatActivity;
43 import androidx.appcompat.widget.Toolbar; // The AndroidX toolbar must be used until the minimum API is >= 21.
44 import androidx.core.app.NavUtils;
45 import androidx.fragment.app.DialogFragment;
46 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
48 import com.stoutner.privacybrowser.R;
49 import com.stoutner.privacybrowser.asynctasks.GetSource;
50 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
52 public class ViewSourceActivity extends AppCompatActivity {
53 // `activity` is used in `onCreate()` and `goBack()`.
54 private Activity activity;
56 // The color spans are used in `onCreate()` and `highlightUrlText()`.
57 private ForegroundColorSpan redColorSpan;
58 private ForegroundColorSpan initialGrayColorSpan;
59 private ForegroundColorSpan finalGrayColorSpan;
62 protected void onCreate(Bundle savedInstanceState) {
63 // Get a handle for the shared preferences.
64 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
66 // Get the screenshot preference.
67 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
69 // Disable screenshots if not allowed.
70 if (!allowScreenshots) {
71 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
75 setTheme(R.style.PrivacyBrowser);
77 // Run the default commands.
78 super.onCreate(savedInstanceState);
80 // Get the launching intent
81 Intent intent = getIntent();
83 // Get the information from the intent.
84 String userAgent = intent.getStringExtra("user_agent");
85 String currentUrl = intent.getStringExtra("current_url");
87 // Store a handle for the current activity.
90 // Set the content view.
91 setContentView(R.layout.view_source_coordinatorlayout);
93 // The AndroidX toolbar must be used until the minimum API is >= 21.
94 Toolbar toolbar = findViewById(R.id.view_source_toolbar);
95 setSupportActionBar(toolbar);
97 // Get a handle for the action bar.
98 final ActionBar actionBar = getSupportActionBar();
100 // Remove the incorrect lint warning that the action bar might be null.
101 assert actionBar != null;
103 // Add the custom layout to the action bar.
104 actionBar.setCustomView(R.layout.view_source_app_bar);
105 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
107 // Get a handle for the url text box.
108 EditText urlEditText = findViewById(R.id.url_edittext);
110 // Populate the URL text box.
111 urlEditText.setText(currentUrl);
113 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
114 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
115 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
116 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
118 // Apply text highlighting to the URL.
121 // Get a handle for the input method manager, which is used to hide the keyboard.
122 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
124 // Remove the lint warning that the input method manager might be null.
125 assert inputMethodManager != null;
127 // Remove the formatting from the URL when the user is editing the text.
128 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
129 if (hasFocus) { // The user is editing `urlTextBox`.
130 // Remove the highlighting.
131 urlEditText.getText().removeSpan(redColorSpan);
132 urlEditText.getText().removeSpan(initialGrayColorSpan);
133 urlEditText.getText().removeSpan(finalGrayColorSpan);
134 } else { // The user has stopped editing `urlTextBox`.
135 // Hide the soft keyboard.
136 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
138 // Move to the beginning of the string.
139 urlEditText.setSelection(0);
141 // Reapply the highlighting.
146 // Set the go button on the keyboard to request new source data.
147 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
148 // Request new source data if the enter key was pressed.
149 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
150 // Hide the soft keyboard.
151 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
153 // Remove the focus from the URL box.
154 urlEditText.clearFocus();
157 String url = urlEditText.getText().toString();
159 // Get new source data for the current URL if it beings with `http`.
160 if (url.startsWith("http")) {
161 new GetSource(this, this, userAgent).execute(url);
164 // Consume the key press.
167 // Do not consume the key press.
172 // Get a handle for the swipe refresh layout.
173 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.view_source_swiperefreshlayout);
175 // Implement swipe to refresh.
176 swipeRefreshLayout.setOnRefreshListener(() -> {
178 String url = urlEditText.getText().toString();
180 // Get new source data for the URL if it begins with `http`.
181 if (url.startsWith("http")) {
182 new GetSource(this, this, userAgent).execute(url);
184 // Stop the refresh animation.
185 swipeRefreshLayout.setRefreshing(false);
189 // Get the current theme status.
190 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
192 // Set the refresh color scheme according to the theme.
193 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
194 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
196 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
199 // Initialize a color background typed value.
200 TypedValue colorBackgroundTypedValue = new TypedValue();
202 // Get the color background from the theme.
203 getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
205 // Get the color background int from the typed value.
206 int colorBackgroundInt = colorBackgroundTypedValue.data;
208 // Set the swipe refresh background color.
209 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
211 // Get the source using an AsyncTask if the URL begins with `http`.
212 if ((currentUrl != null) && currentUrl.startsWith("http")) {
213 new GetSource(this, this, userAgent).execute(currentUrl);
218 public boolean onCreateOptionsMenu(Menu menu) {
219 // Inflate the menu. This adds items to the action bar if it is present.
220 getMenuInflater().inflate(R.menu.view_source_options_menu, menu);
227 public boolean onOptionsItemSelected(@NonNull MenuItem menuItem) {
228 // Get a handle for the about alert dialog.
229 DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
231 // Show the about alert dialog.
232 aboutDialogFragment.show(getSupportFragmentManager(), getString(R.string.about));
234 // Consume the event.
238 public void goBack(View view) {
240 NavUtils.navigateUpFromSameTask(activity);
243 private void highlightUrlText() {
244 // Get a handle for the URL EditText.
245 EditText urlEditText = findViewById(R.id.url_edittext);
247 // Get the URL string.
248 String urlString = urlEditText.getText().toString();
250 // Highlight the URL according to the protocol.
251 if (urlString.startsWith("file://")) { // This is a file URL.
252 // De-emphasize only the protocol.
253 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
254 } else if (urlString.startsWith("content://")) {
255 // De-emphasize only the protocol.
256 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
257 } else { // This is a web URL.
258 // Get the index of the `/` immediately after the domain name.
259 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
261 // Create a base URL string.
265 if (endOfDomainName > 0) { // There is at least one character after the base URL.
267 baseUrl = urlString.substring(0, endOfDomainName);
268 } else { // There are no characters after the base URL.
269 // Set the base URL to be the entire URL string.
273 // Get the index of the last `.` in the domain.
274 int lastDotIndex = baseUrl.lastIndexOf(".");
276 // Get the index of the penultimate `.` in the domain.
277 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
279 // Markup the beginning of the URL.
280 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
281 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
283 // De-emphasize subdomains.
284 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
285 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
287 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
288 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
289 // De-emphasize the protocol and the additional subdomains.
290 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
291 } else { // There is only one subdomain in the domain name.
292 // De-emphasize only the protocol.
293 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
297 // De-emphasize the text after the domain name.
298 if (endOfDomainName > 0) {
299 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);