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.core.app.ActivityCompat;
99 import androidx.core.content.ContextCompat;
100 import androidx.core.view.GravityCompat;
101 import androidx.drawerlayout.widget.DrawerLayout;
102 import androidx.fragment.app.DialogFragment;
103 import androidx.fragment.app.FragmentManager;
104 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
105 import androidx.viewpager.widget.ViewPager;
107 import com.google.android.material.floatingactionbutton.FloatingActionButton;
108 import com.google.android.material.navigation.NavigationView;
109 import com.google.android.material.snackbar.Snackbar;
110 import com.google.android.material.tabs.TabLayout;
112 import com.stoutner.privacybrowser.BuildConfig;
113 import com.stoutner.privacybrowser.R;
114 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
115 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
116 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
117 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
118 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
120 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
121 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
123 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
124 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
125 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
126 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
127 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
128 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
129 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
130 import com.stoutner.privacybrowser.helpers.AdHelper;
131 import com.stoutner.privacybrowser.helpers.BlockListHelper;
132 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
133 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
134 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
135 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
136 import com.stoutner.privacybrowser.views.NestedScrollWebView;
138 import java.io.ByteArrayInputStream;
139 import java.io.ByteArrayOutputStream;
141 import java.io.IOException;
142 import java.io.UnsupportedEncodingException;
143 import java.net.MalformedURLException;
145 import java.net.URLDecoder;
146 import java.net.URLEncoder;
147 import java.util.ArrayList;
148 import java.util.Date;
149 import java.util.HashMap;
150 import java.util.HashSet;
151 import java.util.List;
152 import java.util.Map;
153 import java.util.Set;
155 // TODO. New tabs are white in dark mode.
156 // TODO. Find on page.
158 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
159 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
160 DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
161 EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener {
163 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
164 public static String orbotStatus;
166 // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`.
167 public static WebViewPagerAdapter webViewPagerAdapter;
169 // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`. They are used in `onRestart()`.
170 public static boolean loadUrlOnRestart;
171 public static String urlToLoadOnRestart;
173 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
174 public static boolean restartFromBookmarksActivity;
176 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
177 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
178 public static String currentBookmarksFolder;
180 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
181 public final static int UNRECOGNIZED_USER_AGENT = -1;
182 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
183 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
184 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
185 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
186 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
190 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
191 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
192 private NestedScrollWebView currentWebView;
194 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
195 private final Map<String, String> customHeaders = new HashMap<>();
197 // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
198 private String searchURL;
200 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
201 private Menu optionsMenu;
203 // The blocklists are populated in `onCreate()` and accessed from `initializeWebView()`.
204 private ArrayList<List<String[]>> easyList;
205 private ArrayList<List<String[]>> easyPrivacy;
206 private ArrayList<List<String[]>> fanboysAnnoyanceList;
207 private ArrayList<List<String[]>> fanboysSocialList;
208 private ArrayList<List<String[]>> ultraPrivacy;
210 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
211 private String webViewDefaultUserAgent;
213 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
214 private boolean proxyThroughOrbot;
216 // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
217 private boolean incognitoModeEnabled;
219 // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
220 private boolean fullScreenBrowsingModeEnabled;
222 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
223 private boolean inFullScreenBrowsingMode;
225 // The hide app bar tracker is used in `applyAppSettings()` and `initializeWebView()`.
226 private boolean hideAppBar;
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 download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
273 private String downloadUrl;
274 private String downloadContentDisposition;
275 private long downloadContentLength;
277 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
278 private String downloadImageUrl;
280 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
281 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
282 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
285 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
286 @SuppressLint("ClickableViewAccessibility")
287 protected void onCreate(Bundle savedInstanceState) {
288 // Get a handle for the shared preferences.
289 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
291 // Get the theme and screenshot preferences.
292 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
293 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
295 // Disable screenshots if not allowed.
296 if (!allowScreenshots) {
297 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
300 // Set the activity theme.
302 setTheme(R.style.PrivacyBrowserDark);
304 setTheme(R.style.PrivacyBrowserLight);
307 // Run the default commands.
308 super.onCreate(savedInstanceState);
310 // Set the content view.
311 setContentView(R.layout.main_framelayout);
313 // Get a handle for the input method.
314 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
316 // Remove the lint warning below that the input method manager might be null.
317 assert inputMethodManager != null;
319 // Get a handle for the toolbar.
320 Toolbar toolbar = findViewById(R.id.toolbar);
322 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
323 setSupportActionBar(toolbar);
325 // Get a handle for the action bar.
326 ActionBar actionBar = getSupportActionBar();
328 // This is needed to get rid of the Android Studio warning that the action bar might be null.
329 assert actionBar != null;
331 // Add the custom layout, which shows the URL text bar.
332 actionBar.setCustomView(R.layout.url_app_bar);
333 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
335 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
336 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
337 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
338 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
340 // Get handles for the URL views.
341 EditText urlEditText = findViewById(R.id.url_edittext);
343 // Remove the formatting from `urlTextBar` when the user is editing the text.
344 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
345 if (hasFocus) { // The user is editing the URL text box.
346 // Remove the highlighting.
347 urlEditText.getText().removeSpan(redColorSpan);
348 urlEditText.getText().removeSpan(initialGrayColorSpan);
349 urlEditText.getText().removeSpan(finalGrayColorSpan);
350 } else { // The user has stopped editing the URL text box.
351 // Move to the beginning of the string.
352 urlEditText.setSelection(0);
354 // Reapply the highlighting.
359 // Set the go button on the keyboard to load the URL in `urlTextBox`.
360 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
361 // If the event is a key-down event on the `enter` button, load the URL.
362 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
363 // Load the URL into the mainWebView and consume the event.
364 loadUrlFromTextBox();
366 // If the enter key was pressed, consume the event.
369 // If any other key was pressed, do not consume the event.
374 // Initialize the Orbot status and the waiting for Orbot trackers.
375 orbotStatus = "unknown";
376 waitingForOrbot = false;
378 // Create an Orbot status `BroadcastReceiver`.
379 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
381 public void onReceive(Context context, Intent intent) {
382 // Store the content of the status message in `orbotStatus`.
383 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
385 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
386 if (orbotStatus.equals("ON") && waitingForOrbot) {
387 // Reset the waiting for Orbot status.
388 waitingForOrbot = false;
390 // Get the intent that started the app.
391 Intent launchingIntent = getIntent();
393 // Get the information from the intent.
394 String launchingIntentAction = launchingIntent.getAction();
395 Uri launchingIntentUriData = launchingIntent.getData();
397 // If the intent action is a web search, perform the search.
398 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
399 // Create an encoded URL string.
400 String encodedUrlString;
402 // Sanitize the search input and convert it to a search.
404 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
405 } catch (UnsupportedEncodingException exception) {
406 encodedUrlString = "";
409 // Load the completed search URL.
410 loadUrl(searchURL + encodedUrlString);
411 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
412 // Load the URL from the intent.
413 loadUrl(launchingIntentUriData.toString());
414 } else { // The is no URL in the intent.
415 // Select the homepage based on the proxy through Orbot status.
416 if (proxyThroughOrbot) {
417 // Load the Tor homepage.
418 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
420 // Load the normal homepage.
421 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
428 // Register `orbotStatusBroadcastReceiver` on `this` context.
429 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
431 // Instantiate the blocklist helper.
432 BlockListHelper blockListHelper = new BlockListHelper();
434 // Parse the block lists.
435 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
436 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
437 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
438 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
439 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
441 // Get handles for views that need to be modified.
442 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
443 NavigationView navigationView = findViewById(R.id.navigationview);
444 TabLayout tabLayout = findViewById(R.id.tablayout);
445 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
446 ViewPager webViewPager = findViewById(R.id.webviewpager);
447 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
448 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
449 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
450 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
451 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
453 // Listen for touches on the navigation menu.
454 navigationView.setNavigationItemSelectedListener(this);
456 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
457 Menu navigationMenu = navigationView.getMenu();
458 MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
459 MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
460 MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
461 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
462 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
464 // Initialize the web view pager adapter.
465 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
467 // Set the pager adapter on the web view pager.
468 webViewPager.setAdapter(webViewPagerAdapter);
470 // Store up to 100 tabs in memory.
471 webViewPager.setOffscreenPageLimit(100);
473 // Update the web view pager every time a tab is modified.
474 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
476 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
481 public void onPageSelected(int position) {
482 // Set the current WebView.
483 setCurrentWebView(position);
485 // 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.
486 if (tabLayout.getSelectedTabPosition() != position) {
487 // Create a handler to select the tab.
488 Handler selectTabHandler = new Handler();
490 // Create a runnable select the new tab.
491 Runnable selectTabRunnable = () -> {
492 // Get a handle for the tab.
493 TabLayout.Tab tab = tabLayout.getTabAt(position);
495 // Assert that the tab is not null.
502 // Select the tab layout after 100 milliseconds, which leaves enough time for a new tab to be created.
503 selectTabHandler.postDelayed(selectTabRunnable, 100);
508 public void onPageScrollStateChanged(int state) {
513 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
514 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
516 public void onTabSelected(TabLayout.Tab tab) {
517 // Select the same page in the view pager.
518 webViewPager.setCurrentItem(tab.getPosition());
522 public void onTabUnselected(TabLayout.Tab tab) {
527 public void onTabReselected(TabLayout.Tab tab) {
528 // Instantiate the View SSL Certificate dialog.
529 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
531 // Display the View SSL Certificate dialog.
532 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
536 // Add the first tab.
539 // 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.
540 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
542 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
543 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
544 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
545 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
547 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
548 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
549 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
550 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
553 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
554 launchBookmarksActivityFab.setOnClickListener(v -> {
555 // Get a copy of the favorite icon bitmap.
556 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
558 // Create a favorite icon byte array output stream.
559 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
561 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
562 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
564 // Convert the favorite icon byte array stream to a byte array.
565 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
567 // Create an intent to launch the bookmarks activity.
568 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
570 // Add the extra information to the intent.
571 bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
572 bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
573 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
574 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
577 startActivity(bookmarksIntent);
580 // Set the create new bookmark folder FAB to display an alert dialog.
581 createBookmarkFolderFab.setOnClickListener(v -> {
582 // Create a create bookmark folder dialog.
583 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
585 // Show the create bookmark folder dialog.
586 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
589 // Set the create new bookmark FAB to display an alert dialog.
590 createBookmarkFab.setOnClickListener(view -> {
591 // Instantiate the create bookmark dialog.
592 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
594 // Display the create bookmark dialog.
595 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
598 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
599 findOnPageEditText.addTextChangedListener(new TextWatcher() {
601 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
606 public void onTextChanged(CharSequence s, int start, int before, int count) {
611 public void afterTextChanged(Editable s) {
612 // Search for the text in `mainWebView`.
613 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
617 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
618 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
619 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
620 // Hide the soft keyboard.
621 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
623 // Consume the event.
625 } else { // A different key was pressed.
626 // Do not consume the event.
631 // Implement swipe to refresh.
632 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
634 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
635 swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
637 // Set the swipe to refresh color according to the theme.
639 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
640 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
642 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
645 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
646 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
647 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
649 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
650 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
652 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
653 currentBookmarksFolder = "";
655 // Load the home folder, which is `""` in the database.
656 loadBookmarksFolder();
658 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
659 // Convert the id from long to int to match the format of the bookmarks database.
660 int databaseID = (int) id;
662 // Get the bookmark cursor for this ID and move it to the first row.
663 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
664 bookmarkCursor.moveToFirst();
666 // Act upon the bookmark according to the type.
667 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
668 // Store the new folder name in `currentBookmarksFolder`.
669 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
671 // Load the new folder.
672 loadBookmarksFolder();
673 } else { // The selected bookmark is not a folder.
674 // Load the bookmark URL.
675 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
677 // Close the bookmarks drawer.
678 drawerLayout.closeDrawer(GravityCompat.END);
681 // Close the `Cursor`.
682 bookmarkCursor.close();
685 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
686 // Convert the database ID from `long` to `int`.
687 int databaseId = (int) id;
689 // Find out if the selected bookmark is a folder.
690 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
693 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
694 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
696 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
697 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
698 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
700 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
701 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
702 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
705 // Consume the event.
709 // Get the status bar pixel size.
710 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
711 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
713 // Get the resource density.
714 float screenDensity = getResources().getDisplayMetrics().density;
716 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
717 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
718 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
719 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
721 // The drawer listener is used to update the navigation menu.`
722 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
724 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
728 public void onDrawerOpened(@NonNull View drawerView) {
732 public void onDrawerClosed(@NonNull View drawerView) {
736 public void onDrawerStateChanged(int newState) {
737 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
738 // Get handles for the drawer headers.
739 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
740 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
742 // 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.
743 if (navigationHeaderTextView != null) {
744 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
747 // 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.
748 if (bookmarksHeaderTextView != null) {
749 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
752 // Update the navigation menu items.
753 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
754 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
755 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
756 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
757 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
759 // Hide the keyboard (if displayed).
760 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
762 // 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.
763 urlEditText.clearFocus();
764 currentWebView.clearFocus();
769 // Create the hamburger icon at the start of the AppBar.
770 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
772 // 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).
773 customHeaders.put("X-Requested-With", "");
775 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
776 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
778 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
779 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
781 // Get a handle for the WebView.
782 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
784 // Store the default user agent.
785 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
787 // Destroy the bare WebView.
788 bareWebView.destroy();
792 protected void onNewIntent(Intent intent) {
793 // Get the information from the intent.
794 String intentAction = intent.getAction();
795 Uri intentUriData = intent.getData();
797 // 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.
798 if (intentUriData != null) {
799 // Sets the new intent as the activity intent, which replaces the one that originally started the app.
805 // Create a URL string.
808 // If the intent action is a web search, perform the search.
809 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
810 // Create an encoded URL string.
811 String encodedUrlString;
813 // Sanitize the search input and convert it to a search.
815 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
816 } catch (UnsupportedEncodingException exception) {
817 encodedUrlString = "";
820 // Add the base search URL.
821 url = searchURL + encodedUrlString;
822 } else { // The intent should contain a URL.
823 // Set the intent data as the URL.
824 url = intentUriData.toString();
830 // Get a handle for the drawer layout.
831 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
833 // Close the navigation drawer if it is open.
834 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
835 drawerLayout.closeDrawer(GravityCompat.START);
838 // Close the bookmarks drawer if it is open.
839 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
840 drawerLayout.closeDrawer(GravityCompat.END);
843 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
844 currentWebView.requestFocus();
849 public void onRestart() {
850 // Run the default commands.
853 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
854 if (proxyThroughOrbot) {
855 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
856 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
858 // Send the intent to the Orbot package.
859 orbotIntent.setPackage("org.torproject.android");
862 sendBroadcast(orbotIntent);
865 // Apply the app settings if returning from the Settings activity.
866 if (reapplyAppSettingsOnRestart) {
867 // Reset the reapply app settings on restart tracker.
868 reapplyAppSettingsOnRestart = false;
870 // Apply the app settings.
874 // Apply the domain settings if returning from the settings or domains activity.
875 if (reapplyDomainSettingsOnRestart) {
876 // Reset the reapply domain settings on restart tracker.
877 reapplyDomainSettingsOnRestart = false;
879 // Reapply the domain settings for each tab.
880 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
881 // Get the WebView tab fragment.
882 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
884 // Get the fragment view.
885 View fragmentView = webViewTabFragment.getView();
887 // Only reload the WebViews if they exist.
888 if (fragmentView != null) {
889 // Get the nested scroll WebView from the tab fragment.
890 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
892 // Reset the current domain name so the domain settings will be reapplied.
893 nestedScrollWebView.resetCurrentDomainName();
895 // Reapply the domain settings.
896 applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
901 // Load the URL on restart (used when loading a bookmark).
902 if (loadUrlOnRestart) {
903 // Load the specified URL.
904 loadUrl(urlToLoadOnRestart);
906 // Reset the load on restart tracker.
907 loadUrlOnRestart = false;
910 // Update the bookmarks drawer if returning from the Bookmarks activity.
911 if (restartFromBookmarksActivity) {
912 // Get a handle for the drawer layout.
913 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
915 // Close the bookmarks drawer.
916 drawerLayout.closeDrawer(GravityCompat.END);
918 // Reload the bookmarks drawer.
919 loadBookmarksFolder();
921 // Reset `restartFromBookmarksActivity`.
922 restartFromBookmarksActivity = false;
925 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
926 updatePrivacyIcons(true);
929 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
931 public void onResume() {
932 // Run the default commands.
935 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
936 // Get the WebView tab fragment.
937 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
939 // Get the fragment view.
940 View fragmentView = webViewTabFragment.getView();
942 // Only resume the WebViews if they exist (they won't when the app is first created).
943 if (fragmentView != null) {
944 // Get the nested scroll WebView from the tab fragment.
945 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
947 // Resume the nested scroll WebView JavaScript timers.
948 nestedScrollWebView.resumeTimers();
950 // Resume the nested scroll WebView.
951 nestedScrollWebView.onResume();
955 // Display a message to the user if waiting for Orbot.
956 if (waitingForOrbot && !orbotStatus.equals("ON")) {
957 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
958 currentWebView.getSettings().setUseWideViewPort(false);
960 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
961 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
964 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
965 // Get a handle for the root frame layouts.
966 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
968 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
969 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
971 /* Hide the system bars.
972 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
973 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
974 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
975 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
977 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
978 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
979 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
981 AdHelper.resumeAd(findViewById(R.id.adview));
986 public void onPause() {
987 // Run the default commands.
990 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
991 // Get the WebView tab fragment.
992 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
994 // Get the fragment view.
995 View fragmentView = webViewTabFragment.getView();
997 // Only pause the WebViews if they exist (they won't when the app is first created).
998 if (fragmentView != null) {
999 // Get the nested scroll WebView from the tab fragment.
1000 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1002 // Pause the nested scroll WebView.
1003 nestedScrollWebView.onPause();
1005 // Pause the nested scroll WebView JavaScript timers.
1006 nestedScrollWebView.pauseTimers();
1010 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1011 if (BuildConfig.FLAVOR.contentEquals("free")) {
1013 AdHelper.pauseAd(findViewById(R.id.adview));
1018 public void onDestroy() {
1019 // Unregister the Orbot status broadcast receiver.
1020 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1022 // Close the bookmarks cursor and database.
1023 bookmarksCursor.close();
1024 bookmarksDatabaseHelper.close();
1026 // Run the default commands.
1031 public boolean onCreateOptionsMenu(Menu menu) {
1032 // Inflate the menu. This adds items to the action bar if it is present.
1033 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1035 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
1038 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1039 updatePrivacyIcons(false);
1041 // Get handles for the menu items.
1042 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1043 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1044 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1045 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1046 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1047 MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
1048 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1050 // Only display third-party cookies if API >= 21
1051 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1053 // Only display the form data menu items if the API < 26.
1054 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1055 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1057 // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
1058 clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
1060 // Only show Ad Consent if this is the free flavor.
1061 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1063 // Get the shared preference values.
1064 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1066 // Get the dark theme and app bar preferences..
1067 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1068 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
1070 // 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.
1071 if (displayAdditionalAppBarIcons) {
1072 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1073 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1074 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1075 } else { //Do not display the additional icons.
1076 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1077 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1078 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1081 // Replace Refresh with Stop if a URL is already loading.
1082 if (currentWebView != null && currentWebView.getProgress() != 100) {
1084 refreshMenuItem.setTitle(R.string.stop);
1086 // If the icon is displayed in the AppBar, set it according to the theme.
1087 if (displayAdditionalAppBarIcons) {
1089 refreshMenuItem.setIcon(R.drawable.close_dark);
1091 refreshMenuItem.setIcon(R.drawable.close_light);
1100 public boolean onPrepareOptionsMenu(Menu menu) {
1101 // Get handles for the menu items.
1102 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1103 MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1104 MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1105 MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1106 MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1107 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1108 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1109 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1110 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1111 MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
1112 MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
1113 MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1114 MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1115 MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1116 MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1117 MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1118 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1119 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1120 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1121 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1122 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1124 // Get a handle for the cookie manager.
1125 CookieManager cookieManager = CookieManager.getInstance();
1127 // Initialize the current user agent string and the font size.
1128 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1131 // 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.
1132 if (currentWebView != null) {
1133 // Set the add or edit domain text.
1134 if (currentWebView.getDomainSettingsApplied()) {
1135 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1137 addOrEditDomain.setTitle(R.string.add_domain_settings);
1140 // Get the current user agent from the WebView.
1141 currentUserAgent = currentWebView.getSettings().getUserAgentString();
1143 // Get the current font size from the
1144 fontSize = currentWebView.getSettings().getTextZoom();
1146 // Set the status of the menu item checkboxes.
1147 domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1148 saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26.
1149 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1150 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1151 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1152 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1153 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1154 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1155 swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1156 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1157 nightModeMenuItem.setChecked(currentWebView.getNightMode());
1159 // Initialize the display names for the blocklists with the number of blocked requests.
1160 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1161 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1162 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1163 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1164 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1165 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1166 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1168 // Only modify third-party cookies if the API >= 21.
1169 if (Build.VERSION.SDK_INT >= 21) {
1170 // Set the status of the third-party cookies checkbox.
1171 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1173 // Enable third-party cookies if first-party cookies are enabled.
1174 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
1177 // Enable DOM Storage if JavaScript is enabled.
1178 domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1181 // Set the status of the menu item checkboxes.
1182 firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1183 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1185 // Enable Clear Cookies if there are any.
1186 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1188 // 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`.
1189 String privateDataDirectoryString = getApplicationInfo().dataDir;
1191 // Get a count of the number of files in the Local Storage directory.
1192 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1193 int localStorageDirectoryNumberOfFiles = 0;
1194 if (localStorageDirectory.exists()) {
1195 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1198 // Get a count of the number of files in the IndexedDB directory.
1199 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1200 int indexedDBDirectoryNumberOfFiles = 0;
1201 if (indexedDBDirectory.exists()) {
1202 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1205 // Enable Clear DOM Storage if there is any.
1206 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1208 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1209 if (Build.VERSION.SDK_INT < 26) {
1210 // Get the WebView database.
1211 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1213 // Enable the clear form data menu item if there is anything to clear.
1214 clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1217 // Enable Clear Data if any of the submenu items are enabled.
1218 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1220 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1221 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1223 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1224 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1225 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1226 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1227 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1228 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1229 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1230 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1231 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1232 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1233 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1234 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1235 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1236 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1237 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1238 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1239 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1240 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1241 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1242 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1243 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1244 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1245 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1246 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1247 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1248 } else { // Custom user agent.
1249 menu.findItem(R.id.user_agent_custom).setChecked(true);
1252 // Instantiate the font size title and the selected font size menu item.
1253 String fontSizeTitle;
1254 MenuItem selectedFontSizeMenuItem;
1256 // Prepare the font size title and current size menu item.
1259 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1260 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1264 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1265 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1269 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1270 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1274 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1275 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1279 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1280 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1284 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1285 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1289 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1290 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1294 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1295 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1299 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1300 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1304 // Set the font size title and select the current size menu item.
1305 fontSizeMenuItem.setTitle(fontSizeTitle);
1306 selectedFontSizeMenuItem.setChecked(true);
1308 // Run all the other default commands.
1309 super.onPrepareOptionsMenu(menu);
1311 // Display the menu.
1316 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1317 @SuppressLint("SetJavaScriptEnabled")
1318 public boolean onOptionsItemSelected(MenuItem menuItem) {
1319 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1320 if (inFullScreenBrowsingMode) {
1321 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1322 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1324 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1326 /* Hide the system bars.
1327 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1328 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1329 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1330 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1332 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1333 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1336 // Get the selected menu item ID.
1337 int menuItemId = menuItem.getItemId();
1339 // Get a handle for the shared preferences.
1340 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1342 // Get a handle for the cookie manager.
1343 CookieManager cookieManager = CookieManager.getInstance();
1345 // Run the commands that correlate to the selected menu item.
1346 switch (menuItemId) {
1347 case R.id.toggle_javascript:
1348 // Toggle the JavaScript status.
1349 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1351 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1352 updatePrivacyIcons(true);
1354 // Display a `Snackbar`.
1355 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
1356 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1357 } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled.
1358 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1359 } else { // Privacy mode.
1360 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1363 // Reload the current WebView.
1364 currentWebView.reload();
1367 case R.id.add_or_edit_domain:
1368 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1369 // Reapply the domain settings on returning to `MainWebViewActivity`.
1370 reapplyDomainSettingsOnRestart = true;
1372 // Create an intent to launch the domains activity.
1373 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1375 // Add the extra information to the intent.
1376 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1377 domainsIntent.putExtra("close_on_back", true);
1378 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1380 // Get the current certificate.
1381 SslCertificate sslCertificate = currentWebView.getCertificate();
1383 // Check to see if the SSL certificate is populated.
1384 if (sslCertificate != null) {
1385 // Extract the certificate to strings.
1386 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1387 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1388 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1389 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1390 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1391 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1392 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1393 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1395 // Add the certificate to the intent.
1396 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1397 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1398 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1399 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1400 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1401 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1402 domainsIntent.putExtra("ssl_start_date", startDateLong);
1403 domainsIntent.putExtra("ssl_end_date", endDateLong);
1406 // Check to see if the current IP addresses have been received.
1407 if (currentWebView.hasCurrentIpAddresses()) {
1408 // Add the current IP addresses to the intent.
1409 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1413 startActivity(domainsIntent);
1414 } else { // Add a new domain.
1415 // Apply the new domain settings on returning to `MainWebViewActivity`.
1416 reapplyDomainSettingsOnRestart = true;
1418 // Get the current domain
1419 Uri currentUri = Uri.parse(currentWebView.getUrl());
1420 String currentDomain = currentUri.getHost();
1422 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1423 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1425 // Create the domain and store the database ID.
1426 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1428 // Create an intent to launch the domains activity.
1429 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1431 // Add the extra information to the intent.
1432 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1433 domainsIntent.putExtra("close_on_back", true);
1434 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1436 // Get the current certificate.
1437 SslCertificate sslCertificate = currentWebView.getCertificate();
1439 // Check to see if the SSL certificate is populated.
1440 if (sslCertificate != null) {
1441 // Extract the certificate to strings.
1442 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1443 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1444 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1445 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1446 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1447 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1448 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1449 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1451 // Add the certificate to the intent.
1452 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1453 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1454 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1455 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1456 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1457 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1458 domainsIntent.putExtra("ssl_start_date", startDateLong);
1459 domainsIntent.putExtra("ssl_end_date", endDateLong);
1462 // Check to see if the current IP addresses have been received.
1463 if (currentWebView.hasCurrentIpAddresses()) {
1464 // Add the current IP addresses to the intent.
1465 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1469 startActivity(domainsIntent);
1473 case R.id.toggle_first_party_cookies:
1474 // Switch the first-party cookie status.
1475 cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1477 // Store the first-party cookie status.
1478 currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1480 // Update the menu checkbox.
1481 menuItem.setChecked(cookieManager.acceptCookie());
1483 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1484 updatePrivacyIcons(true);
1486 // Display a snackbar.
1487 if (cookieManager.acceptCookie()) { // First-party cookies are enabled.
1488 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1489 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1490 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1491 } else { // Privacy mode.
1492 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1495 // Reload the current WebView.
1496 currentWebView.reload();
1499 case R.id.toggle_third_party_cookies:
1500 if (Build.VERSION.SDK_INT >= 21) {
1501 // Switch the status of thirdPartyCookiesEnabled.
1502 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1504 // Update the menu checkbox.
1505 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1507 // Display a snackbar.
1508 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1509 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1511 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1514 // Reload the current WebView.
1515 currentWebView.reload();
1516 } // Else do nothing because SDK < 21.
1519 case R.id.toggle_dom_storage:
1520 // Toggle the status of domStorageEnabled.
1521 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1523 // Update the menu checkbox.
1524 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1526 // Update the privacy icon. `true` refreshes the app bar icons.
1527 updatePrivacyIcons(true);
1529 // Display a snackbar.
1530 if (currentWebView.getSettings().getDomStorageEnabled()) {
1531 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1533 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1536 // Reload the current WebView.
1537 currentWebView.reload();
1540 // Form data can be removed once the minimum API >= 26.
1541 case R.id.toggle_save_form_data:
1542 // Switch the status of saveFormDataEnabled.
1543 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1545 // Update the menu checkbox.
1546 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1548 // Display a snackbar.
1549 if (currentWebView.getSettings().getSaveFormData()) {
1550 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1552 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1555 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1556 updatePrivacyIcons(true);
1558 // Reload the current WebView.
1559 currentWebView.reload();
1562 case R.id.clear_cookies:
1563 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1564 .setAction(R.string.undo, v -> {
1565 // Do nothing because everything will be handled by `onDismissed()` below.
1567 .addCallback(new Snackbar.Callback() {
1568 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1570 public void onDismissed(Snackbar snackbar, int event) {
1571 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1572 // Delete the cookies, which command varies by SDK.
1573 if (Build.VERSION.SDK_INT < 21) {
1574 cookieManager.removeAllCookie();
1576 cookieManager.removeAllCookies(null);
1584 case R.id.clear_dom_storage:
1585 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1586 .setAction(R.string.undo, v -> {
1587 // Do nothing because everything will be handled by `onDismissed()` below.
1589 .addCallback(new Snackbar.Callback() {
1590 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1592 public void onDismissed(Snackbar snackbar, int event) {
1593 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1594 // Delete the DOM Storage.
1595 WebStorage webStorage = WebStorage.getInstance();
1596 webStorage.deleteAllData();
1598 // Initialize a handler to manually delete the DOM storage files and directories.
1599 Handler deleteDomStorageHandler = new Handler();
1601 // Setup a runnable to manually delete the DOM storage files and directories.
1602 Runnable deleteDomStorageRunnable = () -> {
1604 // Get a handle for the runtime.
1605 Runtime runtime = Runtime.getRuntime();
1607 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1608 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1609 String privateDataDirectoryString = getApplicationInfo().dataDir;
1611 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1612 Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1614 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1615 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1616 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1617 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1618 Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1620 // Wait for the processes to finish.
1621 deleteLocalStorageProcess.waitFor();
1622 deleteIndexProcess.waitFor();
1623 deleteQuotaManagerProcess.waitFor();
1624 deleteQuotaManagerJournalProcess.waitFor();
1625 deleteDatabasesProcess.waitFor();
1626 } catch (Exception exception) {
1627 // Do nothing if an error is thrown.
1631 // Manually delete the DOM storage files after 200 milliseconds.
1632 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1639 // Form data can be remove once the minimum API >= 26.
1640 case R.id.clear_form_data:
1641 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1642 .setAction(R.string.undo, v -> {
1643 // Do nothing because everything will be handled by `onDismissed()` below.
1645 .addCallback(new Snackbar.Callback() {
1646 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1648 public void onDismissed(Snackbar snackbar, int event) {
1649 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1650 // Delete the form data.
1651 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1652 mainWebViewDatabase.clearFormData();
1660 // Toggle the EasyList status.
1661 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1663 // Update the menu checkbox.
1664 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1666 // Reload the current WebView.
1667 currentWebView.reload();
1670 case R.id.easyprivacy:
1671 // Toggle the EasyPrivacy status.
1672 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1674 // Update the menu checkbox.
1675 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1677 // Reload the current WebView.
1678 currentWebView.reload();
1681 case R.id.fanboys_annoyance_list:
1682 // Toggle Fanboy's Annoyance List status.
1683 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1685 // Update the menu checkbox.
1686 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1688 // Update the staus of Fanboy's Social Blocking List.
1689 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1690 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1692 // Reload the current WebView.
1693 currentWebView.reload();
1696 case R.id.fanboys_social_blocking_list:
1697 // Toggle Fanboy's Social Blocking List status.
1698 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1700 // Update the menu checkbox.
1701 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1703 // Reload the current WebView.
1704 currentWebView.reload();
1707 case R.id.ultraprivacy:
1708 // Toggle the UltraPrivacy status.
1709 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1711 // Update the menu checkbox.
1712 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1714 // Reload the current WebView.
1715 currentWebView.reload();
1718 case R.id.block_all_third_party_requests:
1719 //Toggle the third-party requests blocker status.
1720 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1722 // Update the menu checkbox.
1723 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1725 // Reload the current WebView.
1726 currentWebView.reload();
1729 case R.id.user_agent_privacy_browser:
1730 // Update the user agent.
1731 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1733 // Reload the current WebView.
1734 currentWebView.reload();
1737 case R.id.user_agent_webview_default:
1738 // Update the user agent.
1739 currentWebView.getSettings().setUserAgentString("");
1741 // Reload the current WebView.
1742 currentWebView.reload();
1745 case R.id.user_agent_firefox_on_android:
1746 // Update the user agent.
1747 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1749 // Reload the current WebView.
1750 currentWebView.reload();
1753 case R.id.user_agent_chrome_on_android:
1754 // Update the user agent.
1755 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1757 // Reload the current WebView.
1758 currentWebView.reload();
1761 case R.id.user_agent_safari_on_ios:
1762 // Update the user agent.
1763 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1765 // Reload the current WebView.
1766 currentWebView.reload();
1769 case R.id.user_agent_firefox_on_linux:
1770 // Update the user agent.
1771 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1773 // Reload the current WebView.
1774 currentWebView.reload();
1777 case R.id.user_agent_chromium_on_linux:
1778 // Update the user agent.
1779 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1781 // Reload the current WebView.
1782 currentWebView.reload();
1785 case R.id.user_agent_firefox_on_windows:
1786 // Update the user agent.
1787 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1789 // Reload the current WebView.
1790 currentWebView.reload();
1793 case R.id.user_agent_chrome_on_windows:
1794 // Update the user agent.
1795 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1797 // Reload the current WebView.
1798 currentWebView.reload();
1801 case R.id.user_agent_edge_on_windows:
1802 // Update the user agent.
1803 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1805 // Reload the current WebView.
1806 currentWebView.reload();
1809 case R.id.user_agent_internet_explorer_on_windows:
1810 // Update the user agent.
1811 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1813 // Reload the current WebView.
1814 currentWebView.reload();
1817 case R.id.user_agent_safari_on_macos:
1818 // Update the user agent.
1819 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1821 // Reload the current WebView.
1822 currentWebView.reload();
1825 case R.id.user_agent_custom:
1826 // Update the user agent.
1827 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1829 // Reload the current WebView.
1830 currentWebView.reload();
1833 case R.id.font_size_twenty_five_percent:
1834 currentWebView.getSettings().setTextZoom(25);
1837 case R.id.font_size_fifty_percent:
1838 currentWebView.getSettings().setTextZoom(50);
1841 case R.id.font_size_seventy_five_percent:
1842 currentWebView.getSettings().setTextZoom(75);
1845 case R.id.font_size_one_hundred_percent:
1846 currentWebView.getSettings().setTextZoom(100);
1849 case R.id.font_size_one_hundred_twenty_five_percent:
1850 currentWebView.getSettings().setTextZoom(125);
1853 case R.id.font_size_one_hundred_fifty_percent:
1854 currentWebView.getSettings().setTextZoom(150);
1857 case R.id.font_size_one_hundred_seventy_five_percent:
1858 currentWebView.getSettings().setTextZoom(175);
1861 case R.id.font_size_two_hundred_percent:
1862 currentWebView.getSettings().setTextZoom(200);
1865 case R.id.swipe_to_refresh:
1866 // Toggle the stored status of swipe to refresh.
1867 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1869 // Get a handle for the swipe refresh layout.
1870 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1872 // Update the swipe refresh layout.
1873 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
1874 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.
1875 // Only enable the swipe refresh layout if the WebView is scrolled to the top.
1876 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1877 } else { // For API < 23, the swipe refresh layout is always enabled.
1878 // Enable the swipe refresh layout.
1879 swipeRefreshLayout.setEnabled(true);
1881 } else { // Swipe to refresh is disabled.
1882 // Disable the swipe refresh layout.
1883 swipeRefreshLayout.setEnabled(false);
1887 case R.id.display_images:
1888 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1889 // Disable loading of images.
1890 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1892 // Reload the website to remove existing images.
1893 currentWebView.reload();
1894 } else { // Images are not currently loaded automatically.
1895 // Enable loading of images. Missing images will be loaded without the need for a reload.
1896 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1900 case R.id.night_mode:
1901 // Toggle night mode.
1902 currentWebView.setNightMode(!currentWebView.getNightMode());
1904 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1905 if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript.
1906 // Enable JavaScript.
1907 currentWebView.getSettings().setJavaScriptEnabled(true);
1908 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
1909 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1910 currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
1911 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
1912 // Apply the JavaScript preference.
1913 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1916 // Update the privacy icons.
1917 updatePrivacyIcons(false);
1919 // Reload the website.
1920 currentWebView.reload();
1923 case R.id.find_on_page:
1924 // Get a handle for the views.
1925 Toolbar toolbar = findViewById(R.id.toolbar);
1926 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1927 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1929 // Hide the toolbar.
1930 toolbar.setVisibility(View.GONE);
1932 // Show the find on page linear layout.
1933 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1935 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1936 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1937 findOnPageEditText.postDelayed(() -> {
1938 // Set the focus on `findOnPageEditText`.
1939 findOnPageEditText.requestFocus();
1941 // Get a handle for the input method manager.
1942 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1944 // Remove the lint warning below that the input method manager might be null.
1945 assert inputMethodManager != null;
1947 // Display the keyboard. `0` sets no input flags.
1948 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1952 case R.id.view_source:
1953 // Create an intent to launch the view source activity.
1954 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1956 // Add the variables to the intent.
1957 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1958 viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
1961 startActivity(viewSourceIntent);
1964 case R.id.share_url:
1965 // Setup the share string.
1966 String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1968 // Create the share intent.
1969 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1970 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1971 shareIntent.setType("text/plain");
1974 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1978 // Get a print manager instance.
1979 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1981 // Remove the lint error below that print manager might be null.
1982 assert printManager != null;
1984 // Create a print document adapter from the current WebView.
1985 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1987 // Print the document.
1988 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
1991 case R.id.open_with_app:
1992 openWithApp(currentWebView.getUrl());
1995 case R.id.open_with_browser:
1996 openWithBrowser(currentWebView.getUrl());
1999 case R.id.add_to_homescreen:
2000 // Instantiate the create home screen shortcut dialog.
2001 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
2002 currentWebView.getFavoriteOrDefaultIcon());
2004 // Show the create home screen shortcut dialog.
2005 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2008 case R.id.proxy_through_orbot:
2009 // Toggle the proxy through Orbot variable.
2010 proxyThroughOrbot = !proxyThroughOrbot;
2012 // Apply the proxy through Orbot settings.
2013 applyProxyThroughOrbot(true);
2017 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2018 // Reload the current WebView.
2019 currentWebView.reload();
2020 } else { // The stop button was pushed.
2021 // Stop the loading of the WebView.
2022 currentWebView.stopLoading();
2026 case R.id.ad_consent:
2027 // Display the ad consent dialog.
2028 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2029 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2033 // Don't consume the event.
2034 return super.onOptionsItemSelected(menuItem);
2038 // removeAllCookies is deprecated, but it is required for API < 21.
2040 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2041 // Get the menu item ID.
2042 int menuItemId = menuItem.getItemId();
2044 // Get a handle for the shared preferences.
2045 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2047 // Run the commands that correspond to the selected menu item.
2048 switch (menuItemId) {
2049 case R.id.close_tab:
2050 // Get a handle for the tab layout and the view pager.
2051 TabLayout tabLayout = findViewById(R.id.tablayout);
2052 ViewPager webViewPager = findViewById(R.id.webviewpager);
2054 // Get the current tab number.
2055 int currentTabNumber = tabLayout.getSelectedTabPosition();
2057 // Delete the current tab.
2058 tabLayout.removeTabAt(currentTabNumber);
2060 // 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.
2061 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
2062 setCurrentWebView(currentTabNumber);
2066 case R.id.clear_and_exit:
2067 // Close the bookmarks cursor and database.
2068 bookmarksCursor.close();
2069 bookmarksDatabaseHelper.close();
2071 // Get the status of the clear everything preference.
2072 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2074 // Get a handle for the runtime.
2075 Runtime runtime = Runtime.getRuntime();
2077 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
2078 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
2079 String privateDataDirectoryString = getApplicationInfo().dataDir;
2082 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2083 // The command to remove cookies changed slightly in API 21.
2084 if (Build.VERSION.SDK_INT >= 21) {
2085 CookieManager.getInstance().removeAllCookies(null);
2087 CookieManager.getInstance().removeAllCookie();
2090 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2092 // Two commands must be used because `Runtime.exec()` does not like `*`.
2093 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2094 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2096 // Wait until the processes have finished.
2097 deleteCookiesProcess.waitFor();
2098 deleteCookiesJournalProcess.waitFor();
2099 } catch (Exception exception) {
2100 // Do nothing if an error is thrown.
2104 // Clear DOM storage.
2105 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2106 // Ask `WebStorage` to clear the DOM storage.
2107 WebStorage webStorage = WebStorage.getInstance();
2108 webStorage.deleteAllData();
2110 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2112 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2113 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2115 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2116 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2117 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2118 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2119 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2121 // Wait until the processes have finished.
2122 deleteLocalStorageProcess.waitFor();
2123 deleteIndexProcess.waitFor();
2124 deleteQuotaManagerProcess.waitFor();
2125 deleteQuotaManagerJournalProcess.waitFor();
2126 deleteDatabaseProcess.waitFor();
2127 } catch (Exception exception) {
2128 // Do nothing if an error is thrown.
2132 // Clear form data if the API < 26.
2133 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2134 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2135 webViewDatabase.clearFormData();
2137 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2139 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2140 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2141 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2143 // Wait until the processes have finished.
2144 deleteWebDataProcess.waitFor();
2145 deleteWebDataJournalProcess.waitFor();
2146 } catch (Exception exception) {
2147 // Do nothing if an error is thrown.
2152 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2153 // Clear the cache from each WebView.
2154 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2155 // Get the WebView tab fragment.
2156 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2158 // Get the fragment view.
2159 View fragmentView = webViewTabFragment.getView();
2161 // Only clear the cache if the WebView exists.
2162 if (fragmentView != null) {
2163 // Get the nested scroll WebView from the tab fragment.
2164 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2166 // Clear the cache for this WebView.
2167 nestedScrollWebView.clearCache(true);
2171 // Manually delete the cache directories.
2173 // Delete the main cache directory.
2174 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2176 // Delete the secondary `Service Worker` cache directory.
2177 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2178 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2180 // Wait until the processes have finished.
2181 deleteCacheProcess.waitFor();
2182 deleteServiceWorkerProcess.waitFor();
2183 } catch (Exception exception) {
2184 // Do nothing if an error is thrown.
2188 // Wipe out each WebView.
2189 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2190 // Get the WebView tab fragment.
2191 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2193 // Get the fragment view.
2194 View fragmentView = webViewTabFragment.getView();
2196 // Only wipe out the WebView if it exists.
2197 if (fragmentView != null) {
2198 // Get the nested scroll WebView from the tab fragment.
2199 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2201 // Clear SSL certificate preferences for this WebView.
2202 nestedScrollWebView.clearSslPreferences();
2204 // Clear the back/forward history for this WebView.
2205 nestedScrollWebView.clearHistory();
2207 // Destroy the internal state of `mainWebView`.
2208 nestedScrollWebView.destroy();
2212 // Clear the custom headers.
2213 customHeaders.clear();
2215 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2216 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2217 if (clearEverything) {
2219 // Delete the folder.
2220 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2222 // Wait until the process has finished.
2223 deleteAppWebviewProcess.waitFor();
2224 } catch (Exception exception) {
2225 // Do nothing if an error is thrown.
2229 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2230 if (Build.VERSION.SDK_INT >= 21) {
2231 finishAndRemoveTask();
2236 // Remove the terminated program from RAM. The status code is `0`.
2241 // Select the homepage based on the proxy through Orbot status.
2242 if (proxyThroughOrbot) {
2243 // Load the Tor homepage.
2244 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
2246 // Load the normal homepage.
2247 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2252 if (currentWebView.canGoBack()) {
2253 // Reset the current domain name so that navigation works if third-party requests are blocked.
2254 currentWebView.resetCurrentDomainName();
2256 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2257 currentWebView.setNavigatingHistory(true);
2259 // Load the previous website in the history.
2260 currentWebView.goBack();
2265 if (currentWebView.canGoForward()) {
2266 // Reset the current domain name so that navigation works if third-party requests are blocked.
2267 currentWebView.resetCurrentDomainName();
2269 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2270 currentWebView.setNavigatingHistory(true);
2272 // Load the next website in the history.
2273 currentWebView.goForward();
2278 // Instantiate the URL history dialog.
2279 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2281 // Show the URL history dialog.
2282 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2286 // Populate the resource requests.
2287 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2289 // Create an intent to launch the Requests activity.
2290 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2292 // Add the block third-party requests status to the intent.
2293 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2296 startActivity(requestsIntent);
2299 case R.id.downloads:
2300 // Launch the system Download Manager.
2301 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2303 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2304 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2306 startActivity(downloadManagerIntent);
2310 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2311 reapplyDomainSettingsOnRestart = true;
2313 // Launch the domains activity.
2314 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2316 // Add the extra information to the intent.
2317 domainsIntent.putExtra("current_url", currentWebView.getUrl());
2319 // Get the current certificate.
2320 SslCertificate sslCertificate = currentWebView.getCertificate();
2322 // Check to see if the SSL certificate is populated.
2323 if (sslCertificate != null) {
2324 // Extract the certificate to strings.
2325 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2326 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2327 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2328 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2329 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2330 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2331 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2332 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2334 // Add the certificate to the intent.
2335 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2336 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2337 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2338 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2339 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2340 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2341 domainsIntent.putExtra("ssl_start_date", startDateLong);
2342 domainsIntent.putExtra("ssl_end_date", endDateLong);
2345 // Check to see if the current IP addresses have been received.
2346 if (currentWebView.hasCurrentIpAddresses()) {
2347 // Add the current IP addresses to the intent.
2348 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2352 startActivity(domainsIntent);
2356 // Set the flag to reapply app settings on restart when returning from Settings.
2357 reapplyAppSettingsOnRestart = true;
2359 // Set the flag to reapply the domain settings on restart when returning from Settings.
2360 reapplyDomainSettingsOnRestart = true;
2362 // Launch the settings activity.
2363 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2364 startActivity(settingsIntent);
2367 case R.id.import_export:
2368 // Launch the import/export activity.
2369 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2370 startActivity(importExportIntent);
2374 // Launch the logcat activity.
2375 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2376 startActivity(logcatIntent);
2380 // Launch `GuideActivity`.
2381 Intent guideIntent = new Intent(this, GuideActivity.class);
2382 startActivity(guideIntent);
2386 // Create an intent to launch the about activity.
2387 Intent aboutIntent = new Intent(this, AboutActivity.class);
2389 // Create a string array for the blocklist versions.
2390 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],
2391 ultraPrivacy.get(0).get(0)[0]};
2393 // Add the blocklist versions to the intent.
2394 aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2397 startActivity(aboutIntent);
2401 // Get a handle for the drawer layout.
2402 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2404 // Close the navigation drawer.
2405 drawerLayout.closeDrawer(GravityCompat.START);
2410 public void onPostCreate(Bundle savedInstanceState) {
2411 // Run the default commands.
2412 super.onPostCreate(savedInstanceState);
2414 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2415 actionBarDrawerToggle.syncState();
2419 public void onConfigurationChanged(Configuration newConfig) {
2420 // Run the default commands.
2421 super.onConfigurationChanged(newConfig);
2423 // Get the status bar pixel size.
2424 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2425 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2427 // Get the resource density.
2428 float screenDensity = getResources().getDisplayMetrics().density;
2430 // Recalculate the drawer header padding.
2431 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2432 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2433 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2435 // Reload the ad for the free flavor if not in full screen mode.
2436 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2437 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2438 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2441 // `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:
2442 // https://code.google.com/p/android/issues/detail?id=20493#c8
2443 // ActivityCompat.invalidateOptionsMenu(this);
2447 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2448 // Store the hit test result.
2449 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2451 // Create the URL strings.
2452 final String imageUrl;
2453 final String linkUrl;
2455 // Get handles for the system managers.
2456 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2457 FragmentManager fragmentManager = getSupportFragmentManager();
2458 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2460 // Remove the lint errors below that the clipboard manager might be null.
2461 assert clipboardManager != null;
2463 // Process the link according to the type.
2464 switch (hitTestResult.getType()) {
2465 // `SRC_ANCHOR_TYPE` is a link.
2466 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2467 // Get the target URL.
2468 linkUrl = hitTestResult.getExtra();
2470 // Set the target URL as the title of the `ContextMenu`.
2471 menu.setHeaderTitle(linkUrl);
2473 // Add a Load URL entry.
2474 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2483 // Add an Open with App entry.
2484 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2485 openWithApp(linkUrl);
2489 // Add an Open with Browser entry.
2490 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2491 openWithBrowser(linkUrl);
2495 // Add a Copy URL entry.
2496 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2497 // Save the link URL in a `ClipData`.
2498 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2500 // Set the `ClipData` as the clipboard's primary clip.
2501 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2505 // Add a Download URL entry.
2506 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2507 // Check if the download should be processed by an external app.
2508 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2509 openUrlWithExternalApp(linkUrl);
2510 } else { // Download with Android's download manager.
2511 // Check to see if the storage permission has already been granted.
2512 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2513 // Store the variables for future use by `onRequestPermissionsResult()`.
2514 downloadUrl = linkUrl;
2515 downloadContentDisposition = "none";
2516 downloadContentLength = -1;
2518 // Show a dialog if the user has previously denied the permission.
2519 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2520 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2521 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2523 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2524 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2525 } else { // Show the permission request directly.
2526 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2527 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2529 } else { // The storage permission has already been granted.
2530 // Get a handle for the download file alert dialog.
2531 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2533 // Show the download file alert dialog.
2534 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2540 // Add a Cancel entry, which by default closes the context menu.
2541 menu.add(R.string.cancel);
2544 case WebView.HitTestResult.EMAIL_TYPE:
2545 // Get the target URL.
2546 linkUrl = hitTestResult.getExtra();
2548 // Set the target URL as the title of the `ContextMenu`.
2549 menu.setHeaderTitle(linkUrl);
2551 // Add a Write Email entry.
2552 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2553 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2554 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2556 // Parse the url and set it as the data for the `Intent`.
2557 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2559 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2560 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2563 startActivity(emailIntent);
2567 // Add a Copy Email Address entry.
2568 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2569 // Save the email address in a `ClipData`.
2570 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2572 // Set the `ClipData` as the clipboard's primary clip.
2573 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2577 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2578 menu.add(R.string.cancel);
2581 // `IMAGE_TYPE` is an image.
2582 case WebView.HitTestResult.IMAGE_TYPE:
2583 // Get the image URL.
2584 imageUrl = hitTestResult.getExtra();
2586 // Set the image URL as the title of the `ContextMenu`.
2587 menu.setHeaderTitle(imageUrl);
2589 // Add a View Image entry.
2590 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2595 // Add a Download Image entry.
2596 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2597 // Check if the download should be processed by an external app.
2598 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2599 openUrlWithExternalApp(imageUrl);
2600 } else { // Download with Android's download manager.
2601 // Check to see if the storage permission has already been granted.
2602 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2603 // Store the image URL for use by `onRequestPermissionResult()`.
2604 downloadImageUrl = imageUrl;
2606 // Show a dialog if the user has previously denied the permission.
2607 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2608 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2609 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2611 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2612 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2613 } else { // Show the permission request directly.
2614 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2615 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2617 } else { // The storage permission has already been granted.
2618 // Get a handle for the download image alert dialog.
2619 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2621 // Show the download image alert dialog.
2622 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2628 // Add a Copy URL entry.
2629 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2630 // Save the image URL in a `ClipData`.
2631 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2633 // Set the `ClipData` as the clipboard's primary clip.
2634 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2638 // Add an Open with App entry.
2639 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2640 openWithApp(imageUrl);
2644 // Add an Open with Browser entry.
2645 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2646 openWithBrowser(imageUrl);
2650 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2651 menu.add(R.string.cancel);
2655 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2656 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2657 // Get the image URL.
2658 imageUrl = hitTestResult.getExtra();
2660 // Set the image URL as the title of the `ContextMenu`.
2661 menu.setHeaderTitle(imageUrl);
2663 // Add a `View Image` entry.
2664 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2669 // Add a `Download Image` entry.
2670 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2671 // Check if the download should be processed by an external app.
2672 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2673 openUrlWithExternalApp(imageUrl);
2674 } else { // Download with Android's download manager.
2675 // Check to see if the storage permission has already been granted.
2676 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2677 // Store the image URL for use by `onRequestPermissionResult()`.
2678 downloadImageUrl = imageUrl;
2680 // Show a dialog if the user has previously denied the permission.
2681 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2682 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2683 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2685 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2686 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2687 } else { // Show the permission request directly.
2688 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2689 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2691 } else { // The storage permission has already been granted.
2692 // Get a handle for the download image alert dialog.
2693 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2695 // Show the download image alert dialog.
2696 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2702 // Add a `Copy URL` entry.
2703 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2704 // Save the image URL in a `ClipData`.
2705 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2707 // Set the `ClipData` as the clipboard's primary clip.
2708 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2712 // Add an Open with App entry.
2713 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2714 openWithApp(imageUrl);
2718 // Add an Open with Browser entry.
2719 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2720 openWithBrowser(imageUrl);
2724 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2725 menu.add(R.string.cancel);
2731 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2732 // Get a handle for the bookmarks list view.
2733 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2735 // Get the views from the dialog fragment.
2736 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2737 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2739 // Extract the strings from the edit texts.
2740 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2741 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2743 // Create a favorite icon byte array output stream.
2744 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2746 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2747 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2749 // Convert the favorite icon byte array stream to a byte array.
2750 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2752 // Display the new bookmark below the current items in the (0 indexed) list.
2753 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2755 // Create the bookmark.
2756 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2758 // Update the bookmarks cursor with the current contents of this folder.
2759 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2761 // Update the list view.
2762 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2764 // Scroll to the new bookmark.
2765 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2769 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2770 // Get a handle for the bookmarks list view.
2771 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2773 // Get handles for the views in the dialog fragment.
2774 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2775 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2776 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2778 // Get new folder name string.
2779 String folderNameString = createFolderNameEditText.getText().toString();
2781 // Create a folder icon bitmap.
2782 Bitmap folderIconBitmap;
2784 // Set the folder icon bitmap according to the dialog.
2785 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2786 // Get the default folder icon drawable.
2787 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2789 // Convert the folder icon drawable to a bitmap drawable.
2790 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2792 // Convert the folder icon bitmap drawable to a bitmap.
2793 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2794 } else { // Use the WebView favorite icon.
2795 // Copy the favorite icon bitmap to the folder icon bitmap.
2796 folderIconBitmap = favoriteIconBitmap;
2799 // Create a folder icon byte array output stream.
2800 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2802 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2803 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2805 // Convert the folder icon byte array stream to a byte array.
2806 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2808 // Move all the bookmarks down one in the display order.
2809 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2810 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2811 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2814 // Create the folder, which will be placed at the top of the `ListView`.
2815 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2817 // Update the bookmarks cursor with the current contents of this folder.
2818 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2820 // Update the `ListView`.
2821 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2823 // Scroll to the new folder.
2824 bookmarksListView.setSelection(0);
2828 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2829 // Get handles for the views from `dialogFragment`.
2830 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2831 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2832 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2834 // Store the bookmark strings.
2835 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2836 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2838 // Update the bookmark.
2839 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2840 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2841 } else { // Update the bookmark using the `WebView` favorite icon.
2842 // Create a favorite icon byte array output stream.
2843 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2845 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2846 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2848 // Convert the favorite icon byte array stream to a byte array.
2849 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2851 // Update the bookmark and the favorite icon.
2852 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2855 // Update the bookmarks cursor with the current contents of this folder.
2856 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2858 // Update the list view.
2859 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2863 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2864 // Get handles for the views from `dialogFragment`.
2865 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2866 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2867 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2868 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2870 // Get the new folder name.
2871 String newFolderNameString = editFolderNameEditText.getText().toString();
2873 // Check if the favorite icon has changed.
2874 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2875 // Update the name in the database.
2876 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2877 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2878 // Create the new folder icon Bitmap.
2879 Bitmap folderIconBitmap;
2881 // Populate the new folder icon bitmap.
2882 if (defaultFolderIconRadioButton.isChecked()) {
2883 // Get the default folder icon drawable.
2884 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2886 // Convert the folder icon drawable to a bitmap drawable.
2887 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2889 // Convert the folder icon bitmap drawable to a bitmap.
2890 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2891 } else { // Use the `WebView` favorite icon.
2892 // Copy the favorite icon bitmap to the folder icon bitmap.
2893 folderIconBitmap = favoriteIconBitmap;
2896 // Create a folder icon byte array output stream.
2897 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2899 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2900 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2902 // Convert the folder icon byte array stream to a byte array.
2903 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2905 // Update the folder icon in the database.
2906 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2907 } else { // The folder icon and the name have changed.
2908 // Get the new folder icon `Bitmap`.
2909 Bitmap folderIconBitmap;
2910 if (defaultFolderIconRadioButton.isChecked()) {
2911 // Get the default folder icon drawable.
2912 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2914 // Convert the folder icon drawable to a bitmap drawable.
2915 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2917 // Convert the folder icon bitmap drawable to a bitmap.
2918 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2919 } else { // Use the `WebView` favorite icon.
2920 // Copy the favorite icon bitmap to the folder icon bitmap.
2921 folderIconBitmap = favoriteIconBitmap;
2924 // Create a folder icon byte array output stream.
2925 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2927 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2928 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2930 // Convert the folder icon byte array stream to a byte array.
2931 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2933 // Update the folder name and icon in the database.
2934 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2937 // Update the bookmarks cursor with the current contents of this folder.
2938 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2940 // Update the `ListView`.
2941 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2945 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2946 switch (downloadType) {
2947 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2948 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2949 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2952 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2953 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2954 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2960 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2961 // Get a handle for the fragment manager.
2962 FragmentManager fragmentManager = getSupportFragmentManager();
2964 switch (requestCode) {
2965 case DOWNLOAD_FILE_REQUEST_CODE:
2966 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2967 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2969 // On API 23, displaying the fragment must be delayed or the app will crash.
2970 if (Build.VERSION.SDK_INT == 23) {
2971 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2973 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2976 // Reset the download variables.
2978 downloadContentDisposition = "";
2979 downloadContentLength = 0;
2982 case DOWNLOAD_IMAGE_REQUEST_CODE:
2983 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2984 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
2986 // On API 23, displaying the fragment must be delayed or the app will crash.
2987 if (Build.VERSION.SDK_INT == 23) {
2988 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2990 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2993 // Reset the image URL variable.
2994 downloadImageUrl = "";
3000 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
3001 // Download the image if it has an HTTP or HTTPS URI.
3002 if (imageUrl.startsWith("http")) {
3003 // Get a handle for the system `DOWNLOAD_SERVICE`.
3004 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3006 // Parse `imageUrl`.
3007 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3009 // Get a handle for the cookie manager.
3010 CookieManager cookieManager = CookieManager.getInstance();
3012 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
3013 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3014 if (cookieManager.acceptCookie()) {
3015 // Get the cookies for `imageUrl`.
3016 String cookies = cookieManager.getCookie(imageUrl);
3018 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3019 downloadRequest.addRequestHeader("Cookie", cookies);
3022 // Get the file name from the dialog fragment.
3023 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3024 String imageName = downloadImageNameEditText.getText().toString();
3026 // Specify the download location.
3027 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3028 // Download to the public download directory.
3029 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3030 } else { // External write permission denied.
3031 // Download to the app's external download directory.
3032 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3035 // Allow `MediaScanner` to index the download if it is a media file.
3036 downloadRequest.allowScanningByMediaScanner();
3038 // Add the URL as the description for the download.
3039 downloadRequest.setDescription(imageUrl);
3041 // Show the download notification after the download is completed.
3042 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3044 // Remove the lint warning below that `downloadManager` might be `null`.
3045 assert downloadManager != null;
3047 // Initiate the download.
3048 downloadManager.enqueue(downloadRequest);
3049 } else { // The image is not an HTTP or HTTPS URI.
3050 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3055 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3056 // Download the file if it has an HTTP or HTTPS URI.
3057 if (downloadUrl.startsWith("http")) {
3058 // Get a handle for the system `DOWNLOAD_SERVICE`.
3059 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3061 // Parse `downloadUrl`.
3062 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3064 // Get a handle for the cookie manager.
3065 CookieManager cookieManager = CookieManager.getInstance();
3067 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
3068 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3069 if (cookieManager.acceptCookie()) {
3070 // Get the cookies for `downloadUrl`.
3071 String cookies = cookieManager.getCookie(downloadUrl);
3073 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3074 downloadRequest.addRequestHeader("Cookie", cookies);
3077 // Get the file name from the dialog fragment.
3078 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3079 String fileName = downloadFileNameEditText.getText().toString();
3081 // Specify the download location.
3082 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3083 // Download to the public download directory.
3084 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3085 } else { // External write permission denied.
3086 // Download to the app's external download directory.
3087 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3090 // Allow `MediaScanner` to index the download if it is a media file.
3091 downloadRequest.allowScanningByMediaScanner();
3093 // Add the URL as the description for the download.
3094 downloadRequest.setDescription(downloadUrl);
3096 // Show the download notification after the download is completed.
3097 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3099 // Remove the lint warning below that `downloadManager` might be `null`.
3100 assert downloadManager != null;
3102 // Initiate the download.
3103 downloadManager.enqueue(downloadRequest);
3104 } else { // The download is not an HTTP or HTTPS URI.
3105 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3109 // Override `onBackPressed` to handle the navigation drawer and and the WebView.
3111 public void onBackPressed() {
3112 // Get a handle for the drawer layout.
3113 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3115 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
3116 // Close the navigation drawer.
3117 drawerLayout.closeDrawer(GravityCompat.START);
3118 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
3119 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
3120 // close the bookmarks drawer.
3121 drawerLayout.closeDrawer(GravityCompat.END);
3122 } else { // A subfolder is displayed.
3123 // Place the former parent folder in `currentFolder`.
3124 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3126 // Load the new folder.
3127 loadBookmarksFolder();
3129 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
3130 // Reset the current domain name so that navigation works if third-party requests are blocked.
3131 currentWebView.resetCurrentDomainName();
3133 // Set navigating history so that the domain settings are applied when the new URL is loaded.
3134 currentWebView.setNavigatingHistory(true);
3137 currentWebView.goBack();
3138 } else { // There is nothing else to do.
3139 // Load a blank website.
3144 // 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.
3146 public void onActivityResult(int requestCode, int resultCode, Intent data) {
3147 // File uploads only work on API >= 21.
3148 if (Build.VERSION.SDK_INT >= 21) {
3149 // Pass the file to the WebView.
3150 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3154 private void loadUrlFromTextBox() {
3155 // Get a handle for the URL edit text.
3156 EditText urlEditText = findViewById(R.id.url_edittext);
3158 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
3159 String unformattedUrlString = urlEditText.getText().toString().trim();
3161 // Initialize the formatted URL string.
3164 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
3165 if (unformattedUrlString.startsWith("content://")) { // This is a Content URL.
3166 // Load the entire content URL.
3167 url = unformattedUrlString;
3168 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
3169 unformattedUrlString.startsWith("file://")) { // This is a standard URL.
3170 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
3171 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3172 unformattedUrlString = "https://" + unformattedUrlString;
3175 // Initialize `unformattedUrl`.
3176 URL unformattedUrl = null;
3178 // 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.
3180 unformattedUrl = new URL(unformattedUrlString);
3181 } catch (MalformedURLException e) {
3182 e.printStackTrace();
3185 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3186 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3187 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3188 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3189 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3190 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3193 Uri.Builder uri = new Uri.Builder();
3194 uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3196 // Decode the URI as a UTF-8 string in.
3198 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
3199 } catch (UnsupportedEncodingException exception) {
3200 // Do nothing. The formatted URL string will remain blank.
3202 } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string.
3203 // Create an encoded URL String.
3204 String encodedUrlString;
3206 // Sanitize the search input.
3208 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3209 } catch (UnsupportedEncodingException exception) {
3210 encodedUrlString = "";
3213 // Add the base search URL.
3214 url = searchURL + encodedUrlString;
3217 // 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.
3218 urlEditText.clearFocus();
3224 private void loadUrl(String url) {
3225 // Apply the domain settings.
3226 applyDomainSettings(currentWebView, url, true, false);
3229 currentWebView.loadUrl(url, customHeaders);
3232 public void findPreviousOnPage(View view) {
3233 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
3234 currentWebView.findNext(false);
3237 public void findNextOnPage(View view) {
3238 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3239 currentWebView.findNext(true);
3242 public void closeFindOnPage(View view) {
3243 // Get a handle for the views.
3244 Toolbar toolbar = findViewById(R.id.toolbar);
3245 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3246 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3248 // Delete the contents of `find_on_page_edittext`.
3249 findOnPageEditText.setText(null);
3251 // Clear the highlighted phrases.
3252 currentWebView.clearMatches();
3254 // Hide the find on page linear layout.
3255 findOnPageLinearLayout.setVisibility(View.GONE);
3257 // Show the toolbar.
3258 toolbar.setVisibility(View.VISIBLE);
3260 // Get a handle for the input method manager.
3261 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3263 // Remove the lint warning below that the input method manager might be null.
3264 assert inputMethodManager != null;
3266 // Hide the keyboard.
3267 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3270 private void applyAppSettings() {
3271 // Get a handle for the shared preferences.
3272 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3274 // Store the values from the shared preferences in variables.
3275 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3276 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3277 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3278 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3279 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3281 // Get handles for the views that need to be modified.
3282 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3283 ActionBar actionBar = getSupportActionBar();
3284 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
3286 // Remove the incorrect lint warning below that the action bar might be null.
3287 assert actionBar != null;
3289 // Apply the proxy through Orbot settings.
3290 applyProxyThroughOrbot(false);
3292 // Set Do Not Track status.
3293 if (doNotTrackEnabled) {
3294 customHeaders.put("DNT", "1");
3296 customHeaders.remove("DNT");
3299 // Set the app bar scrolling for each WebView.
3300 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3301 // Get the WebView tab fragment.
3302 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3304 // Get the fragment view.
3305 View fragmentView = webViewTabFragment.getView();
3307 // Only modify the WebViews if they exist.
3308 if (fragmentView != null) {
3309 // Get the nested scroll WebView from the tab fragment.
3310 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3312 // Set the app bar scrolling.
3313 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
3317 // Update the full screen browsing mode settings.
3318 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3319 // Update the visibility of the app bar, which might have changed in the settings.
3321 // Hide the tab linear layout.
3322 tabsLinearLayout.setVisibility(View.GONE);
3324 // Hide the action bar.
3327 // Show the tab linear layout.
3328 tabsLinearLayout.setVisibility(View.VISIBLE);
3330 // Show the action bar.
3334 // Hide the banner ad in the free flavor.
3335 if (BuildConfig.FLAVOR.contentEquals("free")) {
3336 AdHelper.hideAd(findViewById(R.id.adview));
3339 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3340 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3342 /* Hide the system bars.
3343 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3344 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3345 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3346 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3348 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3349 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3350 } else { // Privacy Browser is not in full screen browsing mode.
3351 // 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.
3352 inFullScreenBrowsingMode = false;
3354 // Show the tab linear layout.
3355 tabsLinearLayout.setVisibility(View.VISIBLE);
3357 // Show the action bar.
3360 // Show the banner ad in the free flavor.
3361 if (BuildConfig.FLAVOR.contentEquals("free")) {
3362 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3363 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3366 // Remove the `SYSTEM_UI` flags from the root frame layout.
3367 rootFrameLayout.setSystemUiVisibility(0);
3369 // Add the translucent status flag.
3370 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3375 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3376 @SuppressLint("SetJavaScriptEnabled")
3377 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) {
3378 // Store a copy of the current user agent to track changes for the return boolean.
3379 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3381 // Parse the URL into a URI.
3382 Uri uri = Uri.parse(url);
3384 // Extract the domain from `uri`.
3385 String newHostName = uri.getHost();
3387 // Strings don't like to be null.
3388 if (newHostName == null) {
3392 // 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.
3393 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName)) {
3394 // Set the new host name as the current domain name.
3395 nestedScrollWebView.setCurrentDomainName(newHostName);
3397 // Reset the ignoring of pinned domain information.
3398 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3400 // Clear any pinned SSL certificate or IP addresses.
3401 nestedScrollWebView.clearPinnedSslCertificate();
3402 nestedScrollWebView.clearPinnedIpAddresses();
3404 // Reset the favorite icon if specified.
3406 // Initialize the favorite icon.
3407 nestedScrollWebView.initializeFavoriteIcon();
3409 // Get the current page position.
3410 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3412 // Get a handle for the tab layout.
3413 TabLayout tabLayout = findViewById(R.id.tablayout);
3415 // Get the corresponding tab.
3416 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3418 // Remove the warning below that the tab might be null.
3421 // Get the tab custom view.
3422 View tabCustomView = tab.getCustomView();
3424 // Remove the warning below that the tab custom view might be null.
3425 assert tabCustomView != null;
3427 // Get the tab views.
3428 ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3429 TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3431 // Set the default favorite icon as the favorite icon for this tab.
3432 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3434 // Set the loading title text.
3435 tabTitleTextView.setText(R.string.loading);
3438 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3439 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3441 // Get a full cursor from `domainsDatabaseHelper`.
3442 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3444 // Initialize `domainSettingsSet`.
3445 Set<String> domainSettingsSet = new HashSet<>();
3447 // Get the domain name column index.
3448 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3450 // Populate `domainSettingsSet`.
3451 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3452 // Move `domainsCursor` to the current row.
3453 domainNameCursor.moveToPosition(i);
3455 // Store the domain name in `domainSettingsSet`.
3456 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3459 // Close `domainNameCursor.
3460 domainNameCursor.close();
3462 // Initialize the domain name in database variable.
3463 String domainNameInDatabase = null;
3465 // Check the hostname against the domain settings set.
3466 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3467 // Record the domain name in the database.
3468 domainNameInDatabase = newHostName;
3470 // Set the domain settings applied tracker to true.
3471 nestedScrollWebView.setDomainSettingsApplied(true);
3472 } else { // The hostname is not contained in the domain settings set.
3473 // Set the domain settings applied tracker to false.
3474 nestedScrollWebView.setDomainSettingsApplied(false);
3477 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3478 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3479 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3480 // Set the domain settings applied tracker to true.
3481 nestedScrollWebView.setDomainSettingsApplied(true);
3483 // Store the applied domain names as it appears in the database.
3484 domainNameInDatabase = "*." + newHostName;
3487 // Strip out the lowest subdomain of of the host name.
3488 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3492 // Get a handle for the shared preferences.
3493 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3495 // Store the general preference information.
3496 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3497 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3498 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3499 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3500 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3502 // Get a handle for the cookie manager.
3503 CookieManager cookieManager = CookieManager.getInstance();
3505 // Get handles for the views.
3506 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3507 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3509 // Initialize the user agent array adapter and string array.
3510 ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3511 String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3513 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3514 // Get a cursor for the current host and move it to the first position.
3515 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3516 currentDomainSettingsCursor.moveToFirst();
3518 // Get the settings from the cursor.
3519 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3520 nestedScrollWebView.setDomainSettingsJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3521 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3522 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3523 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3524 // Form data can be removed once the minimum API >= 26.
3525 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3526 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST,
3527 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3528 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY,
3529 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3530 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3531 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3532 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3533 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3534 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY,
3535 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3536 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3537 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3538 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3539 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3540 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3541 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3542 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3543 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3544 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3545 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3546 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3547 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3548 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3549 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3550 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3551 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3553 // Create the pinned SSL date variables.
3554 Date pinnedSslStartDate;
3555 Date pinnedSslEndDate;
3557 // 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.
3558 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3559 pinnedSslStartDate = null;
3561 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3564 // 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.
3565 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3566 pinnedSslEndDate = null;
3568 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3571 // If there is a pinned SSL certificate, store it in the WebView.
3572 if (pinnedSslCertificate) {
3573 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3574 pinnedSslStartDate, pinnedSslEndDate);
3577 // If there is a pinned IP address, store it in the WebView.
3578 if (pinnedIpAddresses) {
3579 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3582 // Set night mode according to the night mode int.
3583 switch (nightModeInt) {
3584 case DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT:
3585 // Set night mode according to the current default.
3586 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3589 case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3590 // Enable night mode.
3591 nestedScrollWebView.setNightMode(true);
3594 case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3595 // Disable night mode.
3596 nestedScrollWebView.setNightMode(false);
3600 // Enable JavaScript if night mode is enabled.
3601 if (nestedScrollWebView.getNightMode()) {
3602 // Enable JavaScript.
3603 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3605 // Set JavaScript according to the domain settings.
3606 nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled());
3609 // Close the current host domain settings cursor.
3610 currentDomainSettingsCursor.close();
3612 // Apply the domain settings.
3613 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3615 // Set third-party cookies status if API >= 21.
3616 if (Build.VERSION.SDK_INT >= 21) {
3617 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3620 // Apply the form data setting if the API < 26.
3621 if (Build.VERSION.SDK_INT < 26) {
3622 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3625 // Apply the font size.
3626 if (fontSize == 0) { // Apply the default font size.
3627 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3628 } else { // Apply the specified font size.
3629 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3632 // 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.
3633 // <https://redmine.stoutner.com/issues/160>
3634 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3635 // Set the user agent.
3636 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3637 // Get the array position of the default user agent name.
3638 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3640 // Set the user agent according to the system default.
3641 switch (defaultUserAgentArrayPosition) {
3642 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3643 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3644 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3647 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3648 // Set the user agent to `""`, which uses the default value.
3649 nestedScrollWebView.getSettings().setUserAgentString("");
3652 case SETTINGS_CUSTOM_USER_AGENT:
3653 // Set the default custom user agent.
3654 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3658 // Get the user agent string from the user agent data array
3659 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3661 } else { // Set the user agent according to the stored name.
3662 // Get the array position of the user agent name.
3663 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3665 switch (userAgentArrayPosition) {
3666 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3667 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3670 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3671 // Set the user agent to `""`, which uses the default value.
3672 nestedScrollWebView.getSettings().setUserAgentString("");
3676 // Get the user agent string from the user agent data array.
3677 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3682 // Set swipe to refresh.
3683 switch (swipeToRefreshInt) {
3684 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3685 // Store the swipe to refresh status in the nested scroll WebView.
3686 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3688 // 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.
3689 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3692 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3693 // Store the swipe to refresh status in the nested scroll WebView.
3694 nestedScrollWebView.setSwipeToRefresh(true);
3696 // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3697 swipeRefreshLayout.setEnabled(true);
3700 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3701 // Store the swipe to refresh status in the nested scroll WebView.
3702 nestedScrollWebView.setSwipeToRefresh(false);
3704 // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3705 swipeRefreshLayout.setEnabled(false);
3708 // Set the loading of webpage images.
3709 switch (displayWebpageImagesInt) {
3710 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3711 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3714 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3715 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3718 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3719 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3723 // 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.
3725 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3727 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3729 } else { // The new URL does not have custom domain settings. Load the defaults.
3730 // Store the values from the shared preferences.
3731 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3732 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
3733 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3734 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3735 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3736 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, sharedPreferences.getBoolean("easylist", true));
3737 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3738 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3739 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3740 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3741 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3742 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3744 // Enable JavaScript if night mode is enabled.
3745 if (nestedScrollWebView.getNightMode()) {
3746 // Enable JavaScript.
3747 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3749 // Set JavaScript according to the domain settings.
3750 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3753 // Apply the default settings.
3754 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3755 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3757 // Apply the form data setting if the API < 26.
3758 if (Build.VERSION.SDK_INT < 26) {
3759 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3762 // Store the swipe to refresh status in the nested scroll WebView.
3763 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3765 // Apply swipe to refresh according to the default.
3766 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3768 // Reset the pinned variables.
3769 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
3771 // Set third-party cookies status if API >= 21.
3772 if (Build.VERSION.SDK_INT >= 21) {
3773 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
3776 // 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.
3777 // <https://redmine.stoutner.com/issues/160>
3778 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3779 // Get the array position of the user agent name.
3780 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3782 // Set the user agent.
3783 switch (userAgentArrayPosition) {
3784 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3785 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3786 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3789 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3790 // Set the user agent to `""`, which uses the default value.
3791 nestedScrollWebView.getSettings().setUserAgentString("");
3794 case SETTINGS_CUSTOM_USER_AGENT:
3795 // Set the default custom user agent.
3796 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3800 // Get the user agent string from the user agent data array
3801 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3805 // Set the loading of webpage images.
3806 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3808 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
3809 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
3812 // Close the domains database helper.
3813 domainsDatabaseHelper.close();
3815 // Update the privacy icons.
3816 updatePrivacyIcons(true);
3819 // Reload the website if returning from the Domains activity.
3820 if (reloadWebsite) {
3821 nestedScrollWebView.reload();
3824 // Return the user agent changed status.
3825 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
3828 private void applyProxyThroughOrbot(boolean reloadWebsite) {
3829 // Get a handle for the shared preferences.
3830 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3832 // Get the search and theme preferences.
3833 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3834 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3835 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3836 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3837 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3839 // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
3840 ActionBar actionBar = getSupportActionBar();
3842 // Remove the incorrect lint warning later that the action bar might be null.
3843 assert actionBar != null;
3845 // Set the homepage, search, and proxy options.
3846 if (proxyThroughOrbot) { // Set the Tor options.
3847 // Set the search URL.
3848 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
3849 searchURL = torSearchCustomUrlString;
3850 } else { // Use the string from the pre-built list.
3851 searchURL = torSearchString;
3854 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
3855 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
3857 // Set the `appBar` background to indicate proxying through Orbot is enabled.
3859 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
3861 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
3864 // Check to see if Orbot is ready.
3865 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
3866 // Set `waitingForOrbot`.
3867 waitingForOrbot = true;
3869 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
3870 currentWebView.getSettings().setUseWideViewPort(false);
3872 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
3873 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
3874 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
3875 // Reload the website.
3876 currentWebView.reload();
3878 } else { // Set the non-Tor options.
3879 // Set the search URL.
3880 if (searchString.equals("Custom URL")) { // Get the custom URL string.
3881 searchURL = searchCustomUrlString;
3882 } else { // Use the string from the pre-built list.
3883 searchURL = searchString;
3886 // Reset the proxy to default. The host is `""` and the port is `"0"`.
3887 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
3889 // Set the default `appBar` background.
3891 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
3893 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
3896 // Reset `waitingForOrbot.
3897 waitingForOrbot = false;
3899 // Reload the WebViews if requested.
3900 if (reloadWebsite) {
3901 // Reload the WebViews.
3902 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3903 // Get the WebView tab fragment.
3904 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3906 // Get the fragment view.
3907 View fragmentView = webViewTabFragment.getView();
3909 // Only reload the WebViews if they exist.
3910 if (fragmentView != null) {
3911 // Get the nested scroll WebView from the tab fragment.
3912 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3914 // Reload the WebView.
3915 nestedScrollWebView.reload();
3922 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
3923 // Only update the privacy icons if the options menu and the current WebView have already been populated.
3924 if ((optionsMenu != null) && (currentWebView != null)) {
3925 // Get a handle for the shared preferences.
3926 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3928 // Get the theme and screenshot preferences.
3929 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3931 // Get handles for the menu items.
3932 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
3933 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
3934 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
3935 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
3937 // Update the privacy icon.
3938 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
3939 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
3940 } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled.
3941 privacyMenuItem.setIcon(R.drawable.warning);
3942 } else { // All the dangerous features are disabled.
3943 privacyMenuItem.setIcon(R.drawable.privacy_mode);
3946 // Update the first-party cookies icon.
3947 if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled.
3948 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
3949 } else { // First-party cookies are disabled.
3951 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
3953 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
3957 // Update the DOM storage icon.
3958 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
3959 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
3960 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
3962 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
3964 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
3966 } else { // JavaScript is disabled, so DOM storage is ghosted.
3968 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
3970 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
3974 // Update the refresh icon.
3976 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
3978 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
3981 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
3982 if (runInvalidateOptionsMenu) {
3983 invalidateOptionsMenu();
3988 private void openUrlWithExternalApp(String url) {
3989 // Create a download intent. Not specifying the action type will display the maximum number of options.
3990 Intent downloadIntent = new Intent();
3992 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
3993 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
3995 // Flag the intent to open in a new task.
3996 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
3998 // Show the chooser.
3999 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4002 private void highlightUrlText() {
4003 // Get a handle for the URL edit text.
4004 EditText urlEditText = findViewById(R.id.url_edittext);
4006 // Only highlight the URL text if the box is not currently selected.
4007 if (!urlEditText.hasFocus()) {
4008 // Get the URL string.
4009 String urlString = urlEditText.getText().toString();
4011 // Highlight the URL according to the protocol.
4012 if (urlString.startsWith("file://")) { // This is a file URL.
4013 // De-emphasize only the protocol.
4014 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4015 } else if (urlString.startsWith("content://")) {
4016 // De-emphasize only the protocol.
4017 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4018 } else { // This is a web URL.
4019 // Get the index of the `/` immediately after the domain name.
4020 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4022 // Create a base URL string.
4025 // Get the base URL.
4026 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4027 // Get the base URL.
4028 baseUrl = urlString.substring(0, endOfDomainName);
4029 } else { // There are no characters after the base URL.
4030 // Set the base URL to be the entire URL string.
4031 baseUrl = urlString;
4034 // Get the index of the last `.` in the domain.
4035 int lastDotIndex = baseUrl.lastIndexOf(".");
4037 // Get the index of the penultimate `.` in the domain.
4038 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4040 // Markup the beginning of the URL.
4041 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4042 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4044 // De-emphasize subdomains.
4045 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4046 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4048 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4049 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4050 // De-emphasize the protocol and the additional subdomains.
4051 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4052 } else { // There is only one subdomain in the domain name.
4053 // De-emphasize only the protocol.
4054 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4058 // De-emphasize the text after the domain name.
4059 if (endOfDomainName > 0) {
4060 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4066 private void loadBookmarksFolder() {
4067 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4068 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4070 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4071 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4073 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4074 // Inflate the individual item layout. `false` does not attach it to the root.
4075 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4079 public void bindView(View view, Context context, Cursor cursor) {
4080 // Get handles for the views.
4081 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4082 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4084 // Get the favorite icon byte array from the cursor.
4085 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4087 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4088 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4090 // Display the bitmap in `bookmarkFavoriteIcon`.
4091 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4093 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4094 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4095 bookmarkNameTextView.setText(bookmarkNameString);
4097 // Make the font bold for folders.
4098 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4099 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4100 } else { // Reset the font to default for normal bookmarks.
4101 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4106 // Get a handle for the bookmarks list view.
4107 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4109 // Populate the list view with the adapter.
4110 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4112 // Get a handle for the bookmarks title text view.
4113 TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4115 // Set the bookmarks drawer title.
4116 if (currentBookmarksFolder.isEmpty()) {
4117 bookmarksTitleTextView.setText(R.string.bookmarks);
4119 bookmarksTitleTextView.setText(currentBookmarksFolder);
4123 private void openWithApp(String url) {
4124 // Create the open with intent with `ACTION_VIEW`.
4125 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4127 // Set the URI but not the MIME type. This should open all available apps.
4128 openWithAppIntent.setData(Uri.parse(url));
4130 // Flag the intent to open in a new task.
4131 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4133 // Show the chooser.
4134 startActivity(openWithAppIntent);
4137 private void openWithBrowser(String url) {
4138 // Create the open with intent with `ACTION_VIEW`.
4139 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4141 // Set the URI and the MIME type. `"text/html"` should load browser options.
4142 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4144 // Flag the intent to open in a new task.
4145 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4147 // Show the chooser.
4148 startActivity(openWithBrowserIntent);
4151 public void addTab(View view) {
4152 // Get a handle for the tab layout and the view pager.
4153 TabLayout tabLayout = findViewById(R.id.tablayout);
4154 ViewPager webViewPager = findViewById(R.id.webviewpager);
4156 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4157 int newTabNumber = tabLayout.getTabCount();
4160 tabLayout.addTab(tabLayout.newTab());
4163 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4165 // Remove the lint warning below that the current tab might be null.
4166 assert newTab != null;
4168 // Set a custom view on the new tab.
4169 newTab.setCustomView(R.layout.tab_custom_view);
4171 // Add the new WebView page.
4172 webViewPagerAdapter.addPage(newTabNumber, webViewPager);
4175 private void setCurrentWebView(int pageNumber) {
4176 // Get a handle for the shared preferences.
4177 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4179 // Get the theme preference.
4180 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4182 // Get handles for the URL views.
4183 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4184 EditText urlEditText = findViewById(R.id.url_edittext);
4185 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4187 //Stop the swipe to refresh indicator if it is running
4188 swipeRefreshLayout.setRefreshing(false);
4190 // Get the WebView tab fragment.
4191 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4193 // Get the fragment view.
4194 View fragmentView = webViewTabFragment.getView();
4196 // Set the current WebView if the fragment view is not null.
4197 if (fragmentView != null) {
4198 // Store the current WebView.
4199 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4201 // Update the status of swipe to refresh.
4202 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
4203 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.
4204 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.
4205 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4207 // Enable the swipe refresh layout.
4208 swipeRefreshLayout.setEnabled(true);
4210 } else { // Swipe to refresh is disabled.
4211 // Disable the swipe refresh layout.
4212 swipeRefreshLayout.setEnabled(false);
4215 // Get a handle for the cookie manager.
4216 CookieManager cookieManager = CookieManager.getInstance();
4218 // Set the first-party cookie status.
4219 cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
4221 // Update the privacy icons. `true` redraws the icons in the app bar.
4222 updatePrivacyIcons(true);
4224 // Clear the focus from the URL text box.
4225 urlEditText.clearFocus();
4227 // Get a handle for the input method manager.
4228 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4230 // Remove the lint warning below that the input method manager might be null.
4231 assert inputMethodManager != null;
4233 // Hide the soft keyboard.
4234 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4236 // Display the current URL in the URL text box.
4237 urlEditText.setText(currentWebView.getUrl());
4239 // Highlight the URL text.
4242 // Set the background to indicate the domain settings status.
4243 if (currentWebView.getDomainSettingsApplied()) {
4244 // 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.
4246 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4248 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4251 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4257 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar) {
4258 // Get handles for the activity views.
4259 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4260 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4261 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4262 ActionBar actionBar = getSupportActionBar();
4263 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
4264 EditText urlEditText = findViewById(R.id.url_edittext);
4265 TabLayout tabLayout = findViewById(R.id.tablayout);
4266 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4268 // Remove the incorrect lint warning below that the action bar might be null.
4269 assert actionBar != null;
4271 // Get a handle for the activity
4272 Activity activity = this;
4274 // Get a handle for the input method manager.
4275 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4277 // Instantiate the blocklist helper.
4278 BlockListHelper blockListHelper = new BlockListHelper();
4280 // Remove the lint warning below that the input method manager might be null.
4281 assert inputMethodManager != null;
4283 // Get a handle for the shared preferences.
4284 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4286 // Get the relevant preferences.
4287 boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4289 // Initialize the favorite icon.
4290 nestedScrollWebView.initializeFavoriteIcon();
4292 // Set the app bar scrolling.
4293 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4295 // Allow pinch to zoom.
4296 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4298 // Hide zoom controls.
4299 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4301 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4302 if (Build.VERSION.SDK_INT >= 21) {
4303 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4306 // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
4307 nestedScrollWebView.getSettings().setUseWideViewPort(true);
4309 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4310 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4312 // Explicitly disable geolocation.
4313 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4315 // Create a double-tap gesture detector to toggle full-screen mode.
4316 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4317 // Override `onDoubleTap()`. All other events are handled using the default settings.
4319 public boolean onDoubleTap(MotionEvent event) {
4320 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4321 // Toggle the full screen browsing mode tracker.
4322 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4324 // Toggle the full screen browsing mode.
4325 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4326 // Hide the app bar if specified.
4328 // Hide the tab linear layout.
4329 tabsLinearLayout.setVisibility(View.GONE);
4331 // Hide the action bar.
4335 // Hide the banner ad in the free flavor.
4336 if (BuildConfig.FLAVOR.contentEquals("free")) {
4337 AdHelper.hideAd(findViewById(R.id.adview));
4340 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4341 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4343 /* Hide the system bars.
4344 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4345 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4346 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4347 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4349 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4350 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4351 } else { // Switch to normal viewing mode.
4352 // Show the tab linear layout.
4353 tabsLinearLayout.setVisibility(View.VISIBLE);
4355 // Show the action bar.
4358 // Show the banner ad in the free flavor.
4359 if (BuildConfig.FLAVOR.contentEquals("free")) {
4361 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4364 // Remove the `SYSTEM_UI` flags from the root frame layout.
4365 rootFrameLayout.setSystemUiVisibility(0);
4367 // Add the translucent status flag.
4368 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4371 // Consume the double-tap.
4373 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4379 // Pass all touch events on the WebView through the double-tap gesture detector.
4380 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4381 // Call `performClick()` on the view, which is required for accessibility.
4382 view.performClick();
4384 // Send the event to the gesture detector.
4385 return doubleTapGestureDetector.onTouchEvent(event);
4388 // Register the WebView for a context menu. This is used to see link targets and download images.
4389 registerForContextMenu(nestedScrollWebView);
4391 // Allow the downloading of files.
4392 nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4393 // Check if the download should be processed by an external app.
4394 if (downloadWithExternalApp) { // Download with an external app.
4395 // Create a download intent. Not specifying the action type will display the maximum number of options.
4396 Intent downloadIntent = new Intent();
4398 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4399 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4401 // Flag the intent to open in a new task.
4402 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4404 // Show the chooser.
4405 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4406 } else { // Download with Android's download manager.
4407 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4408 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4409 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4411 // Store the variables for future use by `onRequestPermissionsResult()`.
4413 downloadContentDisposition = contentDisposition;
4414 downloadContentLength = contentLength;
4416 // Show a dialog if the user has previously denied the permission.
4417 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4418 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4419 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4421 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4422 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4423 } else { // Show the permission request directly.
4424 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4425 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4427 } else { // The storage permission has already been granted.
4428 // Get a handle for the download file alert dialog.
4429 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4431 // Show the download file alert dialog.
4432 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4437 // Update the find on page count.
4438 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4439 // Get a handle for `findOnPageCountTextView`.
4440 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4443 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4444 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4445 // Set `findOnPageCountTextView` to `0/0`.
4446 findOnPageCountTextView.setText(R.string.zero_of_zero);
4447 } else if (isDoneCounting) { // There are matches.
4448 // `activeMatchOrdinal` is zero-based.
4449 int activeMatch = activeMatchOrdinal + 1;
4451 // Build the match string.
4452 String matchString = activeMatch + "/" + numberOfMatches;
4454 // Set `findOnPageCountTextView`.
4455 findOnPageCountTextView.setText(matchString);
4460 if (Build.VERSION.SDK_INT >= 23) {
4461 nestedScrollWebView.setOnScrollChangeListener((View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) -> {
4462 // Update the status of swipe to refresh if it is enabled.
4463 if (nestedScrollWebView.getSwipeToRefresh()) {
4464 // Only enable swipe to refresh if the WebView is scrolled to the top.
4465 swipeRefreshLayout.setEnabled(scrollY == 0);
4470 // Set the web chrome client.
4471 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4472 // Update the progress bar when a page is loading.
4474 public void onProgressChanged(WebView view, int progress) {
4475 // Inject the night mode CSS if night mode is enabled.
4476 if (nestedScrollWebView.getNightMode()) {
4477 // `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
4478 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
4479 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
4480 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4481 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4482 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4483 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4484 // Initialize a handler to display `mainWebView`.
4485 Handler displayWebViewHandler = new Handler();
4487 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4488 Runnable displayWebViewRunnable = () -> {
4489 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
4490 if (progressBar.getVisibility() == View.GONE) {
4491 nestedScrollWebView.setVisibility(View.VISIBLE);
4495 // Display the WebView after 500 milliseconds.
4496 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4500 // Update the progress bar.
4501 progressBar.setProgress(progress);
4503 // Set the visibility of the progress bar.
4504 if (progress < 100) {
4505 // Show the progress bar.
4506 progressBar.setVisibility(View.VISIBLE);
4508 // Hide the progress bar.
4509 progressBar.setVisibility(View.GONE);
4511 // Display the nested scroll WebView if night mode is disabled.
4512 // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
4513 // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
4514 if (!nestedScrollWebView.getNightMode()) {
4515 nestedScrollWebView.setVisibility(View.VISIBLE);
4518 //Stop the swipe to refresh indicator if it is running
4519 swipeRefreshLayout.setRefreshing(false);
4523 // Set the favorite icon when it changes.
4525 public void onReceivedIcon(WebView view, Bitmap icon) {
4526 // Only update the favorite icon if the website has finished loading.
4527 if (progressBar.getVisibility() == View.GONE) {
4528 // Store the new favorite icon.
4529 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
4531 // Get the current page position.
4532 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4534 // Get the current tab.
4535 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4537 // Check to see if the tab has been populated.
4539 // Get the custom view from the tab.
4540 View tabView = tab.getCustomView();
4542 // Check to see if the custom tab view has been populated.
4543 if (tabView != null) {
4544 // Get the favorite icon image view from the tab.
4545 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
4547 // Display the favorite icon in the tab.
4548 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4554 // Save a copy of the title when it changes.
4556 public void onReceivedTitle(WebView view, String title) {
4557 // Get the current page position.
4558 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4560 // Get the current tab.
4561 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4563 // Only populate the title text view if the tab has been fully created.
4565 // Get the custom view from the tab.
4566 View tabView = tab.getCustomView();
4568 // Remove the incorrect warning below that the current tab view might be null.
4569 assert tabView != null;
4571 // Get the title text view from the tab.
4572 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
4574 // Set the title as the tab text.
4575 tabTitleTextView.setText(title);
4579 // Enter full screen video.
4581 public void onShowCustomView(View video, CustomViewCallback callback) {
4582 // Get a handle for the full screen video frame layout.
4583 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4585 // Set the full screen video flag.
4586 displayingFullScreenVideo = true;
4588 // Pause the ad if this is the free flavor.
4589 if (BuildConfig.FLAVOR.contentEquals("free")) {
4590 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4591 AdHelper.pauseAd(findViewById(R.id.adview));
4594 // Hide the keyboard.
4595 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4597 // Hide the main content relative layout.
4598 mainContentRelativeLayout.setVisibility(View.GONE);
4600 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4601 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4603 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4604 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4606 /* Hide the system bars.
4607 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4608 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4609 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4610 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4612 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4613 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4615 // Disable the sliding drawers.
4616 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4618 // Add the video view to the full screen video frame layout.
4619 fullScreenVideoFrameLayout.addView(video);
4621 // Show the full screen video frame layout.
4622 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4625 // Exit full screen video.
4627 public void onHideCustomView() {
4628 // Get a handle for the full screen video frame layout.
4629 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4631 // Unset the full screen video flag.
4632 displayingFullScreenVideo = false;
4634 // Remove all the views from the full screen video frame layout.
4635 fullScreenVideoFrameLayout.removeAllViews();
4637 // Hide the full screen video frame layout.
4638 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4640 // Enable the sliding drawers.
4641 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4643 // Show the main content relative layout.
4644 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4646 // Apply the appropriate full screen mode flags.
4647 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4648 // Hide the app bar if specified.
4650 // Hide the tab linear layout.
4651 tabsLinearLayout.setVisibility(View.GONE);
4653 // Hide the action bar.
4657 // Hide the banner ad in the free flavor.
4658 if (BuildConfig.FLAVOR.contentEquals("free")) {
4659 AdHelper.hideAd(findViewById(R.id.adview));
4662 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4663 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4665 /* Hide the system bars.
4666 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4667 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4668 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4669 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4671 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4672 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4673 } else { // Switch to normal viewing mode.
4674 // Remove the `SYSTEM_UI` flags from the root frame layout.
4675 rootFrameLayout.setSystemUiVisibility(0);
4677 // Add the translucent status flag.
4678 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4681 // Reload the ad for the free flavor if not in full screen mode.
4682 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4684 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4690 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4691 // Show the file chooser if the device is running API >= 21.
4692 if (Build.VERSION.SDK_INT >= 21) {
4693 // Store the file path callback.
4694 fileChooserCallback = filePathCallback;
4696 // Create an intent to open a chooser based ont the file chooser parameters.
4697 Intent fileChooserIntent = fileChooserParams.createIntent();
4699 // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4700 startActivityForResult(fileChooserIntent, 0);
4706 nestedScrollWebView.setWebViewClient(new WebViewClient() {
4707 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4708 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4710 public boolean shouldOverrideUrlLoading(WebView view, String url) {
4711 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
4712 // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed.
4713 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
4715 // Check if the user agent has changed.
4716 if (userAgentChanged) {
4717 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
4718 nestedScrollWebView.loadUrl(url, customHeaders);
4720 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4723 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4726 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
4727 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4728 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4730 // Parse the url and set it as the data for the intent.
4731 emailIntent.setData(Uri.parse(url));
4733 // Open the email program in a new task instead of as part of Privacy Browser.
4734 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4737 startActivity(emailIntent);
4739 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4741 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
4742 // Open the dialer and load the phone number, but wait for the user to place the call.
4743 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4745 // Add the phone number to the intent.
4746 dialIntent.setData(Uri.parse(url));
4748 // Open the dialer in a new task instead of as part of Privacy Browser.
4749 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4752 startActivity(dialIntent);
4754 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4756 } else { // Load a system chooser to select an app that can handle the URL.
4757 // Open an app that can handle the URL.
4758 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4760 // Add the URL to the intent.
4761 genericIntent.setData(Uri.parse(url));
4763 // List all apps that can handle the URL instead of just opening the first one.
4764 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4766 // Open the app in a new task instead of as part of Privacy Browser.
4767 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4769 // Start the app or display a snackbar if no app is available to handle the URL.
4771 startActivity(genericIntent);
4772 } catch (ActivityNotFoundException exception) {
4773 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
4776 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4781 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4783 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4784 // Get a handle for the navigation view.
4785 NavigationView navigationView = findViewById(R.id.navigationview);
4787 // Get a handle for the navigation menu.
4788 Menu navigationMenu = navigationView.getMenu();
4790 // Get a handle for the navigation requests menu item. The menu is 0 based.
4791 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
4793 // Create an empty web resource response to be used if the resource request is blocked.
4794 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4796 // Reset the whitelist results tracker.
4797 String[] whitelistResultStringArray = null;
4799 // Initialize the third party request tracker.
4800 boolean isThirdPartyRequest = false;
4802 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
4803 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
4805 // Store a copy of the current domain for use in later requests.
4806 String currentDomain = currentBaseDomain;
4808 // Nobody is happy when comparing null strings.
4809 if ((currentBaseDomain != null) && (url != null)) {
4810 // Convert the request URL to a URI.
4811 Uri requestUri = Uri.parse(url);
4813 // Get the request host name.
4814 String requestBaseDomain = requestUri.getHost();
4816 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
4817 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
4818 // Determine the current base domain.
4819 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4820 // Remove the first subdomain.
4821 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4824 // Determine the request base domain.
4825 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4826 // Remove the first subdomain.
4827 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4830 // Update the third party request tracker.
4831 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4835 // Get the current WebView page position.
4836 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4838 // Determine if the WebView is currently displayed.
4839 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
4841 // Block third-party requests if enabled.
4842 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
4843 // Add the result to the resource requests.
4844 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_THIRD_PARTY, url});
4846 // Increment the blocked requests counters.
4847 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4848 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
4850 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4851 if (webViewDisplayed) {
4852 // Updating the UI must be run from the UI thread.
4853 activity.runOnUiThread(() -> {
4854 // Update the menu item titles.
4855 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4857 // Update the options menu if it has been populated.
4858 if (optionsMenu != null) {
4859 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4860 optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
4861 getString(R.string.block_all_third_party_requests));
4866 // Return an empty web resource response.
4867 return emptyWebResourceResponse;
4870 // Check UltraPrivacy if it is enabled.
4871 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)) {
4872 // Check the URL against UltraPrivacy.
4873 String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
4875 // Process the UltraPrivacy results.
4876 if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
4877 // Add the result to the resource requests.
4878 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4879 ultraPrivacyResults[5]});
4881 // Increment the blocked requests counters.
4882 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4883 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY);
4885 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4886 if (webViewDisplayed) {
4887 // Updating the UI must be run from the UI thread.
4888 activity.runOnUiThread(() -> {
4889 // Update the menu item titles.
4890 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4892 // Update the options menu if it has been populated.
4893 if (optionsMenu != null) {
4894 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4895 optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
4900 // The resource request was blocked. Return an empty web resource response.
4901 return emptyWebResourceResponse;
4902 } else if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
4903 // Add a whitelist entry to the resource requests array.
4904 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4905 ultraPrivacyResults[5]});
4907 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
4912 // Check EasyList if it is enabled.
4913 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)) {
4914 // Check the URL against EasyList.
4915 String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
4917 // Process the EasyList results.
4918 if (easyListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
4919 // Add the result to the resource requests.
4920 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
4922 // Increment the blocked requests counters.
4923 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4924 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_LIST);
4926 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4927 if (webViewDisplayed) {
4928 // Updating the UI must be run from the UI thread.
4929 activity.runOnUiThread(() -> {
4930 // Update the menu item titles.
4931 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4933 // Update the options menu if it has been populated.
4934 if (optionsMenu != null) {
4935 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4936 optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
4941 // The resource request was blocked. Return an empty web resource response.
4942 return emptyWebResourceResponse;
4943 } else if (easyListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
4944 // Update the whitelist result string array tracker.
4945 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
4949 // Check EasyPrivacy if it is enabled.
4950 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)) {
4951 // Check the URL against EasyPrivacy.
4952 String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
4954 // Process the EasyPrivacy results.
4955 if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
4956 // Add the result to the resource requests.
4957 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
4958 easyPrivacyResults[5]});
4960 // Increment the blocked requests counters.
4961 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4962 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY);
4964 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4965 if (webViewDisplayed) {
4966 // Updating the UI must be run from the UI thread.
4967 activity.runOnUiThread(() -> {
4968 // Update the menu item titles.
4969 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4971 // Update the options menu if it has been populated.
4972 if (optionsMenu != null) {
4973 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4974 optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
4979 // The resource request was blocked. Return an empty web resource response.
4980 return emptyWebResourceResponse;
4981 } else if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
4982 // Update the whitelist result string array tracker.
4983 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
4987 // Check Fanboy’s Annoyance List if it is enabled.
4988 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
4989 // Check the URL against Fanboy's Annoyance List.
4990 String[] fanboysAnnoyanceListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
4992 // Process the Fanboy's Annoyance List results.
4993 if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
4994 // Add the result to the resource requests.
4995 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4996 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
4998 // Increment the blocked requests counters.
4999 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5000 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5002 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5003 if (webViewDisplayed) {
5004 // Updating the UI must be run from the UI thread.
5005 activity.runOnUiThread(() -> {
5006 // Update the menu item titles.
5007 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5009 // Update the options menu if it has been populated.
5010 if (optionsMenu != null) {
5011 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5012 optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5013 getString(R.string.fanboys_annoyance_list));
5018 // The resource request was blocked. Return an empty web resource response.
5019 return emptyWebResourceResponse;
5020 } else if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
5021 // Update the whitelist result string array tracker.
5022 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5023 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5025 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5026 // Check the URL against Fanboy's Annoyance List.
5027 String[] fanboysSocialListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5029 // Process the Fanboy's Social Blocking List results.
5030 if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5031 // Add the result to the resource requests.
5032 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5033 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5035 // Increment the blocked requests counters.
5036 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5037 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5039 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5040 if (webViewDisplayed) {
5041 // Updating the UI must be run from the UI thread.
5042 activity.runOnUiThread(() -> {
5043 // Update the menu item titles.
5044 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5046 // Update the options menu if it has been populated.
5047 if (optionsMenu != null) {
5048 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5049 optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5050 getString(R.string.fanboys_social_blocking_list));
5055 // The resource request was blocked. Return an empty web resource response.
5056 return emptyWebResourceResponse;
5057 } else if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5058 // Update the whitelist result string array tracker.
5059 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5060 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5064 // Add the request to the log because it hasn't been processed by any of the previous checks.
5065 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5066 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5067 } else { // The request didn't match any blocklist entry. Log it as a default request.
5068 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_DEFAULT, url});
5071 // The resource request has not been blocked. `return null` loads the requested resource.
5075 // Handle HTTP authentication requests.
5077 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5078 // Store the handler.
5079 nestedScrollWebView.setHttpAuthHandler(handler);
5081 // Instantiate an HTTP authentication dialog.
5082 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5084 // Show the HTTP authentication dialog.
5085 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5089 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5090 // Get the theme preference.
5091 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5093 // Reset the list of resource requests.
5094 nestedScrollWebView.clearResourceRequests();
5096 // Reset the requests counters.
5097 nestedScrollWebView.resetRequestsCounters();
5099 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5100 if (nestedScrollWebView.getNightMode()) {
5101 nestedScrollWebView.setVisibility(View.INVISIBLE);
5104 // Hide the keyboard.
5105 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5107 // Check to see if Privacy Browser is waiting on Orbot.
5108 if (!waitingForOrbot) { // Process the URL.
5109 // Get the current page position.
5110 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5112 // Update the URL text bar if the page is currently selected.
5113 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5114 // Display the formatted URL text.
5115 urlEditText.setText(url);
5117 // Apply text highlighting to `urlTextBox`.
5121 // Reset the list of host IP addresses.
5122 nestedScrollWebView.clearCurrentIpAddresses();
5124 // Get a URI for the current URL.
5125 Uri currentUri = Uri.parse(url);
5127 // Get the IP addresses for the host.
5128 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5130 // Apply any custom domain settings if the URL was loaded by navigating history.
5131 if (nestedScrollWebView.getNavigatingHistory()) {
5132 // Reset navigating history.
5133 nestedScrollWebView.setNavigatingHistory(false);
5135 // Apply the domain settings.
5136 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5138 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5139 if (userAgentChanged) {
5144 // Replace Refresh with Stop if the options menu has been created. (The WebView typically begins loading before the menu items are instantiated.)
5145 if (optionsMenu != null) {
5146 // Get a handle for the refresh menu item.
5147 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5150 refreshMenuItem.setTitle(R.string.stop);
5152 // Get the app bar and theme preferences.
5153 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5155 // If the icon is displayed in the AppBar, set it according to the theme.
5156 if (displayAdditionalAppBarIcons) {
5158 refreshMenuItem.setIcon(R.drawable.close_dark);
5160 refreshMenuItem.setIcon(R.drawable.close_light);
5168 public void onPageFinished(WebView view, String url) {
5169 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5170 if (!waitingForOrbot) {
5171 // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5172 nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5175 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5176 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
5177 CookieManager.getInstance().flush();
5180 // Update the Refresh menu item if the options menu has been created.
5181 if (optionsMenu != null) {
5182 // Get a handle for the refresh menu item.
5183 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5185 // Reset the Refresh title.
5186 refreshMenuItem.setTitle(R.string.refresh);
5188 // Get the app bar and theme preferences.
5189 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5190 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5192 // If the icon is displayed in the AppBar, reset it according to the theme.
5193 if (displayAdditionalAppBarIcons) {
5195 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5197 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5202 // Clear the cache and history if Incognito Mode is enabled.
5203 if (incognitoModeEnabled) {
5204 // Clear the cache. `true` includes disk files.
5205 nestedScrollWebView.clearCache(true);
5207 // Clear the back/forward history.
5208 nestedScrollWebView.clearHistory();
5210 // Manually delete cache folders.
5212 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5213 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5214 String privateDataDirectoryString = getApplicationInfo().dataDir;
5216 // Delete the main cache directory.
5217 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5219 // Delete the secondary `Service Worker` cache directory.
5220 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5221 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5222 } catch (IOException e) {
5223 // Do nothing if an error is thrown.
5227 // Update the URL text box and apply domain settings if not waiting on Orbot.
5228 if (!waitingForOrbot) {
5229 // Get the current page position.
5230 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5232 // Update the URL text bar if the page is currently selected.
5233 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5234 // Check to see if the URL is `about:blank`.
5235 if (url.equals("about:blank")) { // The WebView is blank.
5236 // Display the hint in the URL edit text.
5237 urlEditText.setText("");
5239 // Request focus for the URL text box.
5240 urlEditText.requestFocus();
5242 // Display the keyboard.
5243 inputMethodManager.showSoftInput(urlEditText, 0);
5245 // Hide the WebView, which causes the default background color to be displayed according to the theme. // TODO
5246 nestedScrollWebView.setVisibility(View.GONE);
5248 // Apply the domain settings. This clears any settings from the previous domain.
5249 applyDomainSettings(nestedScrollWebView, "", true, false);
5250 } else { // The WebView has loaded a webpage.
5251 // Only update the URL text box if the user is not typing in it.
5252 if (!urlEditText.hasFocus()) {
5253 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5254 urlEditText.setText(nestedScrollWebView.getUrl());
5256 // Apply text highlighting to the URL.
5262 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5263 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5264 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5265 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5270 // Handle SSL Certificate errors.
5272 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5273 // Get the current website SSL certificate.
5274 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5276 // Extract the individual pieces of information from the current website SSL certificate.
5277 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5278 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5279 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5280 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5281 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5282 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5283 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5284 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5286 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5287 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5288 // Get the pinned SSL certificate.
5289 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5291 // Extract the arrays from the array list.
5292 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5293 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5295 // Check if the current SSL certificate matches the pinned certificate.
5296 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5297 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5298 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5299 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5301 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5304 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5305 // Store the SSL error handler.
5306 nestedScrollWebView.setSslErrorHandler(handler);
5308 // Instantiate an SSL certificate error alert dialog.
5309 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
5311 // Show the SSL certificate error dialog.
5312 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5317 // Check to see if this is the first page.
5318 if (pageNumber == 0) {
5319 // Set this nested scroll WebView as the current WebView.
5320 currentWebView = nestedScrollWebView;
5322 // Apply the app settings from the shared preferences.
5325 // Load the website if not waiting for Orbot to connect.
5326 if (!waitingForOrbot) {
5327 // Get the intent that started the app.
5328 Intent launchingIntent = getIntent();
5330 // Get the information from the intent.
5331 String launchingIntentAction = launchingIntent.getAction();
5332 Uri launchingIntentUriData = launchingIntent.getData();
5334 // If the intent action is a web search, perform the search.
5335 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
5336 // Create an encoded URL string.
5337 String encodedUrlString;
5339 // Sanitize the search input and convert it to a search.
5341 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
5342 } catch (UnsupportedEncodingException exception) {
5343 encodedUrlString = "";
5346 // Load the completed search URL.
5347 loadUrl(searchURL + encodedUrlString);
5348 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
5349 // Load the URL from the intent.
5350 loadUrl(launchingIntentUriData.toString());
5351 } else { // The is no URL in the intent.
5352 // Select the homepage based on the proxy through Orbot status.
5353 if (proxyThroughOrbot) {
5354 // Load the Tor homepage.
5355 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
5357 // Load the normal homepage.
5358 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));