2 * Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
4 * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
6 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
8 * Privacy Browser is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
13 * Privacy Browser is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
22 package com.stoutner.privacybrowser.activities;
24 import android.Manifest;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.DownloadManager;
28 import android.app.SearchManager;
29 import android.content.ActivityNotFoundException;
30 import android.content.BroadcastReceiver;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.res.Configuration;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Typeface;
43 import android.graphics.drawable.BitmapDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.net.Uri;
46 import android.net.http.SslCertificate;
47 import android.net.http.SslError;
48 import android.os.Build;
49 import android.os.Bundle;
50 import android.os.Environment;
51 import android.os.Handler;
52 import android.preference.PreferenceManager;
53 import android.print.PrintDocumentAdapter;
54 import android.print.PrintManager;
55 import android.text.Editable;
56 import android.text.Spanned;
57 import android.text.TextWatcher;
58 import android.text.style.ForegroundColorSpan;
59 import android.util.Patterns;
60 import android.view.ContextMenu;
61 import android.view.GestureDetector;
62 import android.view.KeyEvent;
63 import android.view.Menu;
64 import android.view.MenuItem;
65 import android.view.MotionEvent;
66 import android.view.View;
67 import android.view.ViewGroup;
68 import android.view.WindowManager;
69 import android.view.inputmethod.InputMethodManager;
70 import android.webkit.CookieManager;
71 import android.webkit.HttpAuthHandler;
72 import android.webkit.SslErrorHandler;
73 import android.webkit.ValueCallback;
74 import android.webkit.WebChromeClient;
75 import android.webkit.WebResourceResponse;
76 import android.webkit.WebSettings;
77 import android.webkit.WebStorage;
78 import android.webkit.WebView;
79 import android.webkit.WebViewClient;
80 import android.webkit.WebViewDatabase;
81 import android.widget.ArrayAdapter;
82 import android.widget.CursorAdapter;
83 import android.widget.EditText;
84 import android.widget.FrameLayout;
85 import android.widget.ImageView;
86 import android.widget.LinearLayout;
87 import android.widget.ListView;
88 import android.widget.ProgressBar;
89 import android.widget.RadioButton;
90 import android.widget.RelativeLayout;
91 import android.widget.TextView;
93 import androidx.annotation.NonNull;
94 import androidx.appcompat.app.ActionBar;
95 import androidx.appcompat.app.ActionBarDrawerToggle;
96 import androidx.appcompat.app.AppCompatActivity;
97 import androidx.appcompat.widget.Toolbar;
98 import androidx.coordinatorlayout.widget.CoordinatorLayout;
99 import androidx.core.app.ActivityCompat;
100 import androidx.core.content.ContextCompat;
101 import androidx.core.view.GravityCompat;
102 import androidx.drawerlayout.widget.DrawerLayout;
103 import androidx.fragment.app.DialogFragment;
104 import androidx.fragment.app.FragmentManager;
105 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
106 import androidx.viewpager.widget.ViewPager;
108 import com.google.android.material.appbar.AppBarLayout;
109 import com.google.android.material.floatingactionbutton.FloatingActionButton;
110 import com.google.android.material.navigation.NavigationView;
111 import com.google.android.material.snackbar.Snackbar;
112 import com.google.android.material.tabs.TabLayout;
114 import com.stoutner.privacybrowser.BuildConfig;
115 import com.stoutner.privacybrowser.R;
116 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
117 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
118 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
121 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
124 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
125 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
126 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
127 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
128 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
129 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
130 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
131 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
132 import com.stoutner.privacybrowser.helpers.AdHelper;
133 import com.stoutner.privacybrowser.helpers.BlockListHelper;
134 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
135 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
136 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
138 import com.stoutner.privacybrowser.views.NestedScrollWebView;
140 import java.io.ByteArrayInputStream;
141 import java.io.ByteArrayOutputStream;
143 import java.io.IOException;
144 import java.io.UnsupportedEncodingException;
145 import java.net.MalformedURLException;
147 import java.net.URLDecoder;
148 import java.net.URLEncoder;
149 import java.util.ArrayList;
150 import java.util.Date;
151 import java.util.HashMap;
152 import java.util.HashSet;
153 import java.util.List;
154 import java.util.Map;
155 import java.util.Set;
157 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
158 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
159 DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
160 EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener {
162 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
163 public static String orbotStatus;
165 // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`.
166 public static WebViewPagerAdapter webViewPagerAdapter;
168 // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`. They are used in `onRestart()`.
169 public static boolean loadUrlOnRestart;
170 public static String urlToLoadOnRestart;
172 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
173 public static boolean restartFromBookmarksActivity;
175 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
176 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
177 public static String currentBookmarksFolder;
179 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
180 public final static int UNRECOGNIZED_USER_AGENT = -1;
181 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
182 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
183 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
184 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
185 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
189 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
190 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
191 private NestedScrollWebView currentWebView;
193 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
194 private final Map<String, String> customHeaders = new HashMap<>();
196 // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
197 private String searchURL;
199 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
200 private Menu optionsMenu;
202 // The blocklists are populated in `onCreate()` and accessed from `initializeWebView()`.
203 private ArrayList<List<String[]>> easyList;
204 private ArrayList<List<String[]>> easyPrivacy;
205 private ArrayList<List<String[]>> fanboysAnnoyanceList;
206 private ArrayList<List<String[]>> fanboysSocialList;
207 private ArrayList<List<String[]>> ultraPrivacy;
209 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
210 private String webViewDefaultUserAgent;
212 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
213 private boolean proxyThroughOrbot;
215 // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
216 private boolean incognitoModeEnabled;
218 // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
219 private boolean fullScreenBrowsingModeEnabled;
221 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
222 private boolean inFullScreenBrowsingMode;
224 // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`.
225 private boolean hideAppBar;
226 private boolean scrollAppBar;
228 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
229 private boolean reapplyDomainSettingsOnRestart;
231 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
232 private boolean reapplyAppSettingsOnRestart;
234 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
235 private boolean displayingFullScreenVideo;
237 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
238 private BroadcastReceiver orbotStatusBroadcastReceiver;
240 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
241 private boolean waitingForOrbot;
243 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
244 private ActionBarDrawerToggle actionBarDrawerToggle;
246 // The color spans are used in `onCreate()` and `highlightUrlText()`.
247 private ForegroundColorSpan redColorSpan;
248 private ForegroundColorSpan initialGrayColorSpan;
249 private ForegroundColorSpan finalGrayColorSpan;
251 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
252 private int drawerHeaderPaddingLeftAndRight;
253 private int drawerHeaderPaddingTop;
254 private int drawerHeaderPaddingBottom;
256 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
257 // and `loadBookmarksFolder()`.
258 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
260 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
261 private Cursor bookmarksCursor;
263 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
264 private CursorAdapter bookmarksCursorAdapter;
266 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
267 private String oldFolderNameString;
269 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
270 private ValueCallback<Uri[]> fileChooserCallback;
272 // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
273 private int defaultProgressViewStartOffset;
274 private int defaultProgressViewEndOffset;
276 // The swipe refresh layout top padding is used when exiting full screen browsing mode. It is used in an inner class in `initializeWebView()`.
277 private int swipeRefreshLayoutPaddingTop;
279 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
280 private String downloadUrl;
281 private String downloadContentDisposition;
282 private long downloadContentLength;
284 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
285 private String downloadImageUrl;
287 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
288 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
289 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
292 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
293 @SuppressLint("ClickableViewAccessibility")
294 protected void onCreate(Bundle savedInstanceState) {
295 // Get a handle for the shared preferences.
296 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
298 // Get the theme and screenshot preferences.
299 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
300 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
302 // Disable screenshots if not allowed.
303 if (!allowScreenshots) {
304 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
307 // Set the activity theme.
309 setTheme(R.style.PrivacyBrowserDark);
311 setTheme(R.style.PrivacyBrowserLight);
314 // Run the default commands.
315 super.onCreate(savedInstanceState);
317 // Set the content view.
318 setContentView(R.layout.main_framelayout);
320 // Get a handle for the input method.
321 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
323 // Remove the lint warning below that the input method manager might be null.
324 assert inputMethodManager != null;
326 // Get a handle for the toolbar.
327 Toolbar toolbar = findViewById(R.id.toolbar);
329 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
330 setSupportActionBar(toolbar);
332 // Get a handle for the action bar.
333 ActionBar actionBar = getSupportActionBar();
335 // This is needed to get rid of the Android Studio warning that the action bar might be null.
336 assert actionBar != null;
338 // Add the custom layout, which shows the URL text bar.
339 actionBar.setCustomView(R.layout.url_app_bar);
340 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
342 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
343 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
344 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
345 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
347 // Get handles for the URL views.
348 EditText urlEditText = findViewById(R.id.url_edittext);
350 // Remove the formatting from `urlTextBar` when the user is editing the text.
351 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
352 if (hasFocus) { // The user is editing the URL text box.
353 // Remove the highlighting.
354 urlEditText.getText().removeSpan(redColorSpan);
355 urlEditText.getText().removeSpan(initialGrayColorSpan);
356 urlEditText.getText().removeSpan(finalGrayColorSpan);
357 } else { // The user has stopped editing the URL text box.
358 // Move to the beginning of the string.
359 urlEditText.setSelection(0);
361 // Reapply the highlighting.
366 // Set the go button on the keyboard to load the URL in `urlTextBox`.
367 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
368 // If the event is a key-down event on the `enter` button, load the URL.
369 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
370 // Load the URL into the mainWebView and consume the event.
371 loadUrlFromTextBox();
373 // If the enter key was pressed, consume the event.
376 // If any other key was pressed, do not consume the event.
381 // Initialize the Orbot status and the waiting for Orbot trackers.
382 orbotStatus = "unknown";
383 waitingForOrbot = false;
385 // Create an Orbot status `BroadcastReceiver`.
386 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
388 public void onReceive(Context context, Intent intent) {
389 // Store the content of the status message in `orbotStatus`.
390 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
392 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
393 if (orbotStatus.equals("ON") && waitingForOrbot) {
394 // Reset the waiting for Orbot status.
395 waitingForOrbot = false;
397 // Get the intent that started the app.
398 Intent launchingIntent = getIntent();
400 // Get the information from the intent.
401 String launchingIntentAction = launchingIntent.getAction();
402 Uri launchingIntentUriData = launchingIntent.getData();
404 // If the intent action is a web search, perform the search.
405 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
406 // Create an encoded URL string.
407 String encodedUrlString;
409 // Sanitize the search input and convert it to a search.
411 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
412 } catch (UnsupportedEncodingException exception) {
413 encodedUrlString = "";
416 // Load the completed search URL.
417 loadUrl(searchURL + encodedUrlString);
418 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
419 // Load the URL from the intent.
420 loadUrl(launchingIntentUriData.toString());
421 } else { // The is no URL in the intent.
422 // Select the homepage based on the proxy through Orbot status.
423 if (proxyThroughOrbot) {
424 // Load the Tor homepage.
425 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
427 // Load the normal homepage.
428 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
435 // Register `orbotStatusBroadcastReceiver` on `this` context.
436 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
438 // Instantiate the blocklist helper.
439 BlockListHelper blockListHelper = new BlockListHelper();
441 // Parse the block lists.
442 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
443 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
444 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
445 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
446 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
448 // Get handles for views that need to be modified.
449 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
450 NavigationView navigationView = findViewById(R.id.navigationview);
451 TabLayout tabLayout = findViewById(R.id.tablayout);
452 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
453 ViewPager webViewPager = findViewById(R.id.webviewpager);
454 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
455 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
456 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
457 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
458 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
460 // Listen for touches on the navigation menu.
461 navigationView.setNavigationItemSelectedListener(this);
463 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
464 Menu navigationMenu = navigationView.getMenu();
465 MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
466 MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
467 MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
468 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
469 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
471 // Initialize the web view pager adapter.
472 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
474 // Set the pager adapter on the web view pager.
475 webViewPager.setAdapter(webViewPagerAdapter);
477 // Store up to 100 tabs in memory.
478 webViewPager.setOffscreenPageLimit(100);
480 // Update the web view pager every time a tab is modified.
481 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
483 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
488 public void onPageSelected(int position) {
489 // Close the find on page bar if it is open.
490 closeFindOnPage(null);
492 // Set the current WebView.
493 setCurrentWebView(position);
495 // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager or by creating a new tab.
496 if (tabLayout.getSelectedTabPosition() != position) {
497 // Create a handler to select the tab.
498 Handler selectTabHandler = new Handler();
500 // Create a runnable select the new tab.
501 Runnable selectTabRunnable = () -> {
502 // Get a handle for the tab.
503 TabLayout.Tab tab = tabLayout.getTabAt(position);
505 // Assert that the tab is not null.
512 // Select the tab layout after 100 milliseconds, which leaves enough time for a new tab to be created.
513 selectTabHandler.postDelayed(selectTabRunnable, 100);
518 public void onPageScrollStateChanged(int state) {
523 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
524 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
526 public void onTabSelected(TabLayout.Tab tab) {
527 // Select the same page in the view pager.
528 webViewPager.setCurrentItem(tab.getPosition());
532 public void onTabUnselected(TabLayout.Tab tab) {
537 public void onTabReselected(TabLayout.Tab tab) {
538 // Instantiate the View SSL Certificate dialog.
539 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
541 // Display the View SSL Certificate dialog.
542 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
546 // Add the first tab.
549 // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget.
550 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
552 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
553 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
554 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
555 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
557 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
558 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
559 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
560 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
563 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
564 launchBookmarksActivityFab.setOnClickListener(v -> {
565 // Get a copy of the favorite icon bitmap.
566 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
568 // Create a favorite icon byte array output stream.
569 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
571 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
572 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
574 // Convert the favorite icon byte array stream to a byte array.
575 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
577 // Create an intent to launch the bookmarks activity.
578 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
580 // Add the extra information to the intent.
581 bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
582 bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
583 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
584 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
587 startActivity(bookmarksIntent);
590 // Set the create new bookmark folder FAB to display an alert dialog.
591 createBookmarkFolderFab.setOnClickListener(v -> {
592 // Create a create bookmark folder dialog.
593 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
595 // Show the create bookmark folder dialog.
596 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
599 // Set the create new bookmark FAB to display an alert dialog.
600 createBookmarkFab.setOnClickListener(view -> {
601 // Instantiate the create bookmark dialog.
602 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
604 // Display the create bookmark dialog.
605 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
608 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
609 findOnPageEditText.addTextChangedListener(new TextWatcher() {
611 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
616 public void onTextChanged(CharSequence s, int start, int before, int count) {
621 public void afterTextChanged(Editable s) {
622 // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null.
623 if (currentWebView != null) {
624 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
629 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
630 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
631 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
632 // Hide the soft keyboard.
633 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
635 // Consume the event.
637 } else { // A different key was pressed.
638 // Do not consume the event.
643 // Implement swipe to refresh.
644 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
646 // Store the default progress view offsets for use later in `initializeWebView()`.
647 defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
648 defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
650 // Set the swipe to refresh color according to the theme.
652 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
653 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
655 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
658 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
659 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
660 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
662 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
663 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
665 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
666 currentBookmarksFolder = "";
668 // Load the home folder, which is `""` in the database.
669 loadBookmarksFolder();
671 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
672 // Convert the id from long to int to match the format of the bookmarks database.
673 int databaseID = (int) id;
675 // Get the bookmark cursor for this ID and move it to the first row.
676 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
677 bookmarkCursor.moveToFirst();
679 // Act upon the bookmark according to the type.
680 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
681 // Store the new folder name in `currentBookmarksFolder`.
682 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
684 // Load the new folder.
685 loadBookmarksFolder();
686 } else { // The selected bookmark is not a folder.
687 // Load the bookmark URL.
688 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
690 // Close the bookmarks drawer.
691 drawerLayout.closeDrawer(GravityCompat.END);
694 // Close the `Cursor`.
695 bookmarkCursor.close();
698 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
699 // Convert the database ID from `long` to `int`.
700 int databaseId = (int) id;
702 // Find out if the selected bookmark is a folder.
703 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
706 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
707 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
709 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
710 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
711 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
713 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
714 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
715 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
718 // Consume the event.
722 // Get the status bar pixel size.
723 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
724 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
726 // Get the resource density.
727 float screenDensity = getResources().getDisplayMetrics().density;
729 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
730 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
731 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
732 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
734 // The drawer listener is used to update the navigation menu.`
735 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
737 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
741 public void onDrawerOpened(@NonNull View drawerView) {
745 public void onDrawerClosed(@NonNull View drawerView) {
749 public void onDrawerStateChanged(int newState) {
750 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
751 // Get handles for the drawer headers.
752 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
753 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
755 // Apply the navigation header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts.
756 if (navigationHeaderTextView != null) {
757 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
760 // Apply the bookmarks header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts.
761 if (bookmarksHeaderTextView != null) {
762 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
765 // Update the navigation menu items.
766 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
767 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
768 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
769 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
770 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
772 // Hide the keyboard (if displayed).
773 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
775 // Clear the focus from from the URL text box and the WebView. This removes any text selection markers and context menus, which otherwise draw above the open drawers.
776 urlEditText.clearFocus();
777 currentWebView.clearFocus();
782 // Create the hamburger icon at the start of the AppBar.
783 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
785 // Replace the header that `WebView` creates for `X-Requested-With` with a null value. The default value is the application ID (com.stoutner.privacybrowser.standard).
786 customHeaders.put("X-Requested-With", "");
788 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
789 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
791 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
792 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
794 // Get a handle for the WebView.
795 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
797 // Store the default user agent.
798 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
800 // Destroy the bare WebView.
801 bareWebView.destroy();
805 protected void onNewIntent(Intent intent) {
806 // Get the information from the intent.
807 String intentAction = intent.getAction();
808 Uri intentUriData = intent.getData();
810 // Only process the URI if it contains data. If the user pressed the desktop icon after the app was already running the URI will be null.
811 if (intentUriData != null) {
812 // Sets the new intent as the activity intent, which replaces the one that originally started the app.
815 // Get the shared preferences.
816 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
818 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {
823 // Create a URL string.
826 // If the intent action is a web search, perform the search.
827 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
828 // Create an encoded URL string.
829 String encodedUrlString;
831 // Sanitize the search input and convert it to a search.
833 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
834 } catch (UnsupportedEncodingException exception) {
835 encodedUrlString = "";
838 // Add the base search URL.
839 url = searchURL + encodedUrlString;
840 } else { // The intent should contain a URL.
841 // Set the intent data as the URL.
842 url = intentUriData.toString();
848 // Get a handle for the drawer layout.
849 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
851 // Close the navigation drawer if it is open.
852 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
853 drawerLayout.closeDrawer(GravityCompat.START);
856 // Close the bookmarks drawer if it is open.
857 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
858 drawerLayout.closeDrawer(GravityCompat.END);
861 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
862 currentWebView.requestFocus();
867 public void onRestart() {
868 // Run the default commands.
871 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
872 if (proxyThroughOrbot) {
873 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
874 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
876 // Send the intent to the Orbot package.
877 orbotIntent.setPackage("org.torproject.android");
880 sendBroadcast(orbotIntent);
883 // Apply the app settings if returning from the Settings activity.
884 if (reapplyAppSettingsOnRestart) {
885 // Reset the reapply app settings on restart tracker.
886 reapplyAppSettingsOnRestart = false;
888 // Apply the app settings.
892 // Apply the domain settings if returning from the settings or domains activity.
893 if (reapplyDomainSettingsOnRestart) {
894 // Reset the reapply domain settings on restart tracker.
895 reapplyDomainSettingsOnRestart = false;
897 // Reapply the domain settings for each tab.
898 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
899 // Get the WebView tab fragment.
900 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
902 // Get the fragment view.
903 View fragmentView = webViewTabFragment.getView();
905 // Only reload the WebViews if they exist.
906 if (fragmentView != null) {
907 // Get the nested scroll WebView from the tab fragment.
908 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
910 // Reset the current domain name so the domain settings will be reapplied.
911 nestedScrollWebView.resetCurrentDomainName();
913 // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
914 if (nestedScrollWebView.getUrl() != null) {
915 applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
921 // Load the URL on restart (used when loading a bookmark).
922 if (loadUrlOnRestart) {
923 // Load the specified URL.
924 loadUrl(urlToLoadOnRestart);
926 // Reset the load on restart tracker.
927 loadUrlOnRestart = false;
930 // Update the bookmarks drawer if returning from the Bookmarks activity.
931 if (restartFromBookmarksActivity) {
932 // Get a handle for the drawer layout.
933 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
935 // Close the bookmarks drawer.
936 drawerLayout.closeDrawer(GravityCompat.END);
938 // Reload the bookmarks drawer.
939 loadBookmarksFolder();
941 // Reset `restartFromBookmarksActivity`.
942 restartFromBookmarksActivity = false;
945 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
946 updatePrivacyIcons(true);
949 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
951 public void onResume() {
952 // Run the default commands.
955 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
956 // Get the WebView tab fragment.
957 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
959 // Get the fragment view.
960 View fragmentView = webViewTabFragment.getView();
962 // Only resume the WebViews if they exist (they won't when the app is first created).
963 if (fragmentView != null) {
964 // Get the nested scroll WebView from the tab fragment.
965 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
967 // Resume the nested scroll WebView JavaScript timers.
968 nestedScrollWebView.resumeTimers();
970 // Resume the nested scroll WebView.
971 nestedScrollWebView.onResume();
975 // Display a message to the user if waiting for Orbot.
976 if (waitingForOrbot && !orbotStatus.equals("ON")) {
977 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
978 currentWebView.getSettings().setUseWideViewPort(false);
980 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
981 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
984 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
985 // Get a handle for the root frame layouts.
986 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
988 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
989 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
991 /* Hide the system bars.
992 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
993 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
994 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
995 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
997 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
998 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
999 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
1001 AdHelper.resumeAd(findViewById(R.id.adview));
1006 public void onPause() {
1007 // Run the default commands.
1010 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1011 // Get the WebView tab fragment.
1012 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1014 // Get the fragment view.
1015 View fragmentView = webViewTabFragment.getView();
1017 // Only pause the WebViews if they exist (they won't when the app is first created).
1018 if (fragmentView != null) {
1019 // Get the nested scroll WebView from the tab fragment.
1020 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1022 // Pause the nested scroll WebView.
1023 nestedScrollWebView.onPause();
1025 // Pause the nested scroll WebView JavaScript timers.
1026 nestedScrollWebView.pauseTimers();
1030 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1031 if (BuildConfig.FLAVOR.contentEquals("free")) {
1033 AdHelper.pauseAd(findViewById(R.id.adview));
1038 public void onDestroy() {
1039 // Unregister the Orbot status broadcast receiver.
1040 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1042 // Close the bookmarks cursor and database.
1043 bookmarksCursor.close();
1044 bookmarksDatabaseHelper.close();
1046 // Run the default commands.
1051 public boolean onCreateOptionsMenu(Menu menu) {
1052 // Inflate the menu. This adds items to the action bar if it is present.
1053 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1055 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
1058 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1059 updatePrivacyIcons(false);
1061 // Get handles for the menu items.
1062 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1063 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1064 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1065 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1066 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1067 MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
1068 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1070 // Only display third-party cookies if API >= 21
1071 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1073 // Only display the form data menu items if the API < 26.
1074 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1075 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1077 // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
1078 clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
1080 // Only show Ad Consent if this is the free flavor.
1081 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1083 // Get the shared preferences.
1084 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1086 // Get the dark theme and app bar preferences..
1087 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1088 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
1090 // Set the status of the additional app bar icons. Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
1091 if (displayAdditionalAppBarIcons) {
1092 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1093 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1094 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1095 } else { //Do not display the additional icons.
1096 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1097 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1098 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1101 // Replace Refresh with Stop if a URL is already loading.
1102 if (currentWebView != null && currentWebView.getProgress() != 100) {
1104 refreshMenuItem.setTitle(R.string.stop);
1106 // If the icon is displayed in the AppBar, set it according to the theme.
1107 if (displayAdditionalAppBarIcons) {
1109 refreshMenuItem.setIcon(R.drawable.close_dark);
1111 refreshMenuItem.setIcon(R.drawable.close_light);
1120 public boolean onPrepareOptionsMenu(Menu menu) {
1121 // Get handles for the menu items.
1122 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1123 MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1124 MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1125 MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1126 MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1127 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1128 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1129 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1130 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1131 MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
1132 MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
1133 MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1134 MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1135 MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1136 MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1137 MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1138 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1139 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1140 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1141 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1142 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1144 // Get a handle for the cookie manager.
1145 CookieManager cookieManager = CookieManager.getInstance();
1147 // Initialize the current user agent string and the font size.
1148 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1151 // Set items that require the current web view to be populated. It will be null when the program is first opened, as `onPrepareOptionsMenu()` is called before the first WebView is initialized.
1152 if (currentWebView != null) {
1153 // Set the add or edit domain text.
1154 if (currentWebView.getDomainSettingsApplied()) {
1155 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1157 addOrEditDomain.setTitle(R.string.add_domain_settings);
1160 // Get the current user agent from the WebView.
1161 currentUserAgent = currentWebView.getSettings().getUserAgentString();
1163 // Get the current font size from the
1164 fontSize = currentWebView.getSettings().getTextZoom();
1166 // Set the status of the menu item checkboxes.
1167 domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1168 saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26.
1169 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1170 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1171 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1172 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1173 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1174 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1175 swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1176 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1177 nightModeMenuItem.setChecked(currentWebView.getNightMode());
1179 // Initialize the display names for the blocklists with the number of blocked requests.
1180 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1181 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1182 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1183 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1184 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1185 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1186 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1188 // Only modify third-party cookies if the API >= 21.
1189 if (Build.VERSION.SDK_INT >= 21) {
1190 // Set the status of the third-party cookies checkbox.
1191 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1193 // Enable third-party cookies if first-party cookies are enabled.
1194 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
1197 // Enable DOM Storage if JavaScript is enabled.
1198 domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1201 // Set the status of the menu item checkboxes.
1202 firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1203 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1205 // Enable Clear Cookies if there are any.
1206 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1208 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
1209 String privateDataDirectoryString = getApplicationInfo().dataDir;
1211 // Get a count of the number of files in the Local Storage directory.
1212 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1213 int localStorageDirectoryNumberOfFiles = 0;
1214 if (localStorageDirectory.exists()) {
1215 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1218 // Get a count of the number of files in the IndexedDB directory.
1219 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1220 int indexedDBDirectoryNumberOfFiles = 0;
1221 if (indexedDBDirectory.exists()) {
1222 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1225 // Enable Clear DOM Storage if there is any.
1226 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1228 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1229 if (Build.VERSION.SDK_INT < 26) {
1230 // Get the WebView database.
1231 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1233 // Enable the clear form data menu item if there is anything to clear.
1234 clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1237 // Enable Clear Data if any of the submenu items are enabled.
1238 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1240 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1241 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1243 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1244 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1245 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1246 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1247 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1248 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1249 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1250 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1251 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1252 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1253 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1254 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1255 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1256 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1257 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1258 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1259 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1260 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1261 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1262 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1263 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1264 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1265 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1266 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1267 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1268 } else { // Custom user agent.
1269 menu.findItem(R.id.user_agent_custom).setChecked(true);
1272 // Instantiate the font size title and the selected font size menu item.
1273 String fontSizeTitle;
1274 MenuItem selectedFontSizeMenuItem;
1276 // Prepare the font size title and current size menu item.
1279 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1280 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1284 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1285 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1289 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1290 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1294 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1295 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1299 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1300 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1304 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1305 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1309 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1310 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1314 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1315 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1319 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1320 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1324 // Set the font size title and select the current size menu item.
1325 fontSizeMenuItem.setTitle(fontSizeTitle);
1326 selectedFontSizeMenuItem.setChecked(true);
1328 // Run all the other default commands.
1329 super.onPrepareOptionsMenu(menu);
1331 // Display the menu.
1336 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1337 @SuppressLint("SetJavaScriptEnabled")
1338 public boolean onOptionsItemSelected(MenuItem menuItem) {
1339 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1340 if (inFullScreenBrowsingMode) {
1341 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1342 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1344 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1346 /* Hide the system bars.
1347 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1348 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1349 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1350 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1352 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1353 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1356 // Get the selected menu item ID.
1357 int menuItemId = menuItem.getItemId();
1359 // Get a handle for the shared preferences.
1360 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1362 // Get a handle for the cookie manager.
1363 CookieManager cookieManager = CookieManager.getInstance();
1365 // Run the commands that correlate to the selected menu item.
1366 switch (menuItemId) {
1367 case R.id.toggle_javascript:
1368 // Toggle the JavaScript status.
1369 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1371 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1372 updatePrivacyIcons(true);
1374 // Display a `Snackbar`.
1375 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
1376 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1377 } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled.
1378 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1379 } else { // Privacy mode.
1380 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1383 // Reload the current WebView.
1384 currentWebView.reload();
1387 case R.id.add_or_edit_domain:
1388 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1389 // Reapply the domain settings on returning to `MainWebViewActivity`.
1390 reapplyDomainSettingsOnRestart = true;
1392 // Create an intent to launch the domains activity.
1393 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1395 // Add the extra information to the intent.
1396 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1397 domainsIntent.putExtra("close_on_back", true);
1398 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1400 // Get the current certificate.
1401 SslCertificate sslCertificate = currentWebView.getCertificate();
1403 // Check to see if the SSL certificate is populated.
1404 if (sslCertificate != null) {
1405 // Extract the certificate to strings.
1406 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1407 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1408 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1409 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1410 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1411 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1412 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1413 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1415 // Add the certificate to the intent.
1416 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1417 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1418 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1419 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1420 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1421 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1422 domainsIntent.putExtra("ssl_start_date", startDateLong);
1423 domainsIntent.putExtra("ssl_end_date", endDateLong);
1426 // Check to see if the current IP addresses have been received.
1427 if (currentWebView.hasCurrentIpAddresses()) {
1428 // Add the current IP addresses to the intent.
1429 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1433 startActivity(domainsIntent);
1434 } else { // Add a new domain.
1435 // Apply the new domain settings on returning to `MainWebViewActivity`.
1436 reapplyDomainSettingsOnRestart = true;
1438 // Get the current domain
1439 Uri currentUri = Uri.parse(currentWebView.getUrl());
1440 String currentDomain = currentUri.getHost();
1442 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1443 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1445 // Create the domain and store the database ID.
1446 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1448 // Create an intent to launch the domains activity.
1449 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1451 // Add the extra information to the intent.
1452 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1453 domainsIntent.putExtra("close_on_back", true);
1454 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1456 // Get the current certificate.
1457 SslCertificate sslCertificate = currentWebView.getCertificate();
1459 // Check to see if the SSL certificate is populated.
1460 if (sslCertificate != null) {
1461 // Extract the certificate to strings.
1462 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1463 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1464 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1465 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1466 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1467 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1468 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1469 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1471 // Add the certificate to the intent.
1472 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1473 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1474 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1475 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1476 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1477 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1478 domainsIntent.putExtra("ssl_start_date", startDateLong);
1479 domainsIntent.putExtra("ssl_end_date", endDateLong);
1482 // Check to see if the current IP addresses have been received.
1483 if (currentWebView.hasCurrentIpAddresses()) {
1484 // Add the current IP addresses to the intent.
1485 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1489 startActivity(domainsIntent);
1493 case R.id.toggle_first_party_cookies:
1494 // Switch the first-party cookie status.
1495 cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1497 // Store the first-party cookie status.
1498 currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1500 // Update the menu checkbox.
1501 menuItem.setChecked(cookieManager.acceptCookie());
1503 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1504 updatePrivacyIcons(true);
1506 // Display a snackbar.
1507 if (cookieManager.acceptCookie()) { // First-party cookies are enabled.
1508 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1509 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1510 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1511 } else { // Privacy mode.
1512 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1515 // Reload the current WebView.
1516 currentWebView.reload();
1519 case R.id.toggle_third_party_cookies:
1520 if (Build.VERSION.SDK_INT >= 21) {
1521 // Switch the status of thirdPartyCookiesEnabled.
1522 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1524 // Update the menu checkbox.
1525 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1527 // Display a snackbar.
1528 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1529 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1531 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1534 // Reload the current WebView.
1535 currentWebView.reload();
1536 } // Else do nothing because SDK < 21.
1539 case R.id.toggle_dom_storage:
1540 // Toggle the status of domStorageEnabled.
1541 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1543 // Update the menu checkbox.
1544 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1546 // Update the privacy icon. `true` refreshes the app bar icons.
1547 updatePrivacyIcons(true);
1549 // Display a snackbar.
1550 if (currentWebView.getSettings().getDomStorageEnabled()) {
1551 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1553 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1556 // Reload the current WebView.
1557 currentWebView.reload();
1560 // Form data can be removed once the minimum API >= 26.
1561 case R.id.toggle_save_form_data:
1562 // Switch the status of saveFormDataEnabled.
1563 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1565 // Update the menu checkbox.
1566 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1568 // Display a snackbar.
1569 if (currentWebView.getSettings().getSaveFormData()) {
1570 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1572 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1575 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1576 updatePrivacyIcons(true);
1578 // Reload the current WebView.
1579 currentWebView.reload();
1582 case R.id.clear_cookies:
1583 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1584 .setAction(R.string.undo, v -> {
1585 // Do nothing because everything will be handled by `onDismissed()` below.
1587 .addCallback(new Snackbar.Callback() {
1588 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1590 public void onDismissed(Snackbar snackbar, int event) {
1591 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1592 // Delete the cookies, which command varies by SDK.
1593 if (Build.VERSION.SDK_INT < 21) {
1594 cookieManager.removeAllCookie();
1596 cookieManager.removeAllCookies(null);
1604 case R.id.clear_dom_storage:
1605 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1606 .setAction(R.string.undo, v -> {
1607 // Do nothing because everything will be handled by `onDismissed()` below.
1609 .addCallback(new Snackbar.Callback() {
1610 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1612 public void onDismissed(Snackbar snackbar, int event) {
1613 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1614 // Delete the DOM Storage.
1615 WebStorage webStorage = WebStorage.getInstance();
1616 webStorage.deleteAllData();
1618 // Initialize a handler to manually delete the DOM storage files and directories.
1619 Handler deleteDomStorageHandler = new Handler();
1621 // Setup a runnable to manually delete the DOM storage files and directories.
1622 Runnable deleteDomStorageRunnable = () -> {
1624 // Get a handle for the runtime.
1625 Runtime runtime = Runtime.getRuntime();
1627 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1628 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1629 String privateDataDirectoryString = getApplicationInfo().dataDir;
1631 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1632 Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1634 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1635 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1636 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1637 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1638 Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1640 // Wait for the processes to finish.
1641 deleteLocalStorageProcess.waitFor();
1642 deleteIndexProcess.waitFor();
1643 deleteQuotaManagerProcess.waitFor();
1644 deleteQuotaManagerJournalProcess.waitFor();
1645 deleteDatabasesProcess.waitFor();
1646 } catch (Exception exception) {
1647 // Do nothing if an error is thrown.
1651 // Manually delete the DOM storage files after 200 milliseconds.
1652 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1659 // Form data can be remove once the minimum API >= 26.
1660 case R.id.clear_form_data:
1661 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1662 .setAction(R.string.undo, v -> {
1663 // Do nothing because everything will be handled by `onDismissed()` below.
1665 .addCallback(new Snackbar.Callback() {
1666 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1668 public void onDismissed(Snackbar snackbar, int event) {
1669 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1670 // Delete the form data.
1671 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1672 mainWebViewDatabase.clearFormData();
1680 // Toggle the EasyList status.
1681 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1683 // Update the menu checkbox.
1684 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1686 // Reload the current WebView.
1687 currentWebView.reload();
1690 case R.id.easyprivacy:
1691 // Toggle the EasyPrivacy status.
1692 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1694 // Update the menu checkbox.
1695 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1697 // Reload the current WebView.
1698 currentWebView.reload();
1701 case R.id.fanboys_annoyance_list:
1702 // Toggle Fanboy's Annoyance List status.
1703 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1705 // Update the menu checkbox.
1706 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1708 // Update the staus of Fanboy's Social Blocking List.
1709 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1710 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1712 // Reload the current WebView.
1713 currentWebView.reload();
1716 case R.id.fanboys_social_blocking_list:
1717 // Toggle Fanboy's Social Blocking List status.
1718 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1720 // Update the menu checkbox.
1721 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1723 // Reload the current WebView.
1724 currentWebView.reload();
1727 case R.id.ultraprivacy:
1728 // Toggle the UltraPrivacy status.
1729 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1731 // Update the menu checkbox.
1732 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1734 // Reload the current WebView.
1735 currentWebView.reload();
1738 case R.id.block_all_third_party_requests:
1739 //Toggle the third-party requests blocker status.
1740 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1742 // Update the menu checkbox.
1743 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1745 // Reload the current WebView.
1746 currentWebView.reload();
1749 case R.id.user_agent_privacy_browser:
1750 // Update the user agent.
1751 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1753 // Reload the current WebView.
1754 currentWebView.reload();
1757 case R.id.user_agent_webview_default:
1758 // Update the user agent.
1759 currentWebView.getSettings().setUserAgentString("");
1761 // Reload the current WebView.
1762 currentWebView.reload();
1765 case R.id.user_agent_firefox_on_android:
1766 // Update the user agent.
1767 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1769 // Reload the current WebView.
1770 currentWebView.reload();
1773 case R.id.user_agent_chrome_on_android:
1774 // Update the user agent.
1775 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1777 // Reload the current WebView.
1778 currentWebView.reload();
1781 case R.id.user_agent_safari_on_ios:
1782 // Update the user agent.
1783 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1785 // Reload the current WebView.
1786 currentWebView.reload();
1789 case R.id.user_agent_firefox_on_linux:
1790 // Update the user agent.
1791 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1793 // Reload the current WebView.
1794 currentWebView.reload();
1797 case R.id.user_agent_chromium_on_linux:
1798 // Update the user agent.
1799 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1801 // Reload the current WebView.
1802 currentWebView.reload();
1805 case R.id.user_agent_firefox_on_windows:
1806 // Update the user agent.
1807 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1809 // Reload the current WebView.
1810 currentWebView.reload();
1813 case R.id.user_agent_chrome_on_windows:
1814 // Update the user agent.
1815 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1817 // Reload the current WebView.
1818 currentWebView.reload();
1821 case R.id.user_agent_edge_on_windows:
1822 // Update the user agent.
1823 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1825 // Reload the current WebView.
1826 currentWebView.reload();
1829 case R.id.user_agent_internet_explorer_on_windows:
1830 // Update the user agent.
1831 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1833 // Reload the current WebView.
1834 currentWebView.reload();
1837 case R.id.user_agent_safari_on_macos:
1838 // Update the user agent.
1839 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1841 // Reload the current WebView.
1842 currentWebView.reload();
1845 case R.id.user_agent_custom:
1846 // Update the user agent.
1847 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1849 // Reload the current WebView.
1850 currentWebView.reload();
1853 case R.id.font_size_twenty_five_percent:
1854 currentWebView.getSettings().setTextZoom(25);
1857 case R.id.font_size_fifty_percent:
1858 currentWebView.getSettings().setTextZoom(50);
1861 case R.id.font_size_seventy_five_percent:
1862 currentWebView.getSettings().setTextZoom(75);
1865 case R.id.font_size_one_hundred_percent:
1866 currentWebView.getSettings().setTextZoom(100);
1869 case R.id.font_size_one_hundred_twenty_five_percent:
1870 currentWebView.getSettings().setTextZoom(125);
1873 case R.id.font_size_one_hundred_fifty_percent:
1874 currentWebView.getSettings().setTextZoom(150);
1877 case R.id.font_size_one_hundred_seventy_five_percent:
1878 currentWebView.getSettings().setTextZoom(175);
1881 case R.id.font_size_two_hundred_percent:
1882 currentWebView.getSettings().setTextZoom(200);
1885 case R.id.swipe_to_refresh:
1886 // Toggle the stored status of swipe to refresh.
1887 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1889 // Get a handle for the swipe refresh layout.
1890 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1892 // Update the swipe refresh layout.
1893 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
1894 if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, the status of the scroll refresh listener is continuously updated by the on scroll change listener.
1895 // Only enable the swipe refresh layout if the WebView is scrolled to the top.
1896 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1897 } else { // For API < 23, the swipe refresh layout is always enabled.
1898 // Enable the swipe refresh layout.
1899 swipeRefreshLayout.setEnabled(true);
1901 } else { // Swipe to refresh is disabled.
1902 // Disable the swipe refresh layout.
1903 swipeRefreshLayout.setEnabled(false);
1907 case R.id.display_images:
1908 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1909 // Disable loading of images.
1910 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1912 // Reload the website to remove existing images.
1913 currentWebView.reload();
1914 } else { // Images are not currently loaded automatically.
1915 // Enable loading of images. Missing images will be loaded without the need for a reload.
1916 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1920 case R.id.night_mode:
1921 // Toggle night mode.
1922 currentWebView.setNightMode(!currentWebView.getNightMode());
1924 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1925 if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript.
1926 // Enable JavaScript.
1927 currentWebView.getSettings().setJavaScriptEnabled(true);
1928 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
1929 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1930 currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
1931 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
1932 // Apply the JavaScript preference.
1933 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1936 // Update the privacy icons.
1937 updatePrivacyIcons(false);
1939 // Reload the website.
1940 currentWebView.reload();
1943 case R.id.find_on_page:
1944 // Get a handle for the views.
1945 Toolbar toolbar = findViewById(R.id.toolbar);
1946 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1947 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1949 // Set the minimum height of the find on page linear layout to match the toolbar.
1950 findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1952 // Hide the toolbar.
1953 toolbar.setVisibility(View.GONE);
1955 // Show the find on page linear layout.
1956 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1958 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1959 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1960 findOnPageEditText.postDelayed(() -> {
1961 // Set the focus on `findOnPageEditText`.
1962 findOnPageEditText.requestFocus();
1964 // Get a handle for the input method manager.
1965 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1967 // Remove the lint warning below that the input method manager might be null.
1968 assert inputMethodManager != null;
1970 // Display the keyboard. `0` sets no input flags.
1971 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1975 case R.id.view_source:
1976 // Create an intent to launch the view source activity.
1977 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1979 // Add the variables to the intent.
1980 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1981 viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
1984 startActivity(viewSourceIntent);
1987 case R.id.share_url:
1988 // Setup the share string.
1989 String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1991 // Create the share intent.
1992 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1993 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1994 shareIntent.setType("text/plain");
1997 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2001 // Get a print manager instance.
2002 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2004 // Remove the lint error below that print manager might be null.
2005 assert printManager != null;
2007 // Create a print document adapter from the current WebView.
2008 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2010 // Print the document.
2011 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2014 case R.id.open_with_app:
2015 openWithApp(currentWebView.getUrl());
2018 case R.id.open_with_browser:
2019 openWithBrowser(currentWebView.getUrl());
2022 case R.id.add_to_homescreen:
2023 // Instantiate the create home screen shortcut dialog.
2024 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
2025 currentWebView.getFavoriteOrDefaultIcon());
2027 // Show the create home screen shortcut dialog.
2028 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2031 case R.id.proxy_through_orbot:
2032 // Toggle the proxy through Orbot variable.
2033 proxyThroughOrbot = !proxyThroughOrbot;
2035 // Apply the proxy through Orbot settings.
2036 applyProxyThroughOrbot(true);
2040 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2041 // Reload the current WebView.
2042 currentWebView.reload();
2043 } else { // The stop button was pushed.
2044 // Stop the loading of the WebView.
2045 currentWebView.stopLoading();
2049 case R.id.ad_consent:
2050 // Display the ad consent dialog.
2051 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2052 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2056 // Don't consume the event.
2057 return super.onOptionsItemSelected(menuItem);
2061 // removeAllCookies is deprecated, but it is required for API < 21.
2063 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2064 // Get the menu item ID.
2065 int menuItemId = menuItem.getItemId();
2067 // Get a handle for the shared preferences.
2068 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2070 // Run the commands that correspond to the selected menu item.
2071 switch (menuItemId) {
2072 case R.id.close_tab:
2073 // Get a handle for the tab layout and the view pager.
2074 TabLayout tabLayout = findViewById(R.id.tablayout);
2075 ViewPager webViewPager = findViewById(R.id.webviewpager);
2077 // Get the current tab number.
2078 int currentTabNumber = tabLayout.getSelectedTabPosition();
2080 // Delete the current tab.
2081 tabLayout.removeTabAt(currentTabNumber);
2083 // Delete the current page. If the selected page number did not change during the delete, it will return true, meaning that the current WebView must be reset.
2084 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
2085 setCurrentWebView(currentTabNumber);
2089 case R.id.clear_and_exit:
2090 // Close the bookmarks cursor and database.
2091 bookmarksCursor.close();
2092 bookmarksDatabaseHelper.close();
2094 // Get the status of the clear everything preference.
2095 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2097 // Get a handle for the runtime.
2098 Runtime runtime = Runtime.getRuntime();
2100 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
2101 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
2102 String privateDataDirectoryString = getApplicationInfo().dataDir;
2105 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2106 // The command to remove cookies changed slightly in API 21.
2107 if (Build.VERSION.SDK_INT >= 21) {
2108 CookieManager.getInstance().removeAllCookies(null);
2110 CookieManager.getInstance().removeAllCookie();
2113 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2115 // Two commands must be used because `Runtime.exec()` does not like `*`.
2116 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2117 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2119 // Wait until the processes have finished.
2120 deleteCookiesProcess.waitFor();
2121 deleteCookiesJournalProcess.waitFor();
2122 } catch (Exception exception) {
2123 // Do nothing if an error is thrown.
2127 // Clear DOM storage.
2128 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2129 // Ask `WebStorage` to clear the DOM storage.
2130 WebStorage webStorage = WebStorage.getInstance();
2131 webStorage.deleteAllData();
2133 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2135 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2136 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2138 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2139 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2140 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2141 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2142 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2144 // Wait until the processes have finished.
2145 deleteLocalStorageProcess.waitFor();
2146 deleteIndexProcess.waitFor();
2147 deleteQuotaManagerProcess.waitFor();
2148 deleteQuotaManagerJournalProcess.waitFor();
2149 deleteDatabaseProcess.waitFor();
2150 } catch (Exception exception) {
2151 // Do nothing if an error is thrown.
2155 // Clear form data if the API < 26.
2156 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2157 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2158 webViewDatabase.clearFormData();
2160 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2162 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2163 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2164 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2166 // Wait until the processes have finished.
2167 deleteWebDataProcess.waitFor();
2168 deleteWebDataJournalProcess.waitFor();
2169 } catch (Exception exception) {
2170 // Do nothing if an error is thrown.
2175 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2176 // Clear the cache from each WebView.
2177 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2178 // Get the WebView tab fragment.
2179 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2181 // Get the fragment view.
2182 View fragmentView = webViewTabFragment.getView();
2184 // Only clear the cache if the WebView exists.
2185 if (fragmentView != null) {
2186 // Get the nested scroll WebView from the tab fragment.
2187 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2189 // Clear the cache for this WebView.
2190 nestedScrollWebView.clearCache(true);
2194 // Manually delete the cache directories.
2196 // Delete the main cache directory.
2197 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2199 // Delete the secondary `Service Worker` cache directory.
2200 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2201 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2203 // Wait until the processes have finished.
2204 deleteCacheProcess.waitFor();
2205 deleteServiceWorkerProcess.waitFor();
2206 } catch (Exception exception) {
2207 // Do nothing if an error is thrown.
2211 // Wipe out each WebView.
2212 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2213 // Get the WebView tab fragment.
2214 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2216 // Get the fragment view.
2217 View fragmentView = webViewTabFragment.getView();
2219 // Only wipe out the WebView if it exists.
2220 if (fragmentView != null) {
2221 // Get the nested scroll WebView from the tab fragment.
2222 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2224 // Clear SSL certificate preferences for this WebView.
2225 nestedScrollWebView.clearSslPreferences();
2227 // Clear the back/forward history for this WebView.
2228 nestedScrollWebView.clearHistory();
2230 // Destroy the internal state of `mainWebView`.
2231 nestedScrollWebView.destroy();
2235 // Clear the custom headers.
2236 customHeaders.clear();
2238 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2239 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2240 if (clearEverything) {
2242 // Delete the folder.
2243 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2245 // Wait until the process has finished.
2246 deleteAppWebviewProcess.waitFor();
2247 } catch (Exception exception) {
2248 // Do nothing if an error is thrown.
2252 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2253 if (Build.VERSION.SDK_INT >= 21) {
2254 finishAndRemoveTask();
2259 // Remove the terminated program from RAM. The status code is `0`.
2264 // Select the homepage based on the proxy through Orbot status.
2265 if (proxyThroughOrbot) {
2266 // Load the Tor homepage.
2267 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
2269 // Load the normal homepage.
2270 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2275 if (currentWebView.canGoBack()) {
2276 // Reset the current domain name so that navigation works if third-party requests are blocked.
2277 currentWebView.resetCurrentDomainName();
2279 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2280 currentWebView.setNavigatingHistory(true);
2282 // Load the previous website in the history.
2283 currentWebView.goBack();
2288 if (currentWebView.canGoForward()) {
2289 // Reset the current domain name so that navigation works if third-party requests are blocked.
2290 currentWebView.resetCurrentDomainName();
2292 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2293 currentWebView.setNavigatingHistory(true);
2295 // Load the next website in the history.
2296 currentWebView.goForward();
2301 // Instantiate the URL history dialog.
2302 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2304 // Show the URL history dialog.
2305 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2309 // Populate the resource requests.
2310 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2312 // Create an intent to launch the Requests activity.
2313 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2315 // Add the block third-party requests status to the intent.
2316 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2319 startActivity(requestsIntent);
2322 case R.id.downloads:
2323 // Launch the system Download Manager.
2324 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2326 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2327 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2329 startActivity(downloadManagerIntent);
2333 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2334 reapplyDomainSettingsOnRestart = true;
2336 // Launch the domains activity.
2337 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2339 // Add the extra information to the intent.
2340 domainsIntent.putExtra("current_url", currentWebView.getUrl());
2342 // Get the current certificate.
2343 SslCertificate sslCertificate = currentWebView.getCertificate();
2345 // Check to see if the SSL certificate is populated.
2346 if (sslCertificate != null) {
2347 // Extract the certificate to strings.
2348 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2349 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2350 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2351 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2352 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2353 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2354 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2355 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2357 // Add the certificate to the intent.
2358 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2359 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2360 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2361 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2362 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2363 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2364 domainsIntent.putExtra("ssl_start_date", startDateLong);
2365 domainsIntent.putExtra("ssl_end_date", endDateLong);
2368 // Check to see if the current IP addresses have been received.
2369 if (currentWebView.hasCurrentIpAddresses()) {
2370 // Add the current IP addresses to the intent.
2371 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2375 startActivity(domainsIntent);
2379 // Set the flag to reapply app settings on restart when returning from Settings.
2380 reapplyAppSettingsOnRestart = true;
2382 // Set the flag to reapply the domain settings on restart when returning from Settings.
2383 reapplyDomainSettingsOnRestart = true;
2385 // Launch the settings activity.
2386 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2387 startActivity(settingsIntent);
2390 case R.id.import_export:
2391 // Launch the import/export activity.
2392 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2393 startActivity(importExportIntent);
2397 // Launch the logcat activity.
2398 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2399 startActivity(logcatIntent);
2403 // Launch `GuideActivity`.
2404 Intent guideIntent = new Intent(this, GuideActivity.class);
2405 startActivity(guideIntent);
2409 // Create an intent to launch the about activity.
2410 Intent aboutIntent = new Intent(this, AboutActivity.class);
2412 // Create a string array for the blocklist versions.
2413 String[] blocklistVersions = new String[] {easyList.get(0).get(0)[0], easyPrivacy.get(0).get(0)[0], fanboysAnnoyanceList.get(0).get(0)[0], fanboysSocialList.get(0).get(0)[0],
2414 ultraPrivacy.get(0).get(0)[0]};
2416 // Add the blocklist versions to the intent.
2417 aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2420 startActivity(aboutIntent);
2424 // Get a handle for the drawer layout.
2425 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2427 // Close the navigation drawer.
2428 drawerLayout.closeDrawer(GravityCompat.START);
2433 public void onPostCreate(Bundle savedInstanceState) {
2434 // Run the default commands.
2435 super.onPostCreate(savedInstanceState);
2437 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2438 actionBarDrawerToggle.syncState();
2442 public void onConfigurationChanged(Configuration newConfig) {
2443 // Run the default commands.
2444 super.onConfigurationChanged(newConfig);
2446 // Get the status bar pixel size.
2447 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2448 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2450 // Get the resource density.
2451 float screenDensity = getResources().getDisplayMetrics().density;
2453 // Recalculate the drawer header padding.
2454 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2455 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2456 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2458 // Reload the ad for the free flavor if not in full screen mode.
2459 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2460 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2461 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2464 // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
2465 // https://code.google.com/p/android/issues/detail?id=20493#c8
2466 // ActivityCompat.invalidateOptionsMenu(this);
2470 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2471 // Store the hit test result.
2472 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2474 // Create the URL strings.
2475 final String imageUrl;
2476 final String linkUrl;
2478 // Get handles for the system managers.
2479 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2480 FragmentManager fragmentManager = getSupportFragmentManager();
2481 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2483 // Remove the lint errors below that the clipboard manager might be null.
2484 assert clipboardManager != null;
2486 // Process the link according to the type.
2487 switch (hitTestResult.getType()) {
2488 // `SRC_ANCHOR_TYPE` is a link.
2489 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2490 // Get the target URL.
2491 linkUrl = hitTestResult.getExtra();
2493 // Set the target URL as the title of the `ContextMenu`.
2494 menu.setHeaderTitle(linkUrl);
2496 // Add a Load URL entry.
2497 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2506 // Add an Open with App entry.
2507 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2508 openWithApp(linkUrl);
2512 // Add an Open with Browser entry.
2513 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2514 openWithBrowser(linkUrl);
2518 // Add a Copy URL entry.
2519 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2520 // Save the link URL in a `ClipData`.
2521 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2523 // Set the `ClipData` as the clipboard's primary clip.
2524 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2528 // Add a Download URL entry.
2529 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2530 // Check if the download should be processed by an external app.
2531 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2532 openUrlWithExternalApp(linkUrl);
2533 } else { // Download with Android's download manager.
2534 // Check to see if the storage permission has already been granted.
2535 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2536 // Store the variables for future use by `onRequestPermissionsResult()`.
2537 downloadUrl = linkUrl;
2538 downloadContentDisposition = "none";
2539 downloadContentLength = -1;
2541 // Show a dialog if the user has previously denied the permission.
2542 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2543 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2544 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2546 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2547 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2548 } else { // Show the permission request directly.
2549 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2550 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2552 } else { // The storage permission has already been granted.
2553 // Get a handle for the download file alert dialog.
2554 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2556 // Show the download file alert dialog.
2557 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2563 // Add a Cancel entry, which by default closes the context menu.
2564 menu.add(R.string.cancel);
2567 case WebView.HitTestResult.EMAIL_TYPE:
2568 // Get the target URL.
2569 linkUrl = hitTestResult.getExtra();
2571 // Set the target URL as the title of the `ContextMenu`.
2572 menu.setHeaderTitle(linkUrl);
2574 // Add a Write Email entry.
2575 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2576 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2577 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2579 // Parse the url and set it as the data for the `Intent`.
2580 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2582 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2583 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2586 startActivity(emailIntent);
2590 // Add a Copy Email Address entry.
2591 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2592 // Save the email address in a `ClipData`.
2593 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2595 // Set the `ClipData` as the clipboard's primary clip.
2596 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2600 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2601 menu.add(R.string.cancel);
2604 // `IMAGE_TYPE` is an image.
2605 case WebView.HitTestResult.IMAGE_TYPE:
2606 // Get the image URL.
2607 imageUrl = hitTestResult.getExtra();
2609 // Set the image URL as the title of the `ContextMenu`.
2610 menu.setHeaderTitle(imageUrl);
2612 // Add a View Image entry.
2613 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2618 // Add a Download Image entry.
2619 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2620 // Check if the download should be processed by an external app.
2621 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2622 openUrlWithExternalApp(imageUrl);
2623 } else { // Download with Android's download manager.
2624 // Check to see if the storage permission has already been granted.
2625 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2626 // Store the image URL for use by `onRequestPermissionResult()`.
2627 downloadImageUrl = imageUrl;
2629 // Show a dialog if the user has previously denied the permission.
2630 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2631 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2632 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2634 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2635 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2636 } else { // Show the permission request directly.
2637 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2638 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2640 } else { // The storage permission has already been granted.
2641 // Get a handle for the download image alert dialog.
2642 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2644 // Show the download image alert dialog.
2645 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2651 // Add a Copy URL entry.
2652 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2653 // Save the image URL in a `ClipData`.
2654 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2656 // Set the `ClipData` as the clipboard's primary clip.
2657 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2661 // Add an Open with App entry.
2662 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2663 openWithApp(imageUrl);
2667 // Add an Open with Browser entry.
2668 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2669 openWithBrowser(imageUrl);
2673 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2674 menu.add(R.string.cancel);
2678 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2679 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2680 // Get the image URL.
2681 imageUrl = hitTestResult.getExtra();
2683 // Set the image URL as the title of the `ContextMenu`.
2684 menu.setHeaderTitle(imageUrl);
2686 // Add a `View Image` entry.
2687 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2692 // Add a `Download Image` entry.
2693 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2694 // Check if the download should be processed by an external app.
2695 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2696 openUrlWithExternalApp(imageUrl);
2697 } else { // Download with Android's download manager.
2698 // Check to see if the storage permission has already been granted.
2699 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2700 // Store the image URL for use by `onRequestPermissionResult()`.
2701 downloadImageUrl = imageUrl;
2703 // Show a dialog if the user has previously denied the permission.
2704 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2705 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2706 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2708 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2709 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2710 } else { // Show the permission request directly.
2711 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2712 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2714 } else { // The storage permission has already been granted.
2715 // Get a handle for the download image alert dialog.
2716 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2718 // Show the download image alert dialog.
2719 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2725 // Add a `Copy URL` entry.
2726 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2727 // Save the image URL in a `ClipData`.
2728 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2730 // Set the `ClipData` as the clipboard's primary clip.
2731 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2735 // Add an Open with App entry.
2736 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2737 openWithApp(imageUrl);
2741 // Add an Open with Browser entry.
2742 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2743 openWithBrowser(imageUrl);
2747 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2748 menu.add(R.string.cancel);
2754 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2755 // Get a handle for the bookmarks list view.
2756 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2758 // Get the views from the dialog fragment.
2759 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2760 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2762 // Extract the strings from the edit texts.
2763 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2764 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2766 // Create a favorite icon byte array output stream.
2767 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2769 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2770 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2772 // Convert the favorite icon byte array stream to a byte array.
2773 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2775 // Display the new bookmark below the current items in the (0 indexed) list.
2776 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2778 // Create the bookmark.
2779 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2781 // Update the bookmarks cursor with the current contents of this folder.
2782 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2784 // Update the list view.
2785 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2787 // Scroll to the new bookmark.
2788 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2792 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2793 // Get a handle for the bookmarks list view.
2794 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2796 // Get handles for the views in the dialog fragment.
2797 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2798 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2799 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2801 // Get new folder name string.
2802 String folderNameString = createFolderNameEditText.getText().toString();
2804 // Create a folder icon bitmap.
2805 Bitmap folderIconBitmap;
2807 // Set the folder icon bitmap according to the dialog.
2808 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2809 // Get the default folder icon drawable.
2810 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2812 // Convert the folder icon drawable to a bitmap drawable.
2813 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2815 // Convert the folder icon bitmap drawable to a bitmap.
2816 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2817 } else { // Use the WebView favorite icon.
2818 // Copy the favorite icon bitmap to the folder icon bitmap.
2819 folderIconBitmap = favoriteIconBitmap;
2822 // Create a folder icon byte array output stream.
2823 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2825 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2826 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2828 // Convert the folder icon byte array stream to a byte array.
2829 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2831 // Move all the bookmarks down one in the display order.
2832 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2833 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2834 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2837 // Create the folder, which will be placed at the top of the `ListView`.
2838 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2840 // Update the bookmarks cursor with the current contents of this folder.
2841 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2843 // Update the `ListView`.
2844 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2846 // Scroll to the new folder.
2847 bookmarksListView.setSelection(0);
2851 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2852 // Get handles for the views from `dialogFragment`.
2853 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2854 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2855 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2857 // Store the bookmark strings.
2858 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2859 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2861 // Update the bookmark.
2862 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2863 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2864 } else { // Update the bookmark using the `WebView` favorite icon.
2865 // Create a favorite icon byte array output stream.
2866 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2868 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2869 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2871 // Convert the favorite icon byte array stream to a byte array.
2872 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2874 // Update the bookmark and the favorite icon.
2875 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2878 // Update the bookmarks cursor with the current contents of this folder.
2879 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2881 // Update the list view.
2882 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2886 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2887 // Get handles for the views from `dialogFragment`.
2888 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2889 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2890 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2891 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2893 // Get the new folder name.
2894 String newFolderNameString = editFolderNameEditText.getText().toString();
2896 // Check if the favorite icon has changed.
2897 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2898 // Update the name in the database.
2899 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2900 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2901 // Create the new folder icon Bitmap.
2902 Bitmap folderIconBitmap;
2904 // Populate the new folder icon bitmap.
2905 if (defaultFolderIconRadioButton.isChecked()) {
2906 // Get the default folder icon drawable.
2907 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2909 // Convert the folder icon drawable to a bitmap drawable.
2910 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2912 // Convert the folder icon bitmap drawable to a bitmap.
2913 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2914 } else { // Use the `WebView` favorite icon.
2915 // Copy the favorite icon bitmap to the folder icon bitmap.
2916 folderIconBitmap = favoriteIconBitmap;
2919 // Create a folder icon byte array output stream.
2920 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2922 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2923 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2925 // Convert the folder icon byte array stream to a byte array.
2926 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2928 // Update the folder icon in the database.
2929 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2930 } else { // The folder icon and the name have changed.
2931 // Get the new folder icon `Bitmap`.
2932 Bitmap folderIconBitmap;
2933 if (defaultFolderIconRadioButton.isChecked()) {
2934 // Get the default folder icon drawable.
2935 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2937 // Convert the folder icon drawable to a bitmap drawable.
2938 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2940 // Convert the folder icon bitmap drawable to a bitmap.
2941 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2942 } else { // Use the `WebView` favorite icon.
2943 // Copy the favorite icon bitmap to the folder icon bitmap.
2944 folderIconBitmap = favoriteIconBitmap;
2947 // Create a folder icon byte array output stream.
2948 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2950 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2951 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2953 // Convert the folder icon byte array stream to a byte array.
2954 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2956 // Update the folder name and icon in the database.
2957 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2960 // Update the bookmarks cursor with the current contents of this folder.
2961 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2963 // Update the `ListView`.
2964 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2968 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2969 switch (downloadType) {
2970 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2971 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2972 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2975 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2976 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2977 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2983 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2984 // Get a handle for the fragment manager.
2985 FragmentManager fragmentManager = getSupportFragmentManager();
2987 switch (requestCode) {
2988 case DOWNLOAD_FILE_REQUEST_CODE:
2989 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2990 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2992 // On API 23, displaying the fragment must be delayed or the app will crash.
2993 if (Build.VERSION.SDK_INT == 23) {
2994 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2996 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2999 // Reset the download variables.
3001 downloadContentDisposition = "";
3002 downloadContentLength = 0;
3005 case DOWNLOAD_IMAGE_REQUEST_CODE:
3006 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
3007 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
3009 // On API 23, displaying the fragment must be delayed or the app will crash.
3010 if (Build.VERSION.SDK_INT == 23) {
3011 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3013 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
3016 // Reset the image URL variable.
3017 downloadImageUrl = "";
3023 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
3024 // Download the image if it has an HTTP or HTTPS URI.
3025 if (imageUrl.startsWith("http")) {
3026 // Get a handle for the system `DOWNLOAD_SERVICE`.
3027 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3029 // Parse `imageUrl`.
3030 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3032 // Get a handle for the cookie manager.
3033 CookieManager cookieManager = CookieManager.getInstance();
3035 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
3036 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3037 if (cookieManager.acceptCookie()) {
3038 // Get the cookies for `imageUrl`.
3039 String cookies = cookieManager.getCookie(imageUrl);
3041 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3042 downloadRequest.addRequestHeader("Cookie", cookies);
3045 // Get the file name from the dialog fragment.
3046 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3047 String imageName = downloadImageNameEditText.getText().toString();
3049 // Specify the download location.
3050 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3051 // Download to the public download directory.
3052 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3053 } else { // External write permission denied.
3054 // Download to the app's external download directory.
3055 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3058 // Allow `MediaScanner` to index the download if it is a media file.
3059 downloadRequest.allowScanningByMediaScanner();
3061 // Add the URL as the description for the download.
3062 downloadRequest.setDescription(imageUrl);
3064 // Show the download notification after the download is completed.
3065 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3067 // Remove the lint warning below that `downloadManager` might be `null`.
3068 assert downloadManager != null;
3070 // Initiate the download.
3071 downloadManager.enqueue(downloadRequest);
3072 } else { // The image is not an HTTP or HTTPS URI.
3073 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3078 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3079 // Download the file if it has an HTTP or HTTPS URI.
3080 if (downloadUrl.startsWith("http")) {
3081 // Get a handle for the system `DOWNLOAD_SERVICE`.
3082 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3084 // Parse `downloadUrl`.
3085 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3087 // Get a handle for the cookie manager.
3088 CookieManager cookieManager = CookieManager.getInstance();
3090 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
3091 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3092 if (cookieManager.acceptCookie()) {
3093 // Get the cookies for `downloadUrl`.
3094 String cookies = cookieManager.getCookie(downloadUrl);
3096 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3097 downloadRequest.addRequestHeader("Cookie", cookies);
3100 // Get the file name from the dialog fragment.
3101 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3102 String fileName = downloadFileNameEditText.getText().toString();
3104 // Specify the download location.
3105 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3106 // Download to the public download directory.
3107 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3108 } else { // External write permission denied.
3109 // Download to the app's external download directory.
3110 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3113 // Allow `MediaScanner` to index the download if it is a media file.
3114 downloadRequest.allowScanningByMediaScanner();
3116 // Add the URL as the description for the download.
3117 downloadRequest.setDescription(downloadUrl);
3119 // Show the download notification after the download is completed.
3120 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3122 // Remove the lint warning below that `downloadManager` might be `null`.
3123 assert downloadManager != null;
3125 // Initiate the download.
3126 downloadManager.enqueue(downloadRequest);
3127 } else { // The download is not an HTTP or HTTPS URI.
3128 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3132 // Override `onBackPressed` to handle the navigation drawer and and the WebView.
3134 public void onBackPressed() {
3135 // Get a handle for the drawer layout.
3136 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3138 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
3139 // Close the navigation drawer.
3140 drawerLayout.closeDrawer(GravityCompat.START);
3141 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
3142 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
3143 // close the bookmarks drawer.
3144 drawerLayout.closeDrawer(GravityCompat.END);
3145 } else { // A subfolder is displayed.
3146 // Place the former parent folder in `currentFolder`.
3147 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3149 // Load the new folder.
3150 loadBookmarksFolder();
3152 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
3153 // Reset the current domain name so that navigation works if third-party requests are blocked.
3154 currentWebView.resetCurrentDomainName();
3156 // Set navigating history so that the domain settings are applied when the new URL is loaded.
3157 currentWebView.setNavigatingHistory(true);
3160 currentWebView.goBack();
3161 } else { // There is nothing else to do.
3162 // Load a blank website.
3167 // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
3169 public void onActivityResult(int requestCode, int resultCode, Intent data) {
3170 // File uploads only work on API >= 21.
3171 if (Build.VERSION.SDK_INT >= 21) {
3172 // Pass the file to the WebView.
3173 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3177 private void loadUrlFromTextBox() {
3178 // Get a handle for the URL edit text.
3179 EditText urlEditText = findViewById(R.id.url_edittext);
3181 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
3182 String unformattedUrlString = urlEditText.getText().toString().trim();
3184 // Initialize the formatted URL string.
3187 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
3188 if (unformattedUrlString.startsWith("content://")) { // This is a Content URL.
3189 // Load the entire content URL.
3190 url = unformattedUrlString;
3191 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
3192 unformattedUrlString.startsWith("file://")) { // This is a standard URL.
3193 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
3194 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3195 unformattedUrlString = "https://" + unformattedUrlString;
3198 // Initialize `unformattedUrl`.
3199 URL unformattedUrl = null;
3201 // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
3203 unformattedUrl = new URL(unformattedUrlString);
3204 } catch (MalformedURLException e) {
3205 e.printStackTrace();
3208 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3209 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3210 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3211 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3212 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3213 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3216 Uri.Builder uri = new Uri.Builder();
3217 uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3219 // Decode the URI as a UTF-8 string in.
3221 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
3222 } catch (UnsupportedEncodingException exception) {
3223 // Do nothing. The formatted URL string will remain blank.
3225 } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string.
3226 // Create an encoded URL String.
3227 String encodedUrlString;
3229 // Sanitize the search input.
3231 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3232 } catch (UnsupportedEncodingException exception) {
3233 encodedUrlString = "";
3236 // Add the base search URL.
3237 url = searchURL + encodedUrlString;
3240 // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
3241 urlEditText.clearFocus();
3247 private void loadUrl(String url) {
3248 // Apply the domain settings.
3249 applyDomainSettings(currentWebView, url, true, false);
3252 currentWebView.loadUrl(url, customHeaders);
3255 public void findPreviousOnPage(View view) {
3256 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
3257 currentWebView.findNext(false);
3260 public void findNextOnPage(View view) {
3261 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3262 currentWebView.findNext(true);
3265 public void closeFindOnPage(View view) {
3266 // Get a handle for the views.
3267 Toolbar toolbar = findViewById(R.id.toolbar);
3268 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3269 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3271 // Delete the contents of `find_on_page_edittext`.
3272 findOnPageEditText.setText(null);
3274 // Clear the highlighted phrases.
3275 currentWebView.clearMatches();
3277 // Hide the find on page linear layout.
3278 findOnPageLinearLayout.setVisibility(View.GONE);
3280 // Show the toolbar.
3281 toolbar.setVisibility(View.VISIBLE);
3283 // Get a handle for the input method manager.
3284 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3286 // Remove the lint warning below that the input method manager might be null.
3287 assert inputMethodManager != null;
3289 // Hide the keyboard.
3290 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3293 private void applyAppSettings() {
3294 // Get a handle for the shared preferences.
3295 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3297 // Store the values from the shared preferences in variables.
3298 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3299 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3300 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3301 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3302 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3303 scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
3305 // Get handles for the views that need to be modified.
3306 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3307 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
3308 ActionBar actionBar = getSupportActionBar();
3309 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
3310 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3312 // Remove the incorrect lint warning below that the action bar might be null.
3313 assert actionBar != null;
3315 // Apply the proxy through Orbot settings.
3316 applyProxyThroughOrbot(false);
3318 // Set Do Not Track status.
3319 if (doNotTrackEnabled) {
3320 customHeaders.put("DNT", "1");
3322 customHeaders.remove("DNT");
3325 // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command.
3326 CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
3328 // Add the scrolling behavior to the layout parameters.
3330 // Enable scrolling of the app bar.
3331 layoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
3333 // Disable scrolling of the app bar.
3334 layoutParams.setBehavior(null);
3336 // Expand the app bar if it is currently collapsed.
3337 appBarLayout.setExpanded(true);
3340 // Apply the modified layout parameters to the swipe refresh layout.
3341 swipeRefreshLayout.setLayoutParams(layoutParams);
3343 // Set the app bar scrolling for each WebView.
3344 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3345 // Get the WebView tab fragment.
3346 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3348 // Get the fragment view.
3349 View fragmentView = webViewTabFragment.getView();
3351 // Only modify the WebViews if they exist.
3352 if (fragmentView != null) {
3353 // Get the nested scroll WebView from the tab fragment.
3354 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3356 // Set the app bar scrolling.
3357 nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
3361 // Update the full screen browsing mode settings.
3362 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3363 // Update the visibility of the app bar, which might have changed in the settings.
3365 // Hide the tab linear layout.
3366 tabsLinearLayout.setVisibility(View.GONE);
3368 // Hide the action bar.
3371 // Show the tab linear layout.
3372 tabsLinearLayout.setVisibility(View.VISIBLE);
3374 // Show the action bar.
3378 // Hide the banner ad in the free flavor.
3379 if (BuildConfig.FLAVOR.contentEquals("free")) {
3380 AdHelper.hideAd(findViewById(R.id.adview));
3383 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3384 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3386 /* Hide the system bars.
3387 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3388 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3389 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3390 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3392 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3393 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3394 } else { // Privacy Browser is not in full screen browsing mode.
3395 // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3396 inFullScreenBrowsingMode = false;
3398 // Show the tab linear layout.
3399 tabsLinearLayout.setVisibility(View.VISIBLE);
3401 // Show the action bar.
3404 // Show the banner ad in the free flavor.
3405 if (BuildConfig.FLAVOR.contentEquals("free")) {
3406 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3407 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3410 // Remove the `SYSTEM_UI` flags from the root frame layout.
3411 rootFrameLayout.setSystemUiVisibility(0);
3413 // Add the translucent status flag.
3414 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3419 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3420 @SuppressLint("SetJavaScriptEnabled")
3421 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) {
3422 // Store a copy of the current user agent to track changes for the return boolean.
3423 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3425 // Parse the URL into a URI.
3426 Uri uri = Uri.parse(url);
3428 // Extract the domain from `uri`.
3429 String newHostName = uri.getHost();
3431 // Strings don't like to be null.
3432 if (newHostName == null) {
3436 // Only apply the domain settings if a new domain is being loaded. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3437 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName)) {
3438 // Set the new host name as the current domain name.
3439 nestedScrollWebView.setCurrentDomainName(newHostName);
3441 // Reset the ignoring of pinned domain information.
3442 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3444 // Clear any pinned SSL certificate or IP addresses.
3445 nestedScrollWebView.clearPinnedSslCertificate();
3446 nestedScrollWebView.clearPinnedIpAddresses();
3448 // Reset the favorite icon if specified.
3450 // Initialize the favorite icon.
3451 nestedScrollWebView.initializeFavoriteIcon();
3453 // Get the current page position.
3454 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3456 // Get a handle for the tab layout.
3457 TabLayout tabLayout = findViewById(R.id.tablayout);
3459 // Get the corresponding tab.
3460 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3462 // Remove the warning below that the tab might be null.
3465 // Get the tab custom view.
3466 View tabCustomView = tab.getCustomView();
3468 // Remove the warning below that the tab custom view might be null.
3469 assert tabCustomView != null;
3471 // Get the tab views.
3472 ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3473 TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3475 // Set the default favorite icon as the favorite icon for this tab.
3476 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3478 // Set the loading title text.
3479 tabTitleTextView.setText(R.string.loading);
3482 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3483 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3485 // Get a full cursor from `domainsDatabaseHelper`.
3486 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3488 // Initialize `domainSettingsSet`.
3489 Set<String> domainSettingsSet = new HashSet<>();
3491 // Get the domain name column index.
3492 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3494 // Populate `domainSettingsSet`.
3495 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3496 // Move `domainsCursor` to the current row.
3497 domainNameCursor.moveToPosition(i);
3499 // Store the domain name in `domainSettingsSet`.
3500 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3503 // Close `domainNameCursor.
3504 domainNameCursor.close();
3506 // Initialize the domain name in database variable.
3507 String domainNameInDatabase = null;
3509 // Check the hostname against the domain settings set.
3510 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3511 // Record the domain name in the database.
3512 domainNameInDatabase = newHostName;
3514 // Set the domain settings applied tracker to true.
3515 nestedScrollWebView.setDomainSettingsApplied(true);
3516 } else { // The hostname is not contained in the domain settings set.
3517 // Set the domain settings applied tracker to false.
3518 nestedScrollWebView.setDomainSettingsApplied(false);
3521 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3522 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3523 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3524 // Set the domain settings applied tracker to true.
3525 nestedScrollWebView.setDomainSettingsApplied(true);
3527 // Store the applied domain names as it appears in the database.
3528 domainNameInDatabase = "*." + newHostName;
3531 // Strip out the lowest subdomain of of the host name.
3532 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3536 // Get a handle for the shared preferences.
3537 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3539 // Store the general preference information.
3540 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3541 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3542 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3543 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3544 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3546 // Get a handle for the cookie manager.
3547 CookieManager cookieManager = CookieManager.getInstance();
3549 // Get handles for the views.
3550 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3551 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3553 // Initialize the user agent array adapter and string array.
3554 ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3555 String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3557 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3558 // Get a cursor for the current host and move it to the first position.
3559 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3560 currentDomainSettingsCursor.moveToFirst();
3562 // Get the settings from the cursor.
3563 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3564 nestedScrollWebView.setDomainSettingsJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3565 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3566 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3567 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3568 // Form data can be removed once the minimum API >= 26.
3569 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3570 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST,
3571 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3572 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY,
3573 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3574 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3575 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3576 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3577 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3578 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY,
3579 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3580 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3581 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3582 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3583 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3584 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3585 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3586 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3587 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3588 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3589 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3590 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3591 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3592 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3593 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3594 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3595 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3597 // Create the pinned SSL date variables.
3598 Date pinnedSslStartDate;
3599 Date pinnedSslEndDate;
3601 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0 because creating a new Date results in an error if the input is 0.
3602 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3603 pinnedSslStartDate = null;
3605 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3608 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0 because creating a new Date results in an error if the input is 0.
3609 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3610 pinnedSslEndDate = null;
3612 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3615 // If there is a pinned SSL certificate, store it in the WebView.
3616 if (pinnedSslCertificate) {
3617 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3618 pinnedSslStartDate, pinnedSslEndDate);
3621 // If there is a pinned IP address, store it in the WebView.
3622 if (pinnedIpAddresses) {
3623 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3626 // Set night mode according to the night mode int.
3627 switch (nightModeInt) {
3628 case DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT:
3629 // Set night mode according to the current default.
3630 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3633 case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3634 // Enable night mode.
3635 nestedScrollWebView.setNightMode(true);
3638 case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3639 // Disable night mode.
3640 nestedScrollWebView.setNightMode(false);
3644 // Enable JavaScript if night mode is enabled.
3645 if (nestedScrollWebView.getNightMode()) {
3646 // Enable JavaScript.
3647 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3649 // Set JavaScript according to the domain settings.
3650 nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled());
3653 // Close the current host domain settings cursor.
3654 currentDomainSettingsCursor.close();
3656 // Apply the domain settings.
3657 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3659 // Set third-party cookies status if API >= 21.
3660 if (Build.VERSION.SDK_INT >= 21) {
3661 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3664 // Apply the form data setting if the API < 26.
3665 if (Build.VERSION.SDK_INT < 26) {
3666 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3669 // Apply the font size.
3670 if (fontSize == 0) { // Apply the default font size.
3671 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3672 } else { // Apply the specified font size.
3673 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3676 // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
3677 // <https://redmine.stoutner.com/issues/160>
3678 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3679 // Set the user agent.
3680 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3681 // Get the array position of the default user agent name.
3682 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3684 // Set the user agent according to the system default.
3685 switch (defaultUserAgentArrayPosition) {
3686 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3687 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3688 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3691 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3692 // Set the user agent to `""`, which uses the default value.
3693 nestedScrollWebView.getSettings().setUserAgentString("");
3696 case SETTINGS_CUSTOM_USER_AGENT:
3697 // Set the default custom user agent.
3698 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3702 // Get the user agent string from the user agent data array
3703 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3705 } else { // Set the user agent according to the stored name.
3706 // Get the array position of the user agent name.
3707 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3709 switch (userAgentArrayPosition) {
3710 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3711 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3714 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3715 // Set the user agent to `""`, which uses the default value.
3716 nestedScrollWebView.getSettings().setUserAgentString("");
3720 // Get the user agent string from the user agent data array.
3721 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3726 // Set swipe to refresh.
3727 switch (swipeToRefreshInt) {
3728 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3729 // Store the swipe to refresh status in the nested scroll WebView.
3730 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3732 // Apply swipe to refresh according to the default. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3733 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3736 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3737 // Store the swipe to refresh status in the nested scroll WebView.
3738 nestedScrollWebView.setSwipeToRefresh(true);
3740 // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3741 swipeRefreshLayout.setEnabled(true);
3744 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3745 // Store the swipe to refresh status in the nested scroll WebView.
3746 nestedScrollWebView.setSwipeToRefresh(false);
3748 // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3749 swipeRefreshLayout.setEnabled(false);
3752 // Set the loading of webpage images.
3753 switch (displayWebpageImagesInt) {
3754 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3755 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3758 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3759 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3762 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3763 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3767 // Set a green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3769 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3771 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3773 } else { // The new URL does not have custom domain settings. Load the defaults.
3774 // Store the values from the shared preferences.
3775 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3776 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
3777 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3778 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3779 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3780 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, sharedPreferences.getBoolean("easylist", true));
3781 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3782 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3783 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3784 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3785 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3786 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3788 // Enable JavaScript if night mode is enabled.
3789 if (nestedScrollWebView.getNightMode()) {
3790 // Enable JavaScript.
3791 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3793 // Set JavaScript according to the domain settings.
3794 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3797 // Apply the default settings.
3798 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3799 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3801 // Apply the form data setting if the API < 26.
3802 if (Build.VERSION.SDK_INT < 26) {
3803 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3806 // Store the swipe to refresh status in the nested scroll WebView.
3807 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3809 // Apply swipe to refresh according to the default.
3810 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3812 // Reset the pinned variables.
3813 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
3815 // Set third-party cookies status if API >= 21.
3816 if (Build.VERSION.SDK_INT >= 21) {
3817 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
3820 // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
3821 // <https://redmine.stoutner.com/issues/160>
3822 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3823 // Get the array position of the user agent name.
3824 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3826 // Set the user agent.
3827 switch (userAgentArrayPosition) {
3828 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3829 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3830 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3833 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3834 // Set the user agent to `""`, which uses the default value.
3835 nestedScrollWebView.getSettings().setUserAgentString("");
3838 case SETTINGS_CUSTOM_USER_AGENT:
3839 // Set the default custom user agent.
3840 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3844 // Get the user agent string from the user agent data array
3845 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3849 // Set the loading of webpage images.
3850 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3852 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
3853 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
3856 // Close the domains database helper.
3857 domainsDatabaseHelper.close();
3859 // Update the privacy icons.
3860 updatePrivacyIcons(true);
3863 // Reload the website if returning from the Domains activity.
3864 if (reloadWebsite) {
3865 nestedScrollWebView.reload();
3868 // Return the user agent changed status.
3869 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
3872 private void applyProxyThroughOrbot(boolean reloadWebsite) {
3873 // Get a handle for the shared preferences.
3874 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3876 // Get the search and theme preferences.
3877 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3878 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3879 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3880 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3881 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3883 // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
3884 ActionBar actionBar = getSupportActionBar();
3886 // Remove the incorrect lint warning later that the action bar might be null.
3887 assert actionBar != null;
3889 // Set the homepage, search, and proxy options.
3890 if (proxyThroughOrbot) { // Set the Tor options.
3891 // Set the search URL.
3892 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
3893 searchURL = torSearchCustomUrlString;
3894 } else { // Use the string from the pre-built list.
3895 searchURL = torSearchString;
3898 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
3899 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
3901 // Set the `appBar` background to indicate proxying through Orbot is enabled.
3903 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
3905 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
3908 // Check to see if Orbot is ready.
3909 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
3910 // Set `waitingForOrbot`.
3911 waitingForOrbot = true;
3913 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
3914 currentWebView.getSettings().setUseWideViewPort(false);
3916 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
3917 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
3918 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
3919 // Reload the website.
3920 currentWebView.reload();
3922 } else { // Set the non-Tor options.
3923 // Set the search URL.
3924 if (searchString.equals("Custom URL")) { // Get the custom URL string.
3925 searchURL = searchCustomUrlString;
3926 } else { // Use the string from the pre-built list.
3927 searchURL = searchString;
3930 // Reset the proxy to default. The host is `""` and the port is `"0"`.
3931 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
3933 // Set the default `appBar` background.
3935 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
3937 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
3940 // Reset `waitingForOrbot.
3941 waitingForOrbot = false;
3943 // Reload the WebViews if requested.
3944 if (reloadWebsite) {
3945 // Reload the WebViews.
3946 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3947 // Get the WebView tab fragment.
3948 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3950 // Get the fragment view.
3951 View fragmentView = webViewTabFragment.getView();
3953 // Only reload the WebViews if they exist.
3954 if (fragmentView != null) {
3955 // Get the nested scroll WebView from the tab fragment.
3956 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3958 // Reload the WebView.
3959 nestedScrollWebView.reload();
3966 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
3967 // Only update the privacy icons if the options menu and the current WebView have already been populated.
3968 if ((optionsMenu != null) && (currentWebView != null)) {
3969 // Get a handle for the shared preferences.
3970 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3972 // Get the theme and screenshot preferences.
3973 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3975 // Get handles for the menu items.
3976 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
3977 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
3978 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
3979 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
3981 // Update the privacy icon.
3982 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
3983 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
3984 } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled.
3985 privacyMenuItem.setIcon(R.drawable.warning);
3986 } else { // All the dangerous features are disabled.
3987 privacyMenuItem.setIcon(R.drawable.privacy_mode);
3990 // Update the first-party cookies icon.
3991 if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled.
3992 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
3993 } else { // First-party cookies are disabled.
3995 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
3997 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4001 // Update the DOM storage icon.
4002 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
4003 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4004 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
4006 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4008 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4010 } else { // JavaScript is disabled, so DOM storage is ghosted.
4012 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4014 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4018 // Update the refresh icon.
4020 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4022 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4025 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4026 if (runInvalidateOptionsMenu) {
4027 invalidateOptionsMenu();
4032 private void openUrlWithExternalApp(String url) {
4033 // Create a download intent. Not specifying the action type will display the maximum number of options.
4034 Intent downloadIntent = new Intent();
4036 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4037 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4039 // Flag the intent to open in a new task.
4040 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4042 // Show the chooser.
4043 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4046 private void highlightUrlText() {
4047 // Get a handle for the URL edit text.
4048 EditText urlEditText = findViewById(R.id.url_edittext);
4050 // Only highlight the URL text if the box is not currently selected.
4051 if (!urlEditText.hasFocus()) {
4052 // Get the URL string.
4053 String urlString = urlEditText.getText().toString();
4055 // Highlight the URL according to the protocol.
4056 if (urlString.startsWith("file://")) { // This is a file URL.
4057 // De-emphasize only the protocol.
4058 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4059 } else if (urlString.startsWith("content://")) {
4060 // De-emphasize only the protocol.
4061 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4062 } else { // This is a web URL.
4063 // Get the index of the `/` immediately after the domain name.
4064 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4066 // Create a base URL string.
4069 // Get the base URL.
4070 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4071 // Get the base URL.
4072 baseUrl = urlString.substring(0, endOfDomainName);
4073 } else { // There are no characters after the base URL.
4074 // Set the base URL to be the entire URL string.
4075 baseUrl = urlString;
4078 // Get the index of the last `.` in the domain.
4079 int lastDotIndex = baseUrl.lastIndexOf(".");
4081 // Get the index of the penultimate `.` in the domain.
4082 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4084 // Markup the beginning of the URL.
4085 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4086 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4088 // De-emphasize subdomains.
4089 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4090 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4092 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4093 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4094 // De-emphasize the protocol and the additional subdomains.
4095 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4096 } else { // There is only one subdomain in the domain name.
4097 // De-emphasize only the protocol.
4098 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4102 // De-emphasize the text after the domain name.
4103 if (endOfDomainName > 0) {
4104 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4110 private void loadBookmarksFolder() {
4111 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4112 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4114 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4115 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4117 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4118 // Inflate the individual item layout. `false` does not attach it to the root.
4119 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4123 public void bindView(View view, Context context, Cursor cursor) {
4124 // Get handles for the views.
4125 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4126 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4128 // Get the favorite icon byte array from the cursor.
4129 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4131 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4132 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4134 // Display the bitmap in `bookmarkFavoriteIcon`.
4135 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4137 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4138 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4139 bookmarkNameTextView.setText(bookmarkNameString);
4141 // Make the font bold for folders.
4142 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4143 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4144 } else { // Reset the font to default for normal bookmarks.
4145 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4150 // Get a handle for the bookmarks list view.
4151 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4153 // Populate the list view with the adapter.
4154 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4156 // Get a handle for the bookmarks title text view.
4157 TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4159 // Set the bookmarks drawer title.
4160 if (currentBookmarksFolder.isEmpty()) {
4161 bookmarksTitleTextView.setText(R.string.bookmarks);
4163 bookmarksTitleTextView.setText(currentBookmarksFolder);
4167 private void openWithApp(String url) {
4168 // Create the open with intent with `ACTION_VIEW`.
4169 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4171 // Set the URI but not the MIME type. This should open all available apps.
4172 openWithAppIntent.setData(Uri.parse(url));
4174 // Flag the intent to open in a new task.
4175 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4177 // Show the chooser.
4178 startActivity(openWithAppIntent);
4181 private void openWithBrowser(String url) {
4182 // Create the open with intent with `ACTION_VIEW`.
4183 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4185 // Set the URI and the MIME type. `"text/html"` should load browser options.
4186 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4188 // Flag the intent to open in a new task.
4189 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4191 // Show the chooser.
4192 startActivity(openWithBrowserIntent);
4195 public void addTab(View view) {
4196 // Get a handle for the tab layout and the view pager.
4197 TabLayout tabLayout = findViewById(R.id.tablayout);
4198 ViewPager webViewPager = findViewById(R.id.webviewpager);
4200 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4201 int newTabNumber = tabLayout.getTabCount();
4204 tabLayout.addTab(tabLayout.newTab());
4207 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4209 // Remove the lint warning below that the current tab might be null.
4210 assert newTab != null;
4212 // Set a custom view on the new tab.
4213 newTab.setCustomView(R.layout.tab_custom_view);
4215 // Add the new WebView page.
4216 webViewPagerAdapter.addPage(newTabNumber, webViewPager);
4219 private void setCurrentWebView(int pageNumber) {
4220 // Get a handle for the shared preferences.
4221 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4223 // Get the theme preference.
4224 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4226 // Get handles for the URL views.
4227 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4228 EditText urlEditText = findViewById(R.id.url_edittext);
4229 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4231 //Stop the swipe to refresh indicator if it is running
4232 swipeRefreshLayout.setRefreshing(false);
4234 // Get the WebView tab fragment.
4235 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4237 // Get the fragment view.
4238 View fragmentView = webViewTabFragment.getView();
4240 // Set the current WebView if the fragment view is not null.
4241 if (fragmentView != null) {
4242 // Store the current WebView.
4243 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4245 // Update the status of swipe to refresh.
4246 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
4247 if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, swipe refresh layout is continuously updated with an on scroll change listener and only enabled if the WebView is scrolled to the top.
4248 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.
4249 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4251 // Enable the swipe refresh layout.
4252 swipeRefreshLayout.setEnabled(true);
4254 } else { // Swipe to refresh is disabled.
4255 // Disable the swipe refresh layout.
4256 swipeRefreshLayout.setEnabled(false);
4259 // Get a handle for the cookie manager.
4260 CookieManager cookieManager = CookieManager.getInstance();
4262 // Set the first-party cookie status.
4263 cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
4265 // Update the privacy icons. `true` redraws the icons in the app bar.
4266 updatePrivacyIcons(true);
4268 // Get a handle for the input method manager.
4269 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4271 // Remove the lint warning below that the input method manager might be null.
4272 assert inputMethodManager != null;
4274 // Get the current URL.
4275 String url = currentWebView.getUrl();
4277 if ((url == null) || url.equals("about:blank")) { // The WebView is blank.
4278 // Display the hint in the URL edit text.
4279 urlEditText.setText("");
4281 // Request focus for the URL text box.
4282 urlEditText.requestFocus();
4284 // Display the keyboard.
4285 inputMethodManager.showSoftInput(urlEditText, 0);
4286 } else { // The WebView has a loaded URL.
4287 // Clear the focus from the URL text box.
4288 urlEditText.clearFocus();
4290 // Hide the soft keyboard.
4291 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4293 // Display the current URL in the URL text box.
4294 urlEditText.setText(url);
4296 // Highlight the URL text.
4300 // Set the background to indicate the domain settings status.
4301 if (currentWebView.getDomainSettingsApplied()) {
4302 // Set a green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
4304 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4306 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4309 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4315 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar) {
4316 // Get handles for the activity views.
4317 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4318 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4319 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4320 ActionBar actionBar = getSupportActionBar();
4321 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
4322 EditText urlEditText = findViewById(R.id.url_edittext);
4323 TabLayout tabLayout = findViewById(R.id.tablayout);
4324 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4326 // Remove the incorrect lint warning below that the action bar might be null.
4327 assert actionBar != null;
4329 // Get a handle for the activity
4330 Activity activity = this;
4332 // Get a handle for the input method manager.
4333 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4335 // Instantiate the blocklist helper.
4336 BlockListHelper blockListHelper = new BlockListHelper();
4338 // Remove the lint warning below that the input method manager might be null.
4339 assert inputMethodManager != null;
4341 // Get a handle for the shared preferences.
4342 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4344 // Get the relevant preferences.
4345 boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4347 // Initialize the favorite icon.
4348 nestedScrollWebView.initializeFavoriteIcon();
4350 // Set the app bar scrolling.
4351 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4353 // Allow pinch to zoom.
4354 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4356 // Hide zoom controls.
4357 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4359 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4360 if (Build.VERSION.SDK_INT >= 21) {
4361 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4364 // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
4365 nestedScrollWebView.getSettings().setUseWideViewPort(true);
4367 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4368 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4370 // Explicitly disable geolocation.
4371 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4373 // Create a double-tap gesture detector to toggle full-screen mode.
4374 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4375 // Override `onDoubleTap()`. All other events are handled using the default settings.
4377 public boolean onDoubleTap(MotionEvent event) {
4378 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4379 // Toggle the full screen browsing mode tracker.
4380 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4382 // Toggle the full screen browsing mode.
4383 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4384 // Store the swipe refresh layout top padding.
4385 swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop();
4387 // Hide the app bar if specified.
4389 // Close the find on page bar if it is visible.
4390 closeFindOnPage(null);
4392 // Hide the tab linear layout.
4393 tabsLinearLayout.setVisibility(View.GONE);
4395 // Hide the action bar.
4398 // Check to see if app bar scrolling is disabled.
4399 if (!scrollAppBar) {
4400 // Remove the padding from the top of the swipe refresh layout.
4401 swipeRefreshLayout.setPadding(0, 0, 0, 0);
4405 // Hide the banner ad in the free flavor.
4406 if (BuildConfig.FLAVOR.contentEquals("free")) {
4407 AdHelper.hideAd(findViewById(R.id.adview));
4410 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4411 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4413 /* Hide the system bars.
4414 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4415 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4416 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4417 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4419 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4420 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4421 } else { // Switch to normal viewing mode.
4422 // Show the tab linear layout.
4423 tabsLinearLayout.setVisibility(View.VISIBLE);
4425 // Show the action bar.
4428 // Check to see if app bar scrolling is disabled.
4429 if (!scrollAppBar) {
4430 // Add the padding from the top of the swipe refresh layout.
4431 swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0);
4434 // Show the banner ad in the free flavor.
4435 if (BuildConfig.FLAVOR.contentEquals("free")) {
4437 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4440 // Remove the `SYSTEM_UI` flags from the root frame layout.
4441 rootFrameLayout.setSystemUiVisibility(0);
4443 // Add the translucent status flag.
4444 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4447 // Consume the double-tap.
4449 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4455 // Pass all touch events on the WebView through the double-tap gesture detector.
4456 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4457 // Call `performClick()` on the view, which is required for accessibility.
4458 view.performClick();
4460 // Send the event to the gesture detector.
4461 return doubleTapGestureDetector.onTouchEvent(event);
4464 // Register the WebView for a context menu. This is used to see link targets and download images.
4465 registerForContextMenu(nestedScrollWebView);
4467 // Allow the downloading of files.
4468 nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4469 // Check if the download should be processed by an external app.
4470 if (downloadWithExternalApp) { // Download with an external app.
4471 // Create a download intent. Not specifying the action type will display the maximum number of options.
4472 Intent downloadIntent = new Intent();
4474 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4475 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4477 // Flag the intent to open in a new task.
4478 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4480 // Show the chooser.
4481 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4482 } else { // Download with Android's download manager.
4483 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4484 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4485 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4487 // Store the variables for future use by `onRequestPermissionsResult()`.
4489 downloadContentDisposition = contentDisposition;
4490 downloadContentLength = contentLength;
4492 // Show a dialog if the user has previously denied the permission.
4493 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4494 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4495 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4497 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4498 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4499 } else { // Show the permission request directly.
4500 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4501 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4503 } else { // The storage permission has already been granted.
4504 // Get a handle for the download file alert dialog.
4505 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4507 // Show the download file alert dialog.
4508 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4513 // Update the find on page count.
4514 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4515 // Get a handle for `findOnPageCountTextView`.
4516 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4519 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4520 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4521 // Set `findOnPageCountTextView` to `0/0`.
4522 findOnPageCountTextView.setText(R.string.zero_of_zero);
4523 } else if (isDoneCounting) { // There are matches.
4524 // `activeMatchOrdinal` is zero-based.
4525 int activeMatch = activeMatchOrdinal + 1;
4527 // Build the match string.
4528 String matchString = activeMatch + "/" + numberOfMatches;
4530 // Set `findOnPageCountTextView`.
4531 findOnPageCountTextView.setText(matchString);
4536 // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView.
4537 // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`.
4538 nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
4539 if (nestedScrollWebView.getSwipeToRefresh()) {
4540 // Only enable swipe to refresh if the WebView is scrolled to the top.
4541 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
4545 // Set the web chrome client.
4546 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4547 // Update the progress bar when a page is loading.
4549 public void onProgressChanged(WebView view, int progress) {
4550 // Inject the night mode CSS if night mode is enabled.
4551 if (nestedScrollWebView.getNightMode()) { // Night mode is enabled.
4552 // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links
4553 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
4554 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
4555 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4556 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4557 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4558 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4559 // Initialize a handler to display `mainWebView`.
4560 Handler displayWebViewHandler = new Handler();
4562 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4563 Runnable displayWebViewRunnable = () -> {
4564 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
4565 if (progressBar.getVisibility() == View.GONE) {
4566 nestedScrollWebView.setVisibility(View.VISIBLE);
4570 // Display the WebView after 500 milliseconds.
4571 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4573 } else { // Night mode is disabled.
4574 // Display the nested scroll WebView if night mode is disabled.
4575 // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
4576 // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
4577 nestedScrollWebView.setVisibility(View.VISIBLE);
4580 // Update the progress bar.
4581 progressBar.setProgress(progress);
4583 // Set the visibility of the progress bar.
4584 if (progress < 100) {
4585 // Show the progress bar.
4586 progressBar.setVisibility(View.VISIBLE);
4588 // Hide the progress bar.
4589 progressBar.setVisibility(View.GONE);
4591 //Stop the swipe to refresh indicator if it is running
4592 swipeRefreshLayout.setRefreshing(false);
4596 // Set the favorite icon when it changes.
4598 public void onReceivedIcon(WebView view, Bitmap icon) {
4599 // Only update the favorite icon if the website has finished loading.
4600 if (progressBar.getVisibility() == View.GONE) {
4601 // Store the new favorite icon.
4602 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
4604 // Get the current page position.
4605 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4607 // Get the current tab.
4608 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4610 // Check to see if the tab has been populated.
4612 // Get the custom view from the tab.
4613 View tabView = tab.getCustomView();
4615 // Check to see if the custom tab view has been populated.
4616 if (tabView != null) {
4617 // Get the favorite icon image view from the tab.
4618 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
4620 // Display the favorite icon in the tab.
4621 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4627 // Save a copy of the title when it changes.
4629 public void onReceivedTitle(WebView view, String title) {
4630 // Get the current page position.
4631 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4633 // Get the current tab.
4634 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4636 // Only populate the title text view if the tab has been fully created.
4638 // Get the custom view from the tab.
4639 View tabView = tab.getCustomView();
4641 // Remove the incorrect warning below that the current tab view might be null.
4642 assert tabView != null;
4644 // Get the title text view from the tab.
4645 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
4647 // Set the title as the tab text.
4648 tabTitleTextView.setText(title);
4652 // Enter full screen video.
4654 public void onShowCustomView(View video, CustomViewCallback callback) {
4655 // Get a handle for the full screen video frame layout.
4656 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4658 // Set the full screen video flag.
4659 displayingFullScreenVideo = true;
4661 // Pause the ad if this is the free flavor.
4662 if (BuildConfig.FLAVOR.contentEquals("free")) {
4663 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4664 AdHelper.pauseAd(findViewById(R.id.adview));
4667 // Hide the keyboard.
4668 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4670 // Hide the main content relative layout.
4671 mainContentRelativeLayout.setVisibility(View.GONE);
4673 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4674 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4676 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4677 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4679 /* Hide the system bars.
4680 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4681 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4682 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4683 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4685 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4686 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4688 // Disable the sliding drawers.
4689 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4691 // Add the video view to the full screen video frame layout.
4692 fullScreenVideoFrameLayout.addView(video);
4694 // Show the full screen video frame layout.
4695 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4698 // Exit full screen video.
4700 public void onHideCustomView() {
4701 // Get a handle for the full screen video frame layout.
4702 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4704 // Unset the full screen video flag.
4705 displayingFullScreenVideo = false;
4707 // Remove all the views from the full screen video frame layout.
4708 fullScreenVideoFrameLayout.removeAllViews();
4710 // Hide the full screen video frame layout.
4711 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4713 // Enable the sliding drawers.
4714 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4716 // Show the main content relative layout.
4717 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4719 // Apply the appropriate full screen mode flags.
4720 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4721 // Hide the app bar if specified.
4723 // Hide the tab linear layout.
4724 tabsLinearLayout.setVisibility(View.GONE);
4726 // Hide the action bar.
4730 // Hide the banner ad in the free flavor.
4731 if (BuildConfig.FLAVOR.contentEquals("free")) {
4732 AdHelper.hideAd(findViewById(R.id.adview));
4735 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4736 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4738 /* Hide the system bars.
4739 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4740 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4741 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4742 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4744 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4745 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4746 } else { // Switch to normal viewing mode.
4747 // Remove the `SYSTEM_UI` flags from the root frame layout.
4748 rootFrameLayout.setSystemUiVisibility(0);
4750 // Add the translucent status flag.
4751 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4754 // Reload the ad for the free flavor if not in full screen mode.
4755 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4757 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4763 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4764 // Show the file chooser if the device is running API >= 21.
4765 if (Build.VERSION.SDK_INT >= 21) {
4766 // Store the file path callback.
4767 fileChooserCallback = filePathCallback;
4769 // Create an intent to open a chooser based ont the file chooser parameters.
4770 Intent fileChooserIntent = fileChooserParams.createIntent();
4772 // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4773 startActivityForResult(fileChooserIntent, 0);
4779 nestedScrollWebView.setWebViewClient(new WebViewClient() {
4780 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4781 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4783 public boolean shouldOverrideUrlLoading(WebView view, String url) {
4784 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
4785 // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed.
4786 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
4788 // Check if the user agent has changed.
4789 if (userAgentChanged) {
4790 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
4791 nestedScrollWebView.loadUrl(url, customHeaders);
4793 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4796 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4799 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
4800 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4801 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4803 // Parse the url and set it as the data for the intent.
4804 emailIntent.setData(Uri.parse(url));
4806 // Open the email program in a new task instead of as part of Privacy Browser.
4807 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4810 startActivity(emailIntent);
4812 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4814 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
4815 // Open the dialer and load the phone number, but wait for the user to place the call.
4816 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4818 // Add the phone number to the intent.
4819 dialIntent.setData(Uri.parse(url));
4821 // Open the dialer in a new task instead of as part of Privacy Browser.
4822 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4825 startActivity(dialIntent);
4827 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4829 } else { // Load a system chooser to select an app that can handle the URL.
4830 // Open an app that can handle the URL.
4831 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4833 // Add the URL to the intent.
4834 genericIntent.setData(Uri.parse(url));
4836 // List all apps that can handle the URL instead of just opening the first one.
4837 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4839 // Open the app in a new task instead of as part of Privacy Browser.
4840 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4842 // Start the app or display a snackbar if no app is available to handle the URL.
4844 startActivity(genericIntent);
4845 } catch (ActivityNotFoundException exception) {
4846 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
4849 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4854 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4856 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4857 // Get a handle for the navigation view.
4858 NavigationView navigationView = findViewById(R.id.navigationview);
4860 // Get a handle for the navigation menu.
4861 Menu navigationMenu = navigationView.getMenu();
4863 // Get a handle for the navigation requests menu item. The menu is 0 based.
4864 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
4866 // Create an empty web resource response to be used if the resource request is blocked.
4867 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4869 // Reset the whitelist results tracker.
4870 String[] whitelistResultStringArray = null;
4872 // Initialize the third party request tracker.
4873 boolean isThirdPartyRequest = false;
4875 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
4876 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
4878 // Store a copy of the current domain for use in later requests.
4879 String currentDomain = currentBaseDomain;
4881 // Nobody is happy when comparing null strings.
4882 if ((currentBaseDomain != null) && (url != null)) {
4883 // Convert the request URL to a URI.
4884 Uri requestUri = Uri.parse(url);
4886 // Get the request host name.
4887 String requestBaseDomain = requestUri.getHost();
4889 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
4890 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
4891 // Determine the current base domain.
4892 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4893 // Remove the first subdomain.
4894 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4897 // Determine the request base domain.
4898 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4899 // Remove the first subdomain.
4900 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4903 // Update the third party request tracker.
4904 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4908 // Get the current WebView page position.
4909 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4911 // Determine if the WebView is currently displayed.
4912 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
4914 // Block third-party requests if enabled.
4915 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
4916 // Add the result to the resource requests.
4917 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_THIRD_PARTY, url});
4919 // Increment the blocked requests counters.
4920 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4921 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
4923 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4924 if (webViewDisplayed) {
4925 // Updating the UI must be run from the UI thread.
4926 activity.runOnUiThread(() -> {
4927 // Update the menu item titles.
4928 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4930 // Update the options menu if it has been populated.
4931 if (optionsMenu != null) {
4932 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4933 optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
4934 getString(R.string.block_all_third_party_requests));
4939 // Return an empty web resource response.
4940 return emptyWebResourceResponse;
4943 // Check UltraPrivacy if it is enabled.
4944 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)) {
4945 // Check the URL against UltraPrivacy.
4946 String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
4948 // Process the UltraPrivacy results.
4949 if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
4950 // Add the result to the resource requests.
4951 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4952 ultraPrivacyResults[5]});
4954 // Increment the blocked requests counters.
4955 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4956 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY);
4958 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4959 if (webViewDisplayed) {
4960 // Updating the UI must be run from the UI thread.
4961 activity.runOnUiThread(() -> {
4962 // Update the menu item titles.
4963 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4965 // Update the options menu if it has been populated.
4966 if (optionsMenu != null) {
4967 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4968 optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
4973 // The resource request was blocked. Return an empty web resource response.
4974 return emptyWebResourceResponse;
4975 } else if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
4976 // Add a whitelist entry to the resource requests array.
4977 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4978 ultraPrivacyResults[5]});
4980 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
4985 // Check EasyList if it is enabled.
4986 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)) {
4987 // Check the URL against EasyList.
4988 String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
4990 // Process the EasyList results.
4991 if (easyListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
4992 // Add the result to the resource requests.
4993 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
4995 // Increment the blocked requests counters.
4996 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4997 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_LIST);
4999 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5000 if (webViewDisplayed) {
5001 // Updating the UI must be run from the UI thread.
5002 activity.runOnUiThread(() -> {
5003 // Update the menu item titles.
5004 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5006 // Update the options menu if it has been populated.
5007 if (optionsMenu != null) {
5008 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5009 optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
5014 // The resource request was blocked. Return an empty web resource response.
5015 return emptyWebResourceResponse;
5016 } else if (easyListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
5017 // Update the whitelist result string array tracker.
5018 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5022 // Check EasyPrivacy if it is enabled.
5023 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)) {
5024 // Check the URL against EasyPrivacy.
5025 String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5027 // Process the EasyPrivacy results.
5028 if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
5029 // Add the result to the resource requests.
5030 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5031 easyPrivacyResults[5]});
5033 // Increment the blocked requests counters.
5034 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5035 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY);
5037 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5038 if (webViewDisplayed) {
5039 // Updating the UI must be run from the UI thread.
5040 activity.runOnUiThread(() -> {
5041 // Update the menu item titles.
5042 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5044 // Update the options menu if it has been populated.
5045 if (optionsMenu != null) {
5046 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5047 optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
5052 // The resource request was blocked. Return an empty web resource response.
5053 return emptyWebResourceResponse;
5054 } else if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
5055 // Update the whitelist result string array tracker.
5056 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5060 // Check Fanboy’s Annoyance List if it is enabled.
5061 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
5062 // Check the URL against Fanboy's Annoyance List.
5063 String[] fanboysAnnoyanceListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5065 // Process the Fanboy's Annoyance List results.
5066 if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
5067 // Add the result to the resource requests.
5068 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5069 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5071 // Increment the blocked requests counters.
5072 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5073 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5075 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5076 if (webViewDisplayed) {
5077 // Updating the UI must be run from the UI thread.
5078 activity.runOnUiThread(() -> {
5079 // Update the menu item titles.
5080 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5082 // Update the options menu if it has been populated.
5083 if (optionsMenu != null) {
5084 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5085 optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5086 getString(R.string.fanboys_annoyance_list));
5091 // The resource request was blocked. Return an empty web resource response.
5092 return emptyWebResourceResponse;
5093 } else if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
5094 // Update the whitelist result string array tracker.
5095 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5096 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5098 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5099 // Check the URL against Fanboy's Annoyance List.
5100 String[] fanboysSocialListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5102 // Process the Fanboy's Social Blocking List results.
5103 if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5104 // Add the result to the resource requests.
5105 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5106 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5108 // Increment the blocked requests counters.
5109 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5110 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5112 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5113 if (webViewDisplayed) {
5114 // Updating the UI must be run from the UI thread.
5115 activity.runOnUiThread(() -> {
5116 // Update the menu item titles.
5117 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5119 // Update the options menu if it has been populated.
5120 if (optionsMenu != null) {
5121 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5122 optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5123 getString(R.string.fanboys_social_blocking_list));
5128 // The resource request was blocked. Return an empty web resource response.
5129 return emptyWebResourceResponse;
5130 } else if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5131 // Update the whitelist result string array tracker.
5132 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5133 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5137 // Add the request to the log because it hasn't been processed by any of the previous checks.
5138 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5139 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5140 } else { // The request didn't match any blocklist entry. Log it as a default request.
5141 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_DEFAULT, url});
5144 // The resource request has not been blocked. `return null` loads the requested resource.
5148 // Handle HTTP authentication requests.
5150 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5151 // Store the handler.
5152 nestedScrollWebView.setHttpAuthHandler(handler);
5154 // Instantiate an HTTP authentication dialog.
5155 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5157 // Show the HTTP authentication dialog.
5158 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5162 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5163 // Get the preferences.
5164 boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
5165 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5167 // Get a handler for the app bar layout.
5168 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
5170 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.
5172 // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5173 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5175 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5176 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
5178 // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated.
5179 int appBarHeight = appBarLayout.getHeight();
5181 // The swipe refresh layout must be manually moved below the app bar layout.
5182 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5184 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5185 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5188 // Reset the list of resource requests.
5189 nestedScrollWebView.clearResourceRequests();
5191 // Reset the requests counters.
5192 nestedScrollWebView.resetRequestsCounters();
5194 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5195 if (nestedScrollWebView.getNightMode()) {
5196 nestedScrollWebView.setVisibility(View.INVISIBLE);
5198 nestedScrollWebView.setVisibility(View.VISIBLE);
5201 // Hide the keyboard.
5202 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5204 // Check to see if Privacy Browser is waiting on Orbot.
5205 if (!waitingForOrbot) { // Process the URL.
5206 // Get the current page position.
5207 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5209 // Update the URL text bar if the page is currently selected.
5210 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5211 // Display the formatted URL text.
5212 urlEditText.setText(url);
5214 // Apply text highlighting to `urlTextBox`.
5218 // Reset the list of host IP addresses.
5219 nestedScrollWebView.clearCurrentIpAddresses();
5221 // Get a URI for the current URL.
5222 Uri currentUri = Uri.parse(url);
5224 // Get the IP addresses for the host.
5225 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5227 // Apply any custom domain settings if the URL was loaded by navigating history.
5228 if (nestedScrollWebView.getNavigatingHistory()) {
5229 // Reset navigating history.
5230 nestedScrollWebView.setNavigatingHistory(false);
5232 // Apply the domain settings.
5233 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5235 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5236 if (userAgentChanged) {
5241 // Replace Refresh with Stop if the options menu has been created. (The WebView typically begins loading before the menu items are instantiated.)
5242 if (optionsMenu != null) {
5243 // Get a handle for the refresh menu item.
5244 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5247 refreshMenuItem.setTitle(R.string.stop);
5249 // Get the app bar and theme preferences.
5250 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5252 // If the icon is displayed in the AppBar, set it according to the theme.
5253 if (displayAdditionalAppBarIcons) {
5255 refreshMenuItem.setIcon(R.drawable.close_dark);
5257 refreshMenuItem.setIcon(R.drawable.close_light);
5265 public void onPageFinished(WebView view, String url) {
5266 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5267 if (!waitingForOrbot) {
5268 // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5269 nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5272 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5273 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
5274 CookieManager.getInstance().flush();
5277 // Update the Refresh menu item if the options menu has been created.
5278 if (optionsMenu != null) {
5279 // Get a handle for the refresh menu item.
5280 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5282 // Reset the Refresh title.
5283 refreshMenuItem.setTitle(R.string.refresh);
5285 // Get the app bar and theme preferences.
5286 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5287 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5289 // If the icon is displayed in the AppBar, reset it according to the theme.
5290 if (displayAdditionalAppBarIcons) {
5292 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5294 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5299 // Clear the cache and history if Incognito Mode is enabled.
5300 if (incognitoModeEnabled) {
5301 // Clear the cache. `true` includes disk files.
5302 nestedScrollWebView.clearCache(true);
5304 // Clear the back/forward history.
5305 nestedScrollWebView.clearHistory();
5307 // Manually delete cache folders.
5309 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5310 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5311 String privateDataDirectoryString = getApplicationInfo().dataDir;
5313 // Delete the main cache directory.
5314 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5316 // Delete the secondary `Service Worker` cache directory.
5317 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5318 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5319 } catch (IOException e) {
5320 // Do nothing if an error is thrown.
5324 // Update the URL text box and apply domain settings if not waiting on Orbot.
5325 if (!waitingForOrbot) {
5326 // Get the current page position.
5327 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5329 // Update the URL text bar if the page is currently selected.
5330 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5331 // Check to see if the URL is `about:blank`.
5332 if (url.equals("about:blank")) { // The WebView is blank.
5333 // Display the hint in the URL edit text.
5334 urlEditText.setText("");
5336 // Request focus for the URL text box.
5337 urlEditText.requestFocus();
5339 // Display the keyboard.
5340 inputMethodManager.showSoftInput(urlEditText, 0);
5342 // Hide the WebView, which causes the default background color to be displayed according to the theme.
5343 nestedScrollWebView.setVisibility(View.INVISIBLE);
5345 // Apply the domain settings. This clears any settings from the previous domain.
5346 applyDomainSettings(nestedScrollWebView, "", true, false);
5347 } else { // The WebView has loaded a webpage.
5348 // Only update the URL text box if the user is not typing in it.
5349 if (!urlEditText.hasFocus()) {
5350 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5351 urlEditText.setText(nestedScrollWebView.getUrl());
5353 // Apply text highlighting to the URL.
5359 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5360 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5361 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5362 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5367 // Handle SSL Certificate errors.
5369 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5370 // Get the current website SSL certificate.
5371 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5373 // Extract the individual pieces of information from the current website SSL certificate.
5374 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5375 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5376 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5377 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5378 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5379 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5380 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5381 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5383 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5384 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5385 // Get the pinned SSL certificate.
5386 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5388 // Extract the arrays from the array list.
5389 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5390 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5392 // Check if the current SSL certificate matches the pinned certificate.
5393 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5394 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5395 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5396 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5398 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5401 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5402 // Store the SSL error handler.
5403 nestedScrollWebView.setSslErrorHandler(handler);
5405 // Instantiate an SSL certificate error alert dialog.
5406 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
5408 // Show the SSL certificate error dialog.
5409 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5414 // Check to see if this is the first page.
5415 if (pageNumber == 0) {
5416 // Set this nested scroll WebView as the current WebView.
5417 currentWebView = nestedScrollWebView;
5419 // Apply the app settings from the shared preferences.
5422 // Load the website if not waiting for Orbot to connect.
5423 if (!waitingForOrbot) {
5424 // Get the intent that started the app.
5425 Intent launchingIntent = getIntent();
5427 // Get the information from the intent.
5428 String launchingIntentAction = launchingIntent.getAction();
5429 Uri launchingIntentUriData = launchingIntent.getData();
5431 // If the intent action is a web search, perform the search.
5432 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
5433 // Create an encoded URL string.
5434 String encodedUrlString;
5436 // Sanitize the search input and convert it to a search.
5438 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
5439 } catch (UnsupportedEncodingException exception) {
5440 encodedUrlString = "";
5443 // Load the completed search URL.
5444 loadUrl(searchURL + encodedUrlString);
5445 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
5446 // Load the URL from the intent.
5447 loadUrl(launchingIntentUriData.toString());
5448 } else { // The is no URL in the intent.
5449 // Select the homepage based on the proxy through Orbot status.
5450 if (proxyThroughOrbot) {
5451 // Load the Tor homepage.
5452 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
5454 // Load the normal homepage.
5455 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));