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.WebBackForwardList;
75 import android.webkit.WebChromeClient;
76 import android.webkit.WebResourceResponse;
77 import android.webkit.WebSettings;
78 import android.webkit.WebStorage;
79 import android.webkit.WebView;
80 import android.webkit.WebViewClient;
81 import android.webkit.WebViewDatabase;
82 import android.widget.ArrayAdapter;
83 import android.widget.CursorAdapter;
84 import android.widget.EditText;
85 import android.widget.FrameLayout;
86 import android.widget.ImageView;
87 import android.widget.LinearLayout;
88 import android.widget.ListView;
89 import android.widget.ProgressBar;
90 import android.widget.RadioButton;
91 import android.widget.RelativeLayout;
92 import android.widget.TextView;
94 import androidx.annotation.NonNull;
95 import androidx.appcompat.app.ActionBar;
96 import androidx.appcompat.app.ActionBarDrawerToggle;
97 import androidx.appcompat.app.AppCompatActivity;
98 import androidx.appcompat.widget.Toolbar;
99 import androidx.core.app.ActivityCompat;
100 import androidx.core.content.ContextCompat;
101 import androidx.core.view.GravityCompat;
102 import androidx.drawerlayout.widget.DrawerLayout;
103 import androidx.fragment.app.DialogFragment;
104 import androidx.fragment.app.FragmentManager;
105 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
106 import androidx.viewpager.widget.ViewPager;
108 import com.google.android.material.floatingactionbutton.FloatingActionButton;
109 import com.google.android.material.navigation.NavigationView;
110 import com.google.android.material.snackbar.Snackbar;
111 import com.google.android.material.tabs.TabLayout;
113 import com.stoutner.privacybrowser.BuildConfig;
114 import com.stoutner.privacybrowser.R;
115 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
116 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
117 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
118 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
121 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
124 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
125 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
126 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
127 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
128 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
129 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
130 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
131 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
132 import com.stoutner.privacybrowser.helpers.AdHelper;
133 import com.stoutner.privacybrowser.helpers.BlockListHelper;
134 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
135 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
136 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
138 import com.stoutner.privacybrowser.views.NestedScrollWebView;
140 import java.io.ByteArrayInputStream;
141 import java.io.ByteArrayOutputStream;
143 import java.io.IOException;
144 import java.io.UnsupportedEncodingException;
145 import java.net.MalformedURLException;
147 import java.net.URLDecoder;
148 import java.net.URLEncoder;
149 import java.util.ArrayList;
150 import java.util.Date;
151 import java.util.HashMap;
152 import java.util.HashSet;
153 import java.util.List;
154 import java.util.Map;
155 import java.util.Set;
157 // TODO. The swipe refresh indicator needs to be enabled/disabled when switching tabs.
159 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
160 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
161 DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
162 EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener,
163 PinnedMismatchDialog.PinnedMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
165 // `darkTheme` is public static so it can be accessed from everywhere.
166 public static boolean darkTheme;
168 // `allowScreenshots` is public static so it can be accessed from everywhere. It is also used in `onCreate()`.
169 public static boolean allowScreenshots;
172 // `formattedUrlString` is public static so it can be accessed from `AddDomainDialog`, `BookmarksActivity`, `DomainSettingsFragment`, and `PinnedMismatchDialog`.
173 // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, `loadUrlFromTextBox()`, and `applyProxyThroughOrbot()`.
174 public static String formattedUrlString;
176 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
177 public static String orbotStatus;
179 // The WebView pager adapter is accessed from `PinnedMismatchDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`.
180 public static WebViewPagerAdapter webViewPagerAdapter;
182 // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()`
183 public static boolean reloadOnRestart;
185 // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`. It is also used in `onRestart()`.
186 public static boolean loadUrlOnRestart;
188 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
189 public static boolean restartFromBookmarksActivity;
191 // The blocklist versions are public static so they can be accessed from `AboutTabFragment`. They are also used in `onCreate()`.
192 public static String easyListVersion;
193 public static String easyPrivacyVersion;
194 public static String fanboysAnnoyanceVersion;
195 public static String fanboysSocialVersion;
196 public static String ultraPrivacyVersion;
198 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
199 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
200 public static String currentBookmarksFolder;
202 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
203 public final static int UNRECOGNIZED_USER_AGENT = -1;
204 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
205 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
206 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
207 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
208 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
212 // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
213 private boolean navigatingHistory; // TODO.
215 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
216 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
217 private NestedScrollWebView currentWebView;
219 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
220 private final Map<String, String> customHeaders = new HashMap<>();
222 // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`.
223 private boolean firstPartyCookiesEnabled;
225 // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. It can be removed once the minimum API >= 26.
226 private boolean saveFormDataEnabled;
228 // TODO Move to NestedScrollWebView.
229 // `nightMode` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
230 private boolean nightMode;
232 // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`.
233 private String homepage; // TODO ?
235 // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`.
236 private String searchURL; // TODO ?
238 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()` and `updatePrivacyIcons()`.
239 private Menu optionsMenu;
241 // The refresh menu item is set in `onCreateOptionsMenu()` and accessed from `initializeWebView()`.
242 // It must be this way because `initializeWebView()` runs before the menu is created but doesn't actually modify the menu until later.
243 private MenuItem refreshMenuItem; // TODO. Create it from `optionsMenu`.
245 // TODO. This could probably be removed.
246 // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
247 private BlockListHelper blockListHelper;
249 // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
250 private ArrayList<List<String[]>> easyList;
251 private ArrayList<List<String[]>> easyPrivacy;
252 private ArrayList<List<String[]>> fanboysAnnoyanceList;
253 private ArrayList<List<String[]>> fanboysSocialList;
254 private ArrayList<List<String[]>> ultraPrivacy;
256 // The blocklist menu items are used in `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, and `initializeWebView()`. // TODO.
257 private MenuItem blocklistsMenuItem;
258 private MenuItem easyListMenuItem;
259 private MenuItem easyPrivacyMenuItem;
260 private MenuItem fanboysAnnoyanceListMenuItem;
261 private MenuItem fanboysSocialBlockingListMenuItem;
262 private MenuItem ultraPrivacyMenuItem;
263 private MenuItem blockAllThirdPartyRequestsMenuItem;
265 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
266 private String webViewDefaultUserAgent;
268 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
269 private boolean proxyThroughOrbot;
271 // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
272 private boolean incognitoModeEnabled; // TODO.
274 // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
275 private boolean fullScreenBrowsingModeEnabled; // TODO.
277 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
278 private boolean inFullScreenBrowsingMode;
280 // Hide app bar is used in `onCreate()` and `applyAppSettings()`.
281 private boolean hideAppBar; // TODO.
283 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
284 private boolean reapplyDomainSettingsOnRestart;
286 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
287 private boolean reapplyAppSettingsOnRestart;
289 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
290 private boolean displayingFullScreenVideo;
292 // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`.
293 private boolean downloadWithExternalApp; // TODO.
295 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
296 private BroadcastReceiver orbotStatusBroadcastReceiver;
298 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
299 private boolean waitingForOrbot;
301 // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`.
302 private Boolean domainSettingsJavaScriptEnabled; // TODO.
304 // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`.
305 private String waitingForOrbotHtmlString; // TODO.
307 // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`.
308 private String privateDataDirectoryString; // TODO.
310 // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
311 private EditText findOnPageEditText; // TODO.
313 // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`.
314 private boolean displayAdditionalAppBarIcons; // TODO.
316 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
317 private ActionBarDrawerToggle actionBarDrawerToggle; // TODO.
319 // The color spans are used in `onCreate()` and `highlightUrlText()`.
320 private ForegroundColorSpan redColorSpan;
321 private ForegroundColorSpan initialGrayColorSpan;
322 private ForegroundColorSpan finalGrayColorSpan;
324 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
325 private int drawerHeaderPaddingLeftAndRight;
326 private int drawerHeaderPaddingTop;
327 private int drawerHeaderPaddingBottom;
329 // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
330 private SslErrorHandler sslErrorHandler; // TODO.
332 // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`.
333 private static HttpAuthHandler httpAuthHandler; // TODO.
335 // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
336 private InputMethodManager inputMethodManager; // TODO.
338 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
339 // and `loadBookmarksFolder()`.
340 private BookmarksDatabaseHelper bookmarksDatabaseHelper; // TODO.
342 // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
343 private ListView bookmarksListView; // TODO.
345 // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`.
346 private TextView bookmarksTitleTextView; // TODO.
348 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
349 private Cursor bookmarksCursor;
351 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
352 private CursorAdapter bookmarksCursorAdapter;
354 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
355 private String oldFolderNameString;
357 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
358 private ValueCallback<Uri[]> fileChooserCallback;
360 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
361 private String downloadUrl;
362 private String downloadContentDisposition;
363 private long downloadContentLength;
365 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
366 private String downloadImageUrl;
368 // The user agent variables are used in `onCreate()` and `applyDomainSettings()`.
369 private ArrayAdapter<CharSequence> userAgentNamesArray;
370 private String[] userAgentDataArray;
372 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
373 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
374 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
377 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
378 @SuppressLint("ClickableViewAccessibility")
379 protected void onCreate(Bundle savedInstanceState) {
380 // Get a handle for the shared preferences.
381 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
383 // Get the theme and screenshot preferences.
384 darkTheme = sharedPreferences.getBoolean("dark_theme", false);
385 allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
387 // Disable screenshots if not allowed.
388 if (!allowScreenshots) {
389 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
392 // Set the activity theme.
394 setTheme(R.style.PrivacyBrowserDark);
396 setTheme(R.style.PrivacyBrowserLight);
399 // Run the default commands.
400 super.onCreate(savedInstanceState);
402 // Set the content view.
403 setContentView(R.layout.main_framelayout);
405 // Get handles for the input method manager and toolbar.
406 inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
407 Toolbar toolbar = findViewById(R.id.toolbar);
409 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
410 setSupportActionBar(toolbar);
411 ActionBar actionBar = getSupportActionBar();
413 // This is needed to get rid of the Android Studio warning that the action bar might be null.
414 assert actionBar != null;
416 // Add the custom layout, which shows the URL text bar.
417 actionBar.setCustomView(R.layout.url_app_bar);
418 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
420 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
421 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
422 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
423 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
425 // Get handles for the URL views.
426 EditText urlEditText = findViewById(R.id.url_edittext);
428 // Remove the formatting from `urlTextBar` when the user is editing the text.
429 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
430 if (hasFocus) { // The user is editing the URL text box.
431 // Remove the highlighting.
432 urlEditText.getText().removeSpan(redColorSpan);
433 urlEditText.getText().removeSpan(initialGrayColorSpan);
434 urlEditText.getText().removeSpan(finalGrayColorSpan);
435 } else { // The user has stopped editing the URL text box.
436 // Move to the beginning of the string.
437 urlEditText.setSelection(0);
439 // Reapply the highlighting.
444 // Set the go button on the keyboard to load the URL in `urlTextBox`.
445 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
446 // If the event is a key-down event on the `enter` button, load the URL.
447 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
448 // Load the URL into the mainWebView and consume the event.
449 loadUrlFromTextBox();
451 // If the enter key was pressed, consume the event.
454 // If any other key was pressed, do not consume the event.
459 // Set `waitingForOrbotHTMLString`.
460 waitingForOrbotHtmlString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
462 // Initialize the Orbot status and the waiting for Orbot trackers.
463 orbotStatus = "unknown";
464 waitingForOrbot = false;
466 // Create an Orbot status `BroadcastReceiver`.
467 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
469 public void onReceive(Context context, Intent intent) {
470 // Store the content of the status message in `orbotStatus`.
471 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
473 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
474 if (orbotStatus.equals("ON") && waitingForOrbot) {
475 // Reset `waitingForOrbot`.
476 waitingForOrbot = false;
478 // Load `formattedUrlString
479 loadUrl(formattedUrlString);
484 // Register `orbotStatusBroadcastReceiver` on `this` context.
485 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
487 // Instantiate the block list helper.
488 blockListHelper = new BlockListHelper();
490 // Parse the block lists.
491 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
492 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
493 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
494 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
495 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
497 // Store the list versions.
498 easyListVersion = easyList.get(0).get(0)[0];
499 easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
500 fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
501 fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
502 ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
504 // Get handles for views that need to be modified.
505 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
506 NavigationView navigationView = findViewById(R.id.navigationview);
507 TabLayout tabLayout = findViewById(R.id.tablayout);
508 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
509 ViewPager webViewPager = findViewById(R.id.webviewpager);
510 bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
511 bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
512 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
513 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
514 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
515 findOnPageEditText = findViewById(R.id.find_on_page_edittext);
517 // Listen for touches on the navigation menu.
518 navigationView.setNavigationItemSelectedListener(this);
520 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
521 Menu navigationMenu = navigationView.getMenu();
522 MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
523 MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
524 MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
525 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
526 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
528 // Initialize the web view pager adapter.
529 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
531 // Set the pager adapter on the web view pager.
532 webViewPager.setAdapter(webViewPagerAdapter);
534 // Store up to 100 tabs in memory.
535 webViewPager.setOffscreenPageLimit(100);
537 // Update the web view pager every time a tab is modified.
538 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
540 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
545 public void onPageSelected(int position) {
546 // Set the current WebView.
547 setCurrentWebView(position);
549 // 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.
550 if (tabLayout.getSelectedTabPosition() != position) {
551 // Get a handle for the corresponding tab.
552 TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
554 // Assert that the corresponding tab is not null.
555 assert correspondingTab != null;
557 // Select the corresponding tab.
558 correspondingTab.select();
563 public void onPageScrollStateChanged(int state) {
568 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
569 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
571 public void onTabSelected(TabLayout.Tab tab) {
572 // Select the same page in the view pager.
573 webViewPager.setCurrentItem(tab.getPosition());
577 public void onTabUnselected(TabLayout.Tab tab) {
582 public void onTabReselected(TabLayout.Tab tab) {
583 // Instantiate the View SSL Certificate dialog.
584 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteOrDefaultIcon());
586 // Display the View SSL Certificate dialog.
587 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
591 // Add the first tab.
594 // 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.
595 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
597 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
598 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
599 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
600 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
602 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
603 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
604 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
605 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
608 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
609 launchBookmarksActivityFab.setOnClickListener(v -> {
610 // Store the current WebView url and title in the bookmarks activity. // TODO.
611 BookmarksActivity.currentWebViewUrl = currentWebView.getUrl();
612 BookmarksActivity.currentWebViewTitle = currentWebView.getTitle();
614 // Get a copy of the favorite icon bitmap.
615 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
617 // Create a favorite icon byte array output stream.
618 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
620 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
621 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
623 // Convert the favorite icon byte array stream to a byte array.
624 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
626 // Create an intent to launch the bookmarks activity.
627 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
629 // Add the extra information to the intent.
630 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
631 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
634 startActivity(bookmarksIntent);
637 // Set the create new bookmark folder FAB to display an alert dialog.
638 createBookmarkFolderFab.setOnClickListener(v -> {
639 // Create a create bookmark folder dialog.
640 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
642 // Show the create bookmark folder dialog.
643 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
646 // Set the create new bookmark FAB to display an alert dialog.
647 createBookmarkFab.setOnClickListener(view -> {
648 // Instantiate the create bookmark dialog.
649 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
651 // Display the create bookmark dialog.
652 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
655 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
656 findOnPageEditText.addTextChangedListener(new TextWatcher() {
658 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
663 public void onTextChanged(CharSequence s, int start, int before, int count) {
668 public void afterTextChanged(Editable s) {
669 // Search for the text in `mainWebView`.
670 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
674 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
675 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
676 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
677 // Hide the soft keyboard.
678 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
680 // Consume the event.
682 } else { // A different key was pressed.
683 // Do not consume the event.
688 // Implement swipe to refresh.
689 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
691 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
692 swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
694 // Set the swipe to refresh color according to the theme.
696 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
697 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
699 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
702 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
703 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
704 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
706 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
707 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
709 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
710 currentBookmarksFolder = "";
712 // Load the home folder, which is `""` in the database.
713 loadBookmarksFolder();
715 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
716 // Convert the id from long to int to match the format of the bookmarks database.
717 int databaseID = (int) id;
719 // Get the bookmark cursor for this ID and move it to the first row.
720 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
721 bookmarkCursor.moveToFirst();
723 // Act upon the bookmark according to the type.
724 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
725 // Store the new folder name in `currentBookmarksFolder`.
726 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
728 // Load the new folder.
729 loadBookmarksFolder();
730 } else { // The selected bookmark is not a folder.
731 // Load the bookmark URL.
732 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
734 // Close the bookmarks drawer.
735 drawerLayout.closeDrawer(GravityCompat.END);
738 // Close the `Cursor`.
739 bookmarkCursor.close();
742 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
743 // Convert the database ID from `long` to `int`.
744 int databaseId = (int) id;
746 // Find out if the selected bookmark is a folder.
747 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
750 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
751 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
753 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
754 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
755 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
757 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
758 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
759 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
762 // Consume the event.
766 // Get the status bar pixel size.
767 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
768 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
770 // Get the resource density.
771 float screenDensity = getResources().getDisplayMetrics().density;
773 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
774 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
775 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
776 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
778 // The drawer listener is used to update the navigation menu.`
779 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
781 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
785 public void onDrawerOpened(@NonNull View drawerView) {
789 public void onDrawerClosed(@NonNull View drawerView) {
793 public void onDrawerStateChanged(int newState) {
794 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
795 // Get handles for the drawer headers.
796 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
797 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
799 // 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.
800 if (navigationHeaderTextView != null) {
801 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
804 // 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.
805 if (bookmarksHeaderTextView != null) {
806 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
809 // Update the navigation menu items.
810 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
811 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
812 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
813 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
814 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
816 // Hide the keyboard (if displayed).
817 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
819 // 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.
820 urlEditText.clearFocus();
821 currentWebView.clearFocus();
826 // Create the hamburger icon at the start of the AppBar.
827 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
829 // 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).
830 customHeaders.put("X-Requested-With", "");
832 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
833 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
835 // Store the application's private data directory.
836 privateDataDirectoryString = getApplicationInfo().dataDir;
837 // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
839 // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
840 inFullScreenBrowsingMode = false;
842 // Initialize the privacy settings variables.
843 firstPartyCookiesEnabled = false;
844 saveFormDataEnabled = false; // Form data can be removed once the minimum API >= 26.
847 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
848 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
850 // Get a handle for the WebView.
851 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
853 // Store the default user agent.
854 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
856 // Destroy the bare WebView.
857 bareWebView.destroy();
859 // Initialize the user agent array adapter and string array.
860 userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
861 userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
863 // Get the intent that started the app.
864 Intent launchingIntent = getIntent();
866 // Get the information from the intent.
867 String launchingIntentAction = launchingIntent.getAction();
868 Uri launchingIntentUriData = launchingIntent.getData();
870 // If the intent action is a web search, perform the search.
871 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
872 // Create an encoded URL string.
873 String encodedUrlString;
875 // Sanitize the search input and convert it to a search.
877 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
878 } catch (UnsupportedEncodingException exception) {
879 encodedUrlString = "";
882 // Add the base search URL.
883 formattedUrlString = searchURL + encodedUrlString;
884 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
885 // Set the formatted URL string.
886 formattedUrlString = launchingIntentUriData.toString();
891 protected void onNewIntent(Intent intent) {
892 // Get the information from the intent.
893 String intentAction = intent.getAction();
894 Uri intentUriData = intent.getData();
896 // 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.
897 if (intentUriData != null) {
898 // Sets the new intent as the activity intent, which replaces the one that originally started the app.
904 // If the intent action is a web search, perform the search.
905 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
906 // Create an encoded URL string.
907 String encodedUrlString;
909 // Sanitize the search input and convert it to a search.
911 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
912 } catch (UnsupportedEncodingException exception) {
913 encodedUrlString = "";
916 // Add the base search URL.
917 formattedUrlString = searchURL + encodedUrlString;
918 } else { // The intent should contain a URL.
919 // Set the formatted URL string.
920 formattedUrlString = intentUriData.toString();
924 loadUrl(formattedUrlString);
926 // Get a handle for the drawer layout.
927 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
929 // Close the navigation drawer if it is open.
930 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
931 drawerLayout.closeDrawer(GravityCompat.START);
934 // Close the bookmarks drawer if it is open.
935 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
936 drawerLayout.closeDrawer(GravityCompat.END);
939 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
940 currentWebView.requestFocus();
945 public void onRestart() {
946 // Run the default commands.
949 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
950 if (proxyThroughOrbot) {
951 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
952 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
954 // Send the intent to the Orbot package.
955 orbotIntent.setPackage("org.torproject.android");
958 sendBroadcast(orbotIntent);
961 // Apply the app settings if returning from the Settings activity.
962 if (reapplyAppSettingsOnRestart) {
963 // Apply the app settings.
966 // Reload the webpage if displaying of images has been disabled in the Settings activity.
967 if (reloadOnRestart) {
968 // Reload the WebViews.
969 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
970 // Get the WebView tab fragment.
971 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
973 // Get the fragment view.
974 View fragmentView = webViewTabFragment.getView();
976 // Only reload the WebViews if they exist.
977 if (fragmentView != null) {
978 // Get the nested scroll WebView from the tab fragment.
979 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
981 // Reload the WebView. This doesn't seem to work if for WebViews that aren't visible.
982 nestedScrollWebView.reload();
986 // Reset `reloadOnRestartBoolean`.
987 reloadOnRestart = false;
990 // Reset the return from settings flag.
991 reapplyAppSettingsOnRestart = false;
994 // TODO apply to all the tabs.
995 // Apply the domain settings if returning from the Domains activity.
996 if (reapplyDomainSettingsOnRestart) {
997 // Reapply the domain settings.
998 applyDomainSettings(currentWebView, formattedUrlString, false, true);
1000 // Reset `reapplyDomainSettingsOnRestart`.
1001 reapplyDomainSettingsOnRestart = false;
1004 // Load the URL on restart to apply changes to night mode.
1005 if (loadUrlOnRestart) {
1006 // Load the current `formattedUrlString`.
1007 loadUrl(formattedUrlString);
1009 // Reset `loadUrlOnRestart.
1010 loadUrlOnRestart = false;
1013 // Update the bookmarks drawer if returning from the Bookmarks activity.
1014 if (restartFromBookmarksActivity) {
1015 // Get a handle for the drawer layout.
1016 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1018 // Close the bookmarks drawer.
1019 drawerLayout.closeDrawer(GravityCompat.END);
1021 // Reload the bookmarks drawer.
1022 loadBookmarksFolder();
1024 // Reset `restartFromBookmarksActivity`.
1025 restartFromBookmarksActivity = false;
1028 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
1029 updatePrivacyIcons(true);
1032 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
1034 public void onResume() {
1035 // Run the default commands.
1038 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1039 // Get the WebView tab fragment.
1040 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1042 // Get the fragment view.
1043 View fragmentView = webViewTabFragment.getView();
1045 // Only resume the WebViews if they exist (they won't when the app is first created).
1046 if (fragmentView != null) {
1047 // Get the nested scroll WebView from the tab fragment.
1048 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1050 // Resume the nested scroll WebView JavaScript timers.
1051 nestedScrollWebView.resumeTimers();
1053 // Resume the nested scroll WebView.
1054 nestedScrollWebView.onResume();
1058 // Display a message to the user if waiting for Orbot.
1059 if (waitingForOrbot && !orbotStatus.equals("ON")) {
1060 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
1061 currentWebView.getSettings().setUseWideViewPort(false);
1063 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
1064 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
1067 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
1068 // Get a handle for the root frame layouts.
1069 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1071 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1072 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1074 /* Hide the system bars.
1075 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1076 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1077 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1078 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1080 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1081 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1082 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
1084 AdHelper.resumeAd(findViewById(R.id.adview));
1089 public void onPause() {
1090 // Run the default commands.
1093 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1094 // Get the WebView tab fragment.
1095 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1097 // Get the fragment view.
1098 View fragmentView = webViewTabFragment.getView();
1100 // Only pause the WebViews if they exist (they won't when the app is first created).
1101 if (fragmentView != null) {
1102 // Get the nested scroll WebView from the tab fragment.
1103 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1105 // Pause the nested scroll WebView.
1106 nestedScrollWebView.onPause();
1108 // Pause the nested scroll WebView JavaScript timers.
1109 nestedScrollWebView.pauseTimers();
1113 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1114 if (BuildConfig.FLAVOR.contentEquals("free")) {
1116 AdHelper.pauseAd(findViewById(R.id.adview));
1121 public void onDestroy() {
1122 // Unregister the Orbot status broadcast receiver.
1123 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1125 // Close the bookmarks cursor and database.
1126 bookmarksCursor.close();
1127 bookmarksDatabaseHelper.close();
1129 // Run the default commands.
1134 public boolean onCreateOptionsMenu(Menu menu) {
1135 // Inflate the menu. This adds items to the action bar if it is present.
1136 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1138 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
1141 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1142 updatePrivacyIcons(false);
1144 // Get handles for the menu items.
1145 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1146 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1147 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1148 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1149 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1150 refreshMenuItem = menu.findItem(R.id.refresh);
1151 blocklistsMenuItem = menu.findItem(R.id.blocklists);
1152 easyListMenuItem = menu.findItem(R.id.easylist);
1153 easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1154 fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1155 fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1156 ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1157 blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1158 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1160 // Only display third-party cookies if API >= 21
1161 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1163 // Only display the form data menu items if the API < 26.
1164 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1165 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1167 // Only show Ad Consent if this is the free flavor.
1168 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1170 // Get the shared preference values.
1171 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1173 // Get the status of the additional AppBar icons.
1174 displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1176 // 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.
1177 if (displayAdditionalAppBarIcons) {
1178 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1179 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1180 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1181 } else { //Do not display the additional icons.
1182 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1183 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1184 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1187 // Replace Refresh with Stop if a URL is already loading.
1188 if (currentWebView != null && currentWebView.getProgress() != 100) {
1190 refreshMenuItem.setTitle(R.string.stop);
1192 // If the icon is displayed in the AppBar, set it according to the theme.
1193 if (displayAdditionalAppBarIcons) {
1195 refreshMenuItem.setIcon(R.drawable.close_dark);
1197 refreshMenuItem.setIcon(R.drawable.close_light);
1206 public boolean onPrepareOptionsMenu(Menu menu) {
1207 // Get a handle for the swipe refresh layout.
1208 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1210 // Get handles for the menu items.
1211 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1212 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1213 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1214 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1215 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1216 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1217 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1218 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1219 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1220 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1221 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1222 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1223 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1224 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1226 // Get a handle for the cookie manager.
1227 CookieManager cookieManager = CookieManager.getInstance();
1229 // Initialize the current user agent string and the font size.
1230 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1233 // 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.
1234 if (currentWebView != null) {
1235 // Set the add or edit domain text.
1236 if (currentWebView.getDomainSettingsApplied()) {
1237 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1239 addOrEditDomain.setTitle(R.string.add_domain_settings);
1242 // Get the current user agent from the WebView.
1243 currentUserAgent = currentWebView.getSettings().getUserAgentString();
1245 // Get the current font size from the
1246 fontSize = currentWebView.getSettings().getTextZoom();
1248 // Set the status of the menu item checkboxes.
1249 toggleDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1250 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1251 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1252 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1253 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1254 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1255 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1256 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1258 // Initialize the display names for the blocklists with the number of blocked requests.
1259 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1260 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1261 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1262 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1263 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1264 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1265 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1268 // Set the status of the menu item checkboxes.
1269 toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
1270 toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); // Form data can be removed once the minimum API >= 26.
1271 swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
1272 nightModeMenuItem.setChecked(nightMode);
1273 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1275 // Only modify third-party cookies if the API >= 21.
1276 if (Build.VERSION.SDK_INT >= 21) {
1277 // Set the status of the third-party cookies checkbox.
1278 toggleThirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1280 // Enable third-party cookies if first-party cookies are enabled.
1281 toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
1284 // Enable DOM Storage if JavaScript is enabled.
1285 toggleDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1287 // Enable Clear Cookies if there are any.
1288 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1290 // Get a count of the number of files in the Local Storage directory.
1291 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1292 int localStorageDirectoryNumberOfFiles = 0;
1293 if (localStorageDirectory.exists()) {
1294 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1297 // Get a count of the number of files in the IndexedDB directory.
1298 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1299 int indexedDBDirectoryNumberOfFiles = 0;
1300 if (indexedDBDirectory.exists()) {
1301 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1304 // Enable Clear DOM Storage if there is any.
1305 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1307 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1308 if (Build.VERSION.SDK_INT < 26) {
1309 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
1310 clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
1312 // Disable clear form data because it is not supported on current version of Android.
1313 clearFormDataMenuItem.setEnabled(false);
1316 // Enable Clear Data if any of the submenu items are enabled.
1317 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1319 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1320 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1322 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1323 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1324 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1325 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1326 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1327 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1328 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1329 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1330 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1331 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1332 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1333 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1334 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1335 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1336 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1337 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1338 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1339 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1340 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1341 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1342 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1343 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1344 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1345 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1346 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1347 } else { // Custom user agent.
1348 menu.findItem(R.id.user_agent_custom).setChecked(true);
1351 // Instantiate the font size title and the selected font size menu item.
1352 String fontSizeTitle;
1353 MenuItem selectedFontSizeMenuItem;
1355 // Prepare the font size title and current size menu item.
1358 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1359 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1363 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1364 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1368 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1369 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1373 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1374 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1378 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1379 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1383 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1384 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1388 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1389 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1393 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1394 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1398 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1399 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1403 // Set the font size title and select the current size menu item.
1404 fontSizeMenuItem.setTitle(fontSizeTitle);
1405 selectedFontSizeMenuItem.setChecked(true);
1407 // Run all the other default commands.
1408 super.onPrepareOptionsMenu(menu);
1410 // Display the menu.
1415 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1416 @SuppressLint("SetJavaScriptEnabled")
1417 // removeAllCookies is deprecated, but it is required for API < 21.
1418 @SuppressWarnings("deprecation")
1419 public boolean onOptionsItemSelected(MenuItem menuItem) {
1420 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1421 if (inFullScreenBrowsingMode) {
1422 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1423 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1425 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1427 /* Hide the system bars.
1428 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1429 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1430 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1431 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1433 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1434 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1437 // Get the selected menu item ID.
1438 int menuItemId = menuItem.getItemId();
1440 // Get a handle for the shared preferences.
1441 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1443 // Get a handle for the cookie manager.
1444 CookieManager cookieManager = CookieManager.getInstance();
1446 // Run the commands that correlate to the selected menu item.
1447 switch (menuItemId) {
1448 case R.id.toggle_javascript:
1449 // Toggle the JavaScript status.
1450 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1452 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1453 updatePrivacyIcons(true);
1455 // Display a `Snackbar`.
1456 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
1457 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1458 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled.
1459 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1460 } else { // Privacy mode.
1461 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1464 // Reload the current WebView.
1465 currentWebView.reload();
1468 case R.id.add_or_edit_domain:
1469 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1470 // Reapply the domain settings on returning to `MainWebViewActivity`.
1471 reapplyDomainSettingsOnRestart = true;
1472 currentWebView.resetCurrentDomainName();
1474 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
1475 // Store the current SSL certificate and IP addresses in the domains activity.
1476 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1477 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1479 // Create an intent to launch the domains activity.
1480 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1482 // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
1483 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1484 domainsIntent.putExtra("close_on_back", true);
1487 startActivity(domainsIntent);
1488 } else { // Add a new domain.
1489 // Apply the new domain settings on returning to `MainWebViewActivity`.
1490 reapplyDomainSettingsOnRestart = true;
1491 currentWebView.resetCurrentDomainName();
1493 // Get the current domain
1494 Uri currentUri = Uri.parse(formattedUrlString);
1495 String currentDomain = currentUri.getHost();
1497 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1498 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1500 // Create the domain and store the database ID.
1501 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1503 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
1504 // Store the current SSL certificate and IP addresses in the domains activity.
1505 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1506 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1508 // Create an intent to launch the domains activity.
1509 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1511 // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
1512 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1513 domainsIntent.putExtra("close_on_back", true);
1516 startActivity(domainsIntent);
1520 case R.id.toggle_first_party_cookies:
1521 // Switch the status of firstPartyCookiesEnabled.
1522 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
1524 // Update the menu checkbox.
1525 menuItem.setChecked(firstPartyCookiesEnabled);
1527 // Apply the new cookie status.
1528 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1530 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1531 updatePrivacyIcons(true);
1533 // Display a `Snackbar`.
1534 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
1535 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1536 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1537 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1538 } else { // Privacy mode.
1539 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1542 // Reload the current WebView.
1543 currentWebView.reload();
1546 case R.id.toggle_third_party_cookies:
1547 if (Build.VERSION.SDK_INT >= 21) {
1548 // Switch the status of thirdPartyCookiesEnabled.
1549 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1551 // Update the menu checkbox.
1552 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1554 // Display a snackbar.
1555 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1556 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1558 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1561 // Reload the current WebView.
1562 currentWebView.reload();
1563 } // Else do nothing because SDK < 21.
1566 case R.id.toggle_dom_storage:
1567 // Toggle the status of domStorageEnabled.
1568 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1570 // Update the menu checkbox.
1571 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1573 // Update the privacy icon. `true` refreshes the app bar icons.
1574 updatePrivacyIcons(true);
1576 // Display a snackbar.
1577 if (currentWebView.getSettings().getDomStorageEnabled()) {
1578 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1580 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1583 // Reload the current WebView.
1584 currentWebView.reload();
1587 // Form data can be removed once the minimum API >= 26.
1588 case R.id.toggle_save_form_data:
1589 // Switch the status of saveFormDataEnabled.
1590 saveFormDataEnabled = !saveFormDataEnabled;
1592 // Update the menu checkbox.
1593 menuItem.setChecked(saveFormDataEnabled);
1595 // Apply the new form data status.
1596 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
1598 // Display a `Snackbar`.
1599 if (saveFormDataEnabled) {
1600 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1602 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1605 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1606 updatePrivacyIcons(true);
1608 // Reload the current WebView.
1609 currentWebView.reload();
1612 case R.id.clear_cookies:
1613 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1614 .setAction(R.string.undo, v -> {
1615 // Do nothing because everything will be handled by `onDismissed()` below.
1617 .addCallback(new Snackbar.Callback() {
1618 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1620 public void onDismissed(Snackbar snackbar, int event) {
1622 // The user pushed the undo button.
1623 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1627 // The snackbar was dismissed without the undo button being pushed.
1629 // `cookieManager.removeAllCookie()` varies by SDK.
1630 if (Build.VERSION.SDK_INT < 21) {
1631 cookieManager.removeAllCookie();
1633 cookieManager.removeAllCookies(null);
1641 case R.id.clear_dom_storage:
1642 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1643 .setAction(R.string.undo, v -> {
1644 // Do nothing because everything will be handled by `onDismissed()` below.
1646 .addCallback(new Snackbar.Callback() {
1647 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1649 public void onDismissed(Snackbar snackbar, int event) {
1651 // The user pushed the undo button.
1652 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1656 // The snackbar was dismissed without the undo button being pushed.
1658 // Delete the DOM Storage.
1659 WebStorage webStorage = WebStorage.getInstance();
1660 webStorage.deleteAllData();
1662 // Initialize a handler to manually delete the DOM storage files and directories.
1663 Handler deleteDomStorageHandler = new Handler();
1665 // Setup a runnable to manually delete the DOM storage files and directories.
1666 Runnable deleteDomStorageRunnable = () -> {
1668 // Get a handle for the runtime.
1669 Runtime runtime = Runtime.getRuntime();
1671 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1672 Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1674 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1675 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1676 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1677 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1678 Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1680 // Wait for the processes to finish.
1681 deleteLocalStorageProcess.waitFor();
1682 deleteIndexProcess.waitFor();
1683 deleteQuotaManagerProcess.waitFor();
1684 deleteQuotaManagerJournalProcess.waitFor();
1685 deleteDatabasesProcess.waitFor();
1686 } catch (Exception exception) {
1687 // Do nothing if an error is thrown.
1691 // Manually delete the DOM storage files after 200 milliseconds.
1692 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1699 // Form data can be remove once the minimum API >= 26.
1700 case R.id.clear_form_data:
1701 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1702 .setAction(R.string.undo, v -> {
1703 // Do nothing because everything will be handled by `onDismissed()` below.
1705 .addCallback(new Snackbar.Callback() {
1706 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1708 public void onDismissed(Snackbar snackbar, int event) {
1710 // The user pushed the undo button.
1711 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1715 // The snackbar was dismissed without the `Undo` button being pushed.
1717 // Delete the form data.
1718 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1719 mainWebViewDatabase.clearFormData();
1727 // Toggle the EasyList status.
1728 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1730 // Update the menu checkbox.
1731 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1733 // Reload the current WebView.
1734 currentWebView.reload();
1737 case R.id.easyprivacy:
1738 // Toggle the EasyPrivacy status.
1739 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1741 // Update the menu checkbox.
1742 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1744 // Reload the current WebView.
1745 currentWebView.reload();
1748 case R.id.fanboys_annoyance_list:
1749 // Toggle Fanboy's Annoyance List status.
1750 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1752 // Update the menu checkbox.
1753 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1755 // Update the staus of Fanboy's Social Blocking List.
1756 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1757 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1759 // Reload the current WebView.
1760 currentWebView.reload();
1763 case R.id.fanboys_social_blocking_list:
1764 // Toggle Fanboy's Social Blocking List status.
1765 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1767 // Update the menu checkbox.
1768 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1770 // Reload the current WebView.
1771 currentWebView.reload();
1774 case R.id.ultraprivacy:
1775 // Toggle the UltraPrivacy status.
1776 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1778 // Update the menu checkbox.
1779 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1781 // Reload the current WebView.
1782 currentWebView.reload();
1785 case R.id.block_all_third_party_requests:
1786 //Toggle the third-party requests blocker status.
1787 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1789 // Update the menu checkbox.
1790 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1792 // Reload the current WebView.
1793 currentWebView.reload();
1796 case R.id.user_agent_privacy_browser:
1797 // Update the user agent.
1798 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1800 // Reload the current WebView.
1801 currentWebView.reload();
1804 case R.id.user_agent_webview_default:
1805 // Update the user agent.
1806 currentWebView.getSettings().setUserAgentString("");
1808 // Reload the current WebView.
1809 currentWebView.reload();
1812 case R.id.user_agent_firefox_on_android:
1813 // Update the user agent.
1814 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1816 // Reload the current WebView.
1817 currentWebView.reload();
1820 case R.id.user_agent_chrome_on_android:
1821 // Update the user agent.
1822 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1824 // Reload the current WebView.
1825 currentWebView.reload();
1828 case R.id.user_agent_safari_on_ios:
1829 // Update the user agent.
1830 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1832 // Reload the current WebView.
1833 currentWebView.reload();
1836 case R.id.user_agent_firefox_on_linux:
1837 // Update the user agent.
1838 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1840 // Reload the current WebView.
1841 currentWebView.reload();
1844 case R.id.user_agent_chromium_on_linux:
1845 // Update the user agent.
1846 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1848 // Reload the current WebView.
1849 currentWebView.reload();
1852 case R.id.user_agent_firefox_on_windows:
1853 // Update the user agent.
1854 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1856 // Reload the current WebView.
1857 currentWebView.reload();
1860 case R.id.user_agent_chrome_on_windows:
1861 // Update the user agent.
1862 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1864 // Reload the current WebView.
1865 currentWebView.reload();
1868 case R.id.user_agent_edge_on_windows:
1869 // Update the user agent.
1870 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1872 // Reload the current WebView.
1873 currentWebView.reload();
1876 case R.id.user_agent_internet_explorer_on_windows:
1877 // Update the user agent.
1878 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1880 // Reload the current WebView.
1881 currentWebView.reload();
1884 case R.id.user_agent_safari_on_macos:
1885 // Update the user agent.
1886 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1888 // Reload the current WebView.
1889 currentWebView.reload();
1892 case R.id.user_agent_custom:
1893 // Update the user agent.
1894 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1896 // Reload the current WebView.
1897 currentWebView.reload();
1900 case R.id.font_size_twenty_five_percent:
1901 currentWebView.getSettings().setTextZoom(25);
1904 case R.id.font_size_fifty_percent:
1905 currentWebView.getSettings().setTextZoom(50);
1908 case R.id.font_size_seventy_five_percent:
1909 currentWebView.getSettings().setTextZoom(75);
1912 case R.id.font_size_one_hundred_percent:
1913 currentWebView.getSettings().setTextZoom(100);
1916 case R.id.font_size_one_hundred_twenty_five_percent:
1917 currentWebView.getSettings().setTextZoom(125);
1920 case R.id.font_size_one_hundred_fifty_percent:
1921 currentWebView.getSettings().setTextZoom(150);
1924 case R.id.font_size_one_hundred_seventy_five_percent:
1925 currentWebView.getSettings().setTextZoom(175);
1928 case R.id.font_size_two_hundred_percent:
1929 currentWebView.getSettings().setTextZoom(200);
1932 case R.id.swipe_to_refresh:
1933 // Get a handle for the swipe refresh layout.
1934 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1936 // Toggle swipe to refresh.
1937 swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
1940 case R.id.display_images:
1941 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1942 // Disable loading of images.
1943 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1945 // Reload the website to remove existing images.
1946 currentWebView.reload();
1947 } else { // Images are not currently loaded automatically.
1948 // Enable loading of images. Missing images will be loaded without the need for a reload.
1949 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1953 case R.id.night_mode:
1954 // Toggle night mode.
1955 nightMode = !nightMode;
1957 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1958 if (nightMode) { // Night mode is enabled, which requires JavaScript.
1959 // Enable JavaScript.
1960 currentWebView.getSettings().setJavaScriptEnabled(true);
1961 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
1962 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1963 currentWebView.getSettings().setJavaScriptEnabled(domainSettingsJavaScriptEnabled);
1964 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
1965 // Apply the JavaScript preference.
1966 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1969 // Update the privacy icons.
1970 updatePrivacyIcons(false);
1972 // Reload the website.
1973 currentWebView.reload();
1976 case R.id.find_on_page:
1977 // Get a handle for the views.
1978 Toolbar toolbar = findViewById(R.id.toolbar);
1979 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1981 // Hide the toolbar.
1982 toolbar.setVisibility(View.GONE);
1984 // Show the find on page linear layout.
1985 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1987 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1988 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1989 findOnPageEditText.postDelayed(() -> {
1990 // Set the focus on `findOnPageEditText`.
1991 findOnPageEditText.requestFocus();
1993 // Display the keyboard. `0` sets no input flags.
1994 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1998 case R.id.view_source:
1999 // Create an intent to launch the view source activity.
2000 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
2002 // Add the user agent as an extra to the intent.
2003 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
2006 startActivity(viewSourceIntent);
2009 case R.id.share_url:
2010 // Setup the share string.
2011 String shareString = currentWebView.getTitle() + " – " + formattedUrlString;
2013 // Create the share intent.
2014 Intent shareIntent = new Intent(Intent.ACTION_SEND);
2015 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2016 shareIntent.setType("text/plain");
2019 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2023 // Get a `PrintManager` instance.
2024 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2026 // Create a print document adapter form the current WebView.
2027 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2029 // Remove the lint error below that `printManager` might be `null`.
2030 assert printManager != null;
2032 // Print the document. The print attributes are `null`.
2033 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2036 case R.id.open_with_app:
2037 openWithApp(formattedUrlString);
2040 case R.id.open_with_browser:
2041 openWithBrowser(formattedUrlString);
2044 case R.id.add_to_homescreen:
2045 // Instantiate the create home screen shortcut dialog.
2046 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, currentWebView.getFavoriteOrDefaultIcon());
2048 // Show the create home screen shortcut dialog.
2049 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2052 case R.id.proxy_through_orbot:
2053 // Toggle the proxy through Orbot variable.
2054 proxyThroughOrbot = !proxyThroughOrbot;
2056 // Apply the proxy through Orbot settings.
2057 applyProxyThroughOrbot(true);
2061 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2062 // Reload the current WebView.
2063 currentWebView.reload();
2064 } else { // The stop button was pushed.
2065 // Stop the loading of the WebView.
2066 currentWebView.stopLoading();
2070 case R.id.ad_consent:
2071 // Display the ad consent dialog.
2072 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2073 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2077 // Don't consume the event.
2078 return super.onOptionsItemSelected(menuItem);
2082 // removeAllCookies is deprecated, but it is required for API < 21.
2083 @SuppressWarnings("deprecation")
2085 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2086 // Get the menu item ID.
2087 int menuItemId = menuItem.getItemId();
2089 // Run the commands that correspond to the selected menu item.
2090 switch (menuItemId) {
2091 case R.id.close_tab:
2092 // Get a handle for the tab layout and the view pager.
2093 TabLayout tabLayout = findViewById(R.id.tablayout);
2094 ViewPager webViewPager = findViewById(R.id.webviewpager);
2096 // Get the current tab number.
2097 int currentTabNumber = tabLayout.getSelectedTabPosition();
2099 // Delete the current tab.
2100 tabLayout.removeTabAt(currentTabNumber);
2102 // 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.
2103 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
2104 setCurrentWebView(currentTabNumber);
2108 case R.id.clear_and_exit:
2109 // Close the bookmarks cursor and database.
2110 bookmarksCursor.close();
2111 bookmarksDatabaseHelper.close();
2113 // Get a handle for the shared preferences.
2114 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2116 // Get the status of the clear everything preference.
2117 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2119 // Get a handle for the runtime.
2120 Runtime runtime = Runtime.getRuntime();
2123 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2124 // The command to remove cookies changed slightly in API 21.
2125 if (Build.VERSION.SDK_INT >= 21) {
2126 CookieManager.getInstance().removeAllCookies(null);
2128 CookieManager.getInstance().removeAllCookie();
2131 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2133 // Two commands must be used because `Runtime.exec()` does not like `*`.
2134 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2135 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2137 // Wait until the processes have finished.
2138 deleteCookiesProcess.waitFor();
2139 deleteCookiesJournalProcess.waitFor();
2140 } catch (Exception exception) {
2141 // Do nothing if an error is thrown.
2145 // Clear DOM storage.
2146 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2147 // Ask `WebStorage` to clear the DOM storage.
2148 WebStorage webStorage = WebStorage.getInstance();
2149 webStorage.deleteAllData();
2151 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2153 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2154 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2156 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2157 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2158 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2159 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2160 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2162 // Wait until the processes have finished.
2163 deleteLocalStorageProcess.waitFor();
2164 deleteIndexProcess.waitFor();
2165 deleteQuotaManagerProcess.waitFor();
2166 deleteQuotaManagerJournalProcess.waitFor();
2167 deleteDatabaseProcess.waitFor();
2168 } catch (Exception exception) {
2169 // Do nothing if an error is thrown.
2173 // Clear form data if the API < 26.
2174 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2175 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2176 webViewDatabase.clearFormData();
2178 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2180 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2181 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2182 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2184 // Wait until the processes have finished.
2185 deleteWebDataProcess.waitFor();
2186 deleteWebDataJournalProcess.waitFor();
2187 } catch (Exception exception) {
2188 // Do nothing if an error is thrown.
2193 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2194 // Clear the cache from each WebView.
2195 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2196 // Get the WebView tab fragment.
2197 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2199 // Get the fragment view.
2200 View fragmentView = webViewTabFragment.getView();
2202 // Only clear the cache if the WebView exists.
2203 if (fragmentView != null) {
2204 // Get the nested scroll WebView from the tab fragment.
2205 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2207 // Clear the cache for this WebView.
2208 nestedScrollWebView.clearCache(true);
2212 // Manually delete the cache directories.
2214 // Delete the main cache directory.
2215 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2217 // Delete the secondary `Service Worker` cache directory.
2218 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2219 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2221 // Wait until the processes have finished.
2222 deleteCacheProcess.waitFor();
2223 deleteServiceWorkerProcess.waitFor();
2224 } catch (Exception exception) {
2225 // Do nothing if an error is thrown.
2229 // Wipe out each WebView.
2230 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2231 // Get the WebView tab fragment.
2232 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2234 // Get the fragment view.
2235 View fragmentView = webViewTabFragment.getView();
2237 // Only wipe out the WebView if it exists.
2238 if (fragmentView != null) {
2239 // Get the nested scroll WebView from the tab fragment.
2240 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2242 // Clear SSL certificate preferences for this WebView.
2243 nestedScrollWebView.clearSslPreferences();
2245 // Clear the back/forward history for this WebView.
2246 nestedScrollWebView.clearHistory();
2248 // Destroy the internal state of `mainWebView`.
2249 nestedScrollWebView.destroy();
2253 // Clear the formatted URL string.
2254 formattedUrlString = null;
2256 // Clear the custom headers.
2257 customHeaders.clear();
2259 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2260 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2261 if (clearEverything) {
2263 // Delete the folder.
2264 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2266 // Wait until the process has finished.
2267 deleteAppWebviewProcess.waitFor();
2268 } catch (Exception exception) {
2269 // Do nothing if an error is thrown.
2273 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2274 if (Build.VERSION.SDK_INT >= 21) {
2275 finishAndRemoveTask();
2280 // Remove the terminated program from RAM. The status code is `0`.
2289 if (currentWebView.canGoBack()) {
2290 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2291 formattedUrlString = "";
2293 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2294 navigatingHistory = true;
2296 // Load the previous website in the history.
2297 currentWebView.goBack();
2302 if (currentWebView.canGoForward()) {
2303 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2304 formattedUrlString = "";
2306 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2307 navigatingHistory = true;
2309 // Load the next website in the history.
2310 currentWebView.goForward();
2315 // Get the `WebBackForwardList`.
2316 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2318 // Show the URL history dialog and name this instance `R.string.history`.
2319 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2320 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2324 // Populate the resource requests.
2325 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2327 // Create an intent to launch the Requests activity.
2328 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2330 // Add the block third-party requests status to the intent.
2331 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2334 startActivity(requestsIntent);
2337 case R.id.downloads:
2338 // Launch the system Download Manager.
2339 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2341 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2342 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2344 startActivity(downloadManagerIntent);
2348 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2349 reapplyDomainSettingsOnRestart = true;
2350 currentWebView.resetCurrentDomainName(); // TODO. Do this for all tabs.
2352 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
2353 // Store the current SSL certificate and IP addresses in the domains activity.
2354 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
2355 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
2357 // Launch the domains activity.
2358 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2359 startActivity(domainsIntent);
2363 // Set the flag to reapply app settings on restart when returning from Settings.
2364 reapplyAppSettingsOnRestart = true;
2366 // Set the flag to reapply the domain settings on restart when returning from Settings.
2367 reapplyDomainSettingsOnRestart = true;
2368 currentWebView.resetCurrentDomainName(); // TODO. Do this for all tabs.
2370 // Launch the settings activity.
2371 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2372 startActivity(settingsIntent);
2375 case R.id.import_export:
2376 // Launch the import/export activity.
2377 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2378 startActivity(importExportIntent);
2382 // Launch the logcat activity.
2383 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2384 startActivity(logcatIntent);
2388 // Launch `GuideActivity`.
2389 Intent guideIntent = new Intent(this, GuideActivity.class);
2390 startActivity(guideIntent);
2394 // Launch `AboutActivity`.
2395 Intent aboutIntent = new Intent(this, AboutActivity.class);
2396 startActivity(aboutIntent);
2400 // Get a handle for the drawer layout.
2401 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2403 // Close the navigation drawer.
2404 drawerLayout.closeDrawer(GravityCompat.START);
2409 public void onPostCreate(Bundle savedInstanceState) {
2410 // Run the default commands.
2411 super.onPostCreate(savedInstanceState);
2413 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2414 actionBarDrawerToggle.syncState();
2418 public void onConfigurationChanged(Configuration newConfig) {
2419 // Run the default commands.
2420 super.onConfigurationChanged(newConfig);
2422 // Get the status bar pixel size.
2423 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2424 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2426 // Get the resource density.
2427 float screenDensity = getResources().getDisplayMetrics().density;
2429 // Recalculate the drawer header padding.
2430 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2431 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2432 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2434 // Reload the ad for the free flavor if not in full screen mode.
2435 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2436 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2437 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2440 // `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:
2441 // https://code.google.com/p/android/issues/detail?id=20493#c8
2442 // ActivityCompat.invalidateOptionsMenu(this);
2446 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2447 // Store the `HitTestResult`.
2448 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2451 final String imageUrl;
2452 final String linkUrl;
2454 // Get a handle for the the clipboard and fragment managers.
2455 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2456 FragmentManager fragmentManager = getSupportFragmentManager();
2458 // Remove the lint errors below that `clipboardManager` might be `null`.
2459 assert clipboardManager != null;
2461 switch (hitTestResult.getType()) {
2462 // `SRC_ANCHOR_TYPE` is a link.
2463 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2464 // Get the target URL.
2465 linkUrl = hitTestResult.getExtra();
2467 // Set the target URL as the title of the `ContextMenu`.
2468 menu.setHeaderTitle(linkUrl);
2470 // Add a Load URL entry.
2471 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
2476 // Add a Copy URL entry.
2477 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2478 // Save the link URL in a `ClipData`.
2479 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2481 // Set the `ClipData` as the clipboard's primary clip.
2482 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2486 // Add a Download URL entry.
2487 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2488 // Check if the download should be processed by an external app.
2489 if (downloadWithExternalApp) { // Download with an external app.
2490 openUrlWithExternalApp(linkUrl);
2491 } else { // Download with Android's download manager.
2492 // Check to see if the storage permission has already been granted.
2493 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2494 // Store the variables for future use by `onRequestPermissionsResult()`.
2495 downloadUrl = linkUrl;
2496 downloadContentDisposition = "none";
2497 downloadContentLength = -1;
2499 // Show a dialog if the user has previously denied the permission.
2500 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2501 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2502 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2504 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2505 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2506 } else { // Show the permission request directly.
2507 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2508 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2510 } else { // The storage permission has already been granted.
2511 // Get a handle for the download file alert dialog.
2512 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2514 // Show the download file alert dialog.
2515 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2521 // Add an Open with App entry.
2522 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2523 openWithApp(linkUrl);
2527 // Add an Open with Browser entry.
2528 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2529 openWithBrowser(linkUrl);
2533 // Add a Cancel entry, which by default closes the context menu.
2534 menu.add(R.string.cancel);
2537 case WebView.HitTestResult.EMAIL_TYPE:
2538 // Get the target URL.
2539 linkUrl = hitTestResult.getExtra();
2541 // Set the target URL as the title of the `ContextMenu`.
2542 menu.setHeaderTitle(linkUrl);
2544 // Add a Write Email entry.
2545 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2546 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2547 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2549 // Parse the url and set it as the data for the `Intent`.
2550 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2552 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2553 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2556 startActivity(emailIntent);
2560 // Add a Copy Email Address entry.
2561 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2562 // Save the email address in a `ClipData`.
2563 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2565 // Set the `ClipData` as the clipboard's primary clip.
2566 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2570 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2571 menu.add(R.string.cancel);
2574 // `IMAGE_TYPE` is an image.
2575 case WebView.HitTestResult.IMAGE_TYPE:
2576 // Get the image URL.
2577 imageUrl = hitTestResult.getExtra();
2579 // Set the image URL as the title of the `ContextMenu`.
2580 menu.setHeaderTitle(imageUrl);
2582 // Add a View Image entry.
2583 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2588 // Add a Download Image entry.
2589 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2590 // Check if the download should be processed by an external app.
2591 if (downloadWithExternalApp) { // Download with an external app.
2592 openUrlWithExternalApp(imageUrl);
2593 } else { // Download with Android's download manager.
2594 // Check to see if the storage permission has already been granted.
2595 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2596 // Store the image URL for use by `onRequestPermissionResult()`.
2597 downloadImageUrl = imageUrl;
2599 // Show a dialog if the user has previously denied the permission.
2600 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2601 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2602 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2604 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2605 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2606 } else { // Show the permission request directly.
2607 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2608 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2610 } else { // The storage permission has already been granted.
2611 // Get a handle for the download image alert dialog.
2612 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2614 // Show the download image alert dialog.
2615 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2621 // Add a Copy URL entry.
2622 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2623 // Save the image URL in a `ClipData`.
2624 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2626 // Set the `ClipData` as the clipboard's primary clip.
2627 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2631 // Add an Open with App entry.
2632 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2633 openWithApp(imageUrl);
2637 // Add an Open with Browser entry.
2638 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2639 openWithBrowser(imageUrl);
2643 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2644 menu.add(R.string.cancel);
2648 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2649 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2650 // Get the image URL.
2651 imageUrl = hitTestResult.getExtra();
2653 // Set the image URL as the title of the `ContextMenu`.
2654 menu.setHeaderTitle(imageUrl);
2656 // Add a `View Image` entry.
2657 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2662 // Add a `Download Image` entry.
2663 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2664 // Check if the download should be processed by an external app.
2665 if (downloadWithExternalApp) { // Download with an external app.
2666 openUrlWithExternalApp(imageUrl);
2667 } else { // Download with Android's download manager.
2668 // Check to see if the storage permission has already been granted.
2669 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2670 // Store the image URL for use by `onRequestPermissionResult()`.
2671 downloadImageUrl = imageUrl;
2673 // Show a dialog if the user has previously denied the permission.
2674 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2675 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2676 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2678 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2679 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2680 } else { // Show the permission request directly.
2681 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2682 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2684 } else { // The storage permission has already been granted.
2685 // Get a handle for the download image alert dialog.
2686 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2688 // Show the download image alert dialog.
2689 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2695 // Add a `Copy URL` entry.
2696 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2697 // Save the image URL in a `ClipData`.
2698 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2700 // Set the `ClipData` as the clipboard's primary clip.
2701 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2705 // Add an Open with App entry.
2706 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2707 openWithApp(imageUrl);
2711 // Add an Open with Browser entry.
2712 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2713 openWithBrowser(imageUrl);
2717 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2718 menu.add(R.string.cancel);
2724 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2725 // Get the views from the dialog fragment.
2726 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2727 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2729 // Extract the strings from the edit texts.
2730 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2731 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2733 // Create a favorite icon byte array output stream.
2734 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2736 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2737 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2739 // Convert the favorite icon byte array stream to a byte array.
2740 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2742 // Display the new bookmark below the current items in the (0 indexed) list.
2743 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2745 // Create the bookmark.
2746 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2748 // Update the bookmarks cursor with the current contents of this folder.
2749 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2751 // Update the list view.
2752 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2754 // Scroll to the new bookmark.
2755 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2759 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2760 // Get handles for the views in the dialog fragment.
2761 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2762 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2763 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2765 // Get new folder name string.
2766 String folderNameString = createFolderNameEditText.getText().toString();
2768 // Create a folder icon bitmap.
2769 Bitmap folderIconBitmap;
2771 // Set the folder icon bitmap according to the dialog.
2772 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2773 // Get the default folder icon drawable.
2774 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2776 // Convert the folder icon drawable to a bitmap drawable.
2777 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2779 // Convert the folder icon bitmap drawable to a bitmap.
2780 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2781 } else { // Use the WebView favorite icon.
2782 // Copy the favorite icon bitmap to the folder icon bitmap.
2783 folderIconBitmap = favoriteIconBitmap;
2786 // Create a folder icon byte array output stream.
2787 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2789 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2790 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2792 // Convert the folder icon byte array stream to a byte array.
2793 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2795 // Move all the bookmarks down one in the display order.
2796 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2797 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2798 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2801 // Create the folder, which will be placed at the top of the `ListView`.
2802 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2804 // Update the bookmarks cursor with the current contents of this folder.
2805 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2807 // Update the `ListView`.
2808 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2810 // Scroll to the new folder.
2811 bookmarksListView.setSelection(0);
2815 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2816 // Get handles for the views from `dialogFragment`.
2817 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2818 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2819 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2821 // Store the bookmark strings.
2822 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2823 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2825 // Update the bookmark.
2826 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2827 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2828 } else { // Update the bookmark using the `WebView` favorite icon.
2829 // Create a favorite icon byte array output stream.
2830 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2832 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2833 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2835 // Convert the favorite icon byte array stream to a byte array.
2836 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2838 // Update the bookmark and the favorite icon.
2839 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2842 // Update the bookmarks cursor with the current contents of this folder.
2843 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2845 // Update the list view.
2846 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2850 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2851 // Get handles for the views from `dialogFragment`.
2852 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2853 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2854 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2855 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2857 // Get the new folder name.
2858 String newFolderNameString = editFolderNameEditText.getText().toString();
2860 // Check if the favorite icon has changed.
2861 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2862 // Update the name in the database.
2863 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2864 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2865 // Create the new folder icon Bitmap.
2866 Bitmap folderIconBitmap;
2868 // Populate the new folder icon bitmap.
2869 if (defaultFolderIconRadioButton.isChecked()) {
2870 // Get the default folder icon drawable.
2871 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2873 // Convert the folder icon drawable to a bitmap drawable.
2874 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2876 // Convert the folder icon bitmap drawable to a bitmap.
2877 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2878 } else { // Use the `WebView` favorite icon.
2879 // Copy the favorite icon bitmap to the folder icon bitmap.
2880 folderIconBitmap = favoriteIconBitmap;
2883 // Create a folder icon byte array output stream.
2884 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2886 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2887 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2889 // Convert the folder icon byte array stream to a byte array.
2890 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2892 // Update the folder icon in the database.
2893 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2894 } else { // The folder icon and the name have changed.
2895 // Get the new folder icon `Bitmap`.
2896 Bitmap folderIconBitmap;
2897 if (defaultFolderIconRadioButton.isChecked()) {
2898 // Get the default folder icon drawable.
2899 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2901 // Convert the folder icon drawable to a bitmap drawable.
2902 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2904 // Convert the folder icon bitmap drawable to a bitmap.
2905 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2906 } else { // Use the `WebView` favorite icon.
2907 // Copy the favorite icon bitmap to the folder icon bitmap.
2908 folderIconBitmap = favoriteIconBitmap;
2911 // Create a folder icon byte array output stream.
2912 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2914 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2915 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2917 // Convert the folder icon byte array stream to a byte array.
2918 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2920 // Update the folder name and icon in the database.
2921 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2924 // Update the bookmarks cursor with the current contents of this folder.
2925 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2927 // Update the `ListView`.
2928 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2932 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2933 switch (downloadType) {
2934 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2935 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2936 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2939 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2940 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2941 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2947 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2948 // Get a handle for the fragment manager.
2949 FragmentManager fragmentManager = getSupportFragmentManager();
2951 switch (requestCode) {
2952 case DOWNLOAD_FILE_REQUEST_CODE:
2953 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2954 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2956 // On API 23, displaying the fragment must be delayed or the app will crash.
2957 if (Build.VERSION.SDK_INT == 23) {
2958 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2960 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2963 // Reset the download variables.
2965 downloadContentDisposition = "";
2966 downloadContentLength = 0;
2969 case DOWNLOAD_IMAGE_REQUEST_CODE:
2970 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2971 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
2973 // On API 23, displaying the fragment must be delayed or the app will crash.
2974 if (Build.VERSION.SDK_INT == 23) {
2975 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2977 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2980 // Reset the image URL variable.
2981 downloadImageUrl = "";
2987 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
2988 // Download the image if it has an HTTP or HTTPS URI.
2989 if (imageUrl.startsWith("http")) {
2990 // Get a handle for the system `DOWNLOAD_SERVICE`.
2991 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2993 // Parse `imageUrl`.
2994 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
2996 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
2997 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2998 if (firstPartyCookiesEnabled) {
2999 // Get the cookies for `imageUrl`.
3000 String cookies = CookieManager.getInstance().getCookie(imageUrl);
3002 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3003 downloadRequest.addRequestHeader("Cookie", cookies);
3006 // Get the file name from the dialog fragment.
3007 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3008 String imageName = downloadImageNameEditText.getText().toString();
3010 // Specify the download location.
3011 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3012 // Download to the public download directory.
3013 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3014 } else { // External write permission denied.
3015 // Download to the app's external download directory.
3016 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3019 // Allow `MediaScanner` to index the download if it is a media file.
3020 downloadRequest.allowScanningByMediaScanner();
3022 // Add the URL as the description for the download.
3023 downloadRequest.setDescription(imageUrl);
3025 // Show the download notification after the download is completed.
3026 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3028 // Remove the lint warning below that `downloadManager` might be `null`.
3029 assert downloadManager != null;
3031 // Initiate the download.
3032 downloadManager.enqueue(downloadRequest);
3033 } else { // The image is not an HTTP or HTTPS URI.
3034 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3039 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3040 // Download the file if it has an HTTP or HTTPS URI.
3041 if (downloadUrl.startsWith("http")) {
3042 // Get a handle for the system `DOWNLOAD_SERVICE`.
3043 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3045 // Parse `downloadUrl`.
3046 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3048 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
3049 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3050 if (firstPartyCookiesEnabled) {
3051 // Get the cookies for `downloadUrl`.
3052 String cookies = CookieManager.getInstance().getCookie(downloadUrl);
3054 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3055 downloadRequest.addRequestHeader("Cookie", cookies);
3058 // Get the file name from the dialog fragment.
3059 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3060 String fileName = downloadFileNameEditText.getText().toString();
3062 // Specify the download location.
3063 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3064 // Download to the public download directory.
3065 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3066 } else { // External write permission denied.
3067 // Download to the app's external download directory.
3068 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3071 // Allow `MediaScanner` to index the download if it is a media file.
3072 downloadRequest.allowScanningByMediaScanner();
3074 // Add the URL as the description for the download.
3075 downloadRequest.setDescription(downloadUrl);
3077 // Show the download notification after the download is completed.
3078 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3080 // Remove the lint warning below that `downloadManager` might be `null`.
3081 assert downloadManager != null;
3083 // Initiate the download.
3084 downloadManager.enqueue(downloadRequest);
3085 } else { // The download is not an HTTP or HTTPS URI.
3086 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3091 public void onHttpAuthenticationCancel() {
3092 // Cancel the `HttpAuthHandler`.
3093 httpAuthHandler.cancel();
3097 public void onHttpAuthenticationProceed(DialogFragment dialogFragment) {
3098 // Get handles for the `EditTexts`.
3099 EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3100 EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3102 // Proceed with the HTTP authentication.
3103 httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3107 public void onSslErrorCancel() { // TODO. How to handle this with multiple tabs? There could be multiple errors at once.
3108 sslErrorHandler.cancel();
3112 public void onSslErrorProceed() { // TODO. How to handle this with multiple tabs? There could be multiple errors at once.
3113 sslErrorHandler.proceed();
3117 public void onPinnedMismatchBack() { // TODO. Move this logic to the dialog.
3118 if (currentWebView.canGoBack()) { // There is a back page in the history.
3119 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3120 formattedUrlString = ""; // TODO.
3122 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3123 navigatingHistory = true; // TODO.
3126 currentWebView.goBack();
3127 } else { // There are no pages to go back to.
3128 // Load a blank page
3134 public void onPinnedMismatchProceed() { // TODO. Move this logic to the dialog.
3135 // Do not check the pinned information for this domain again until the domain changes.
3136 currentWebView.setIgnorePinnedDomainInformation(true);
3140 public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3141 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3142 formattedUrlString = "";
3144 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3145 navigatingHistory = true;
3147 // Load the history entry.
3148 currentWebView.goBackOrForward(moveBackOrForwardSteps);
3152 public void onClearHistory() {
3153 // Clear the history.
3154 currentWebView.clearHistory();
3157 // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3159 public void onBackPressed() {
3160 // Get a handle for the drawer layout.
3161 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3163 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
3164 // Close the navigation drawer.
3165 drawerLayout.closeDrawer(GravityCompat.START);
3166 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
3167 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
3168 // close the bookmarks drawer.
3169 drawerLayout.closeDrawer(GravityCompat.END);
3170 } else { // A subfolder is displayed.
3171 // Place the former parent folder in `currentFolder`.
3172 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3174 // Load the new folder.
3175 loadBookmarksFolder();
3178 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
3179 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3180 formattedUrlString = "";
3182 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3183 navigatingHistory = true;
3186 currentWebView.goBack();
3187 } else { // There isn't anything to do in Privacy Browser.
3188 // Pass `onBackPressed()` to the system.
3189 super.onBackPressed();
3193 // 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.
3195 public void onActivityResult(int requestCode, int resultCode, Intent data) {
3196 // File uploads only work on API >= 21.
3197 if (Build.VERSION.SDK_INT >= 21) {
3198 // Pass the file to the WebView.
3199 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3203 private void loadUrlFromTextBox() {
3204 // Get a handle for the URL edit text.
3205 EditText urlEditText = findViewById(R.id.url_edittext);
3207 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
3208 String unformattedUrlString = urlEditText.getText().toString().trim();
3210 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
3211 if (unformattedUrlString.startsWith("content://")) {
3212 // Load the entire content URL.
3213 formattedUrlString = unformattedUrlString;
3214 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3215 || unformattedUrlString.startsWith("file://")) {
3216 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
3217 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3218 unformattedUrlString = "https://" + unformattedUrlString;
3221 // Initialize `unformattedUrl`.
3222 URL unformattedUrl = null;
3224 // 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.
3226 unformattedUrl = new URL(unformattedUrlString);
3227 } catch (MalformedURLException e) {
3228 e.printStackTrace();
3231 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3232 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3233 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3234 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3235 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3236 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3239 Uri.Builder formattedUri = new Uri.Builder();
3240 formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3242 // Decode `formattedUri` as a `String` in `UTF-8`.
3244 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
3245 } catch (UnsupportedEncodingException exception) {
3246 // Load a blank string.
3247 formattedUrlString = "";
3249 } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
3250 // Load a blank string.
3251 formattedUrlString = "";
3252 } else { // Search for the contents of the URL box.
3253 // Create an encoded URL String.
3254 String encodedUrlString;
3256 // Sanitize the search input.
3258 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3259 } catch (UnsupportedEncodingException exception) {
3260 encodedUrlString = "";
3263 // Add the base search URL.
3264 formattedUrlString = searchURL + encodedUrlString;
3267 // 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.
3268 urlEditText.clearFocus();
3271 loadUrl(formattedUrlString);
3274 private void loadUrl(String url) {// Apply any custom domain settings.
3275 // Set the URL as the formatted URL string so that checking third-party requests works correctly.
3276 formattedUrlString = url;
3278 // Apply the domain settings.
3279 applyDomainSettings(currentWebView, url, true, false);
3282 currentWebView.loadUrl(url, customHeaders);
3285 public void findPreviousOnPage(View view) {
3286 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
3287 currentWebView.findNext(false);
3290 public void findNextOnPage(View view) {
3291 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3292 currentWebView.findNext(true);
3295 public void closeFindOnPage(View view) {
3296 // Get a handle for the views.
3297 Toolbar toolbar = findViewById(R.id.toolbar);
3298 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3300 // Delete the contents of `find_on_page_edittext`.
3301 findOnPageEditText.setText(null);
3303 // Clear the highlighted phrases.
3304 currentWebView.clearMatches();
3306 // Hide the find on page linear layout.
3307 findOnPageLinearLayout.setVisibility(View.GONE);
3309 // Show the toolbar.
3310 toolbar.setVisibility(View.VISIBLE);
3312 // Hide the keyboard.
3313 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3316 private void applyAppSettings() {
3317 // Get a handle for the shared preferences.
3318 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3320 // Store the values from the shared preferences in variables.
3321 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3322 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3323 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3324 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3325 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3326 downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
3328 // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
3329 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3330 ActionBar actionBar = getSupportActionBar();
3332 // Remove the incorrect lint warnings below that the action bar might be null.
3333 assert actionBar != null;
3335 // Apply the proxy through Orbot settings.
3336 applyProxyThroughOrbot(false);
3338 // Set Do Not Track status.
3339 if (doNotTrackEnabled) {
3340 customHeaders.put("DNT", "1");
3342 customHeaders.remove("DNT");
3345 // Set the app bar scrolling for each WebView.
3346 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3347 // Get the WebView tab fragment.
3348 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3350 // Get the fragment view.
3351 View fragmentView = webViewTabFragment.getView();
3353 // Only modify the WebViews if they exist.
3354 if (fragmentView != null) {
3355 // Get the nested scroll WebView from the tab fragment.
3356 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3358 // Set the app bar scrolling.
3359 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
3363 // Update the full screen browsing mode settings.
3364 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3365 // Update the visibility of the app bar, which might have changed in the settings.
3372 // Hide the banner ad in the free flavor.
3373 if (BuildConfig.FLAVOR.contentEquals("free")) {
3374 AdHelper.hideAd(findViewById(R.id.adview));
3377 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3378 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3380 /* Hide the system bars.
3381 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3382 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3383 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3384 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3386 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3387 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3388 } else { // Privacy Browser is not in full screen browsing mode.
3389 // 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.
3390 inFullScreenBrowsingMode = false;
3392 // Show the app bar.
3395 // Show the banner ad in the free flavor.
3396 if (BuildConfig.FLAVOR.contentEquals("free")) {
3397 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3398 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3401 // Remove the `SYSTEM_UI` flags from the root frame layout.
3402 rootFrameLayout.setSystemUiVisibility(0);
3404 // Add the translucent status flag.
3405 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3410 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3411 @SuppressLint("SetJavaScriptEnabled")
3412 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
3413 // Get a handle for the URL relative layout.
3414 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3416 // Store a copy of the current user agent to track changes for the return boolean.
3417 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3419 // Parse the URL into a URI.
3420 Uri uri = Uri.parse(url);
3422 // Extract the domain from `uri`.
3423 String newHostName = uri.getHost();
3425 // Strings don't like to be null.
3426 if (newHostName == null) {
3430 // 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.
3431 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName)) {
3432 // Set the new host name as the current domain name.
3433 nestedScrollWebView.setCurrentDomainName(newHostName);
3435 // Reset the ignoring of pinned domain information.
3436 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3438 // Clear any pinned SSL certificate or IP addresses.
3439 nestedScrollWebView.clearPinnedSslCertificate();
3440 nestedScrollWebView.clearPinnedIpAddresses();
3442 // Reset the favorite icon if specified.
3443 if (resetFavoriteIcon) {
3444 // Initialize the favorite icon.
3445 currentWebView.initializeFavoriteIcon();
3447 // Get a handle for the tab layout.
3448 TabLayout tabLayout = findViewById(R.id.tablayout);
3450 // Get the current tab.
3451 TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition()); // TODO. We need to get the tab for this WebView, which might not be the current tab.
3453 // Remove the warning below that the current tab might be null.
3454 assert currentTab != null;
3456 // Get the current tab custom view.
3457 View currentTabCustomView = currentTab.getCustomView();
3459 // Remove the warning below that the current tab custom view might be null.
3460 assert currentTabCustomView != null;
3462 // Get the current tab favorite icon image view.
3463 ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
3465 // Set the default favorite icon as the favorite icon for this tab.
3466 currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3469 // Get a handle for the swipe refresh layout.
3470 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3472 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3473 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3475 // Get a full cursor from `domainsDatabaseHelper`.
3476 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3478 // Initialize `domainSettingsSet`.
3479 Set<String> domainSettingsSet = new HashSet<>();
3481 // Get the domain name column index.
3482 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3484 // Populate `domainSettingsSet`.
3485 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3486 // Move `domainsCursor` to the current row.
3487 domainNameCursor.moveToPosition(i);
3489 // Store the domain name in `domainSettingsSet`.
3490 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3493 // Close `domainNameCursor.
3494 domainNameCursor.close();
3496 // Initialize the domain name in database variable.
3497 String domainNameInDatabase = null;
3499 // Check the hostname against the domain settings set.
3500 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3501 // Record the domain name in the database.
3502 domainNameInDatabase = newHostName;
3504 // Set the domain settings applied tracker to true.
3505 nestedScrollWebView.setDomainSettingsApplied(true);
3506 } else { // The hostname is not contained in the domain settings set.
3507 // Set the domain settings applied tracker to false.
3508 nestedScrollWebView.setDomainSettingsApplied(false);
3511 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3512 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3513 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3514 // Set the domain settings applied tracker to true.
3515 nestedScrollWebView.setDomainSettingsApplied(true);
3517 // Store the applied domain names as it appears in the database.
3518 domainNameInDatabase = "*." + newHostName;
3521 // Strip out the lowest subdomain of of the host name.
3522 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3526 // Get a handle for the shared preferences.
3527 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3529 // Store the general preference information.
3530 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3531 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3532 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3533 nightMode = sharedPreferences.getBoolean("night_mode", false); // TODO.
3534 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3536 // Get a handle for the cookie manager.
3537 CookieManager cookieManager = CookieManager.getInstance();
3539 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3540 // Get a cursor for the current host and move it to the first position.
3541 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3542 currentDomainSettingsCursor.moveToFirst();
3544 // Get the settings from the cursor.
3545 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3546 boolean domainJavaScriptEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3547 firstPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); // TODO.
3548 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3549 boolean domainDomStorageEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3550 // Form data can be removed once the minimum API >= 26.
3551 saveFormDataEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); // TODO.
3552 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST,
3553 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3554 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY,
3555 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3556 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3557 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3558 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3559 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3560 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY,
3561 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3562 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3563 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3564 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3565 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3566 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3567 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3568 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3569 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3570 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3571 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3572 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3573 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3574 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3575 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3576 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3577 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3579 // Create the pinned SSL date variables.
3580 Date pinnedSslStartDate;
3581 Date pinnedSslEndDate;
3583 // 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.
3584 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3585 pinnedSslStartDate = null;
3587 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3590 // 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.
3591 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3592 pinnedSslEndDate = null;
3594 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3597 // If there is a pinned SSL certificate, store it in the WebView.
3598 if (pinnedSslCertificate) {
3599 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3600 pinnedSslStartDate, pinnedSslEndDate);
3603 // If there is a pinned IP address, store it in the WebView.
3604 if (pinnedIpAddresses) {
3605 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3608 // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
3609 switch (nightModeInt) {
3610 case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3611 nightMode = true; // TODO.
3614 case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3615 nightMode = false; // TODO.
3620 // Store the domain JavaScript status. This is used by the options menu night mode toggle.
3621 domainSettingsJavaScriptEnabled = domainJavaScriptEnabled;
3623 // Enable JavaScript if night mode is enabled.
3625 // Enable JavaScript.
3626 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3628 // Set JavaScript according to the domain settings.
3629 nestedScrollWebView.getSettings().setJavaScriptEnabled(domainJavaScriptEnabled);
3632 // Close `currentHostDomainSettingsCursor`.
3633 currentDomainSettingsCursor.close();
3635 // Apply the domain settings.
3636 cookieManager.setAcceptCookie(firstPartyCookiesEnabled); //TODO This could be bad.
3637 nestedScrollWebView.getSettings().setDomStorageEnabled(domainDomStorageEnabled); // TODO. Move up.
3639 // Apply the form data setting if the API < 26.
3640 if (Build.VERSION.SDK_INT < 26) {
3641 nestedScrollWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3644 // Apply the font size.
3645 if (fontSize == 0) { // Apply the default font size.
3646 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3647 } else { // Apply the specified font size.
3648 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3651 // Set third-party cookies status if API >= 21.
3652 if (Build.VERSION.SDK_INT >= 21) {
3653 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3656 // 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.
3657 // <https://redmine.stoutner.com/issues/160>
3658 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3659 // Set the user agent.
3660 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3661 // Get the array position of the default user agent name.
3662 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); // TODO Could this be local.
3664 // Set the user agent according to the system default.
3665 switch (defaultUserAgentArrayPosition) {
3666 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3667 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3668 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3671 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3672 // Set the user agent to `""`, which uses the default value.
3673 nestedScrollWebView.getSettings().setUserAgentString("");
3676 case SETTINGS_CUSTOM_USER_AGENT:
3677 // Set the default custom user agent.
3678 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3682 // Get the user agent string from the user agent data array
3683 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3685 } else { // Set the user agent according to the stored name.
3686 // Get the array position of the user agent name.
3687 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3689 switch (userAgentArrayPosition) {
3690 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3691 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3694 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3695 // Set the user agent to `""`, which uses the default value.
3696 nestedScrollWebView.getSettings().setUserAgentString("");
3700 // Get the user agent string from the user agent data array.
3701 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3706 // Set swipe to refresh.
3707 switch (swipeToRefreshInt) { // TODO. This needs to be set and updated by tab.
3708 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3709 // Set swipe to refresh according to the default.
3710 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3713 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3714 // Enable swipe to refresh.
3715 swipeRefreshLayout.setEnabled(true);
3718 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3719 // Disable swipe to refresh.
3720 swipeRefreshLayout.setEnabled(false);
3723 // Set the loading of webpage images.
3724 switch (displayWebpageImagesInt) {
3725 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3726 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3729 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3730 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3733 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3734 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3738 // 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.
3740 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3742 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3744 } else { // The new URL does not have custom domain settings. Load the defaults.
3745 // Store the values from the shared preferences.
3746 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3747 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false); // TODO.
3748 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3749 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3750 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26. // TODO.
3751 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, sharedPreferences.getBoolean("easylist", true));
3752 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3753 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3754 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3755 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3756 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3758 // Enable JavaScript if night mode is enabled.
3760 // Enable JavaScript.
3761 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3763 // Set JavaScript according to the domain settings.
3764 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3767 // Apply the default settings.
3768 cookieManager.setAcceptCookie(firstPartyCookiesEnabled); // TODO.
3769 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3770 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3772 // Apply the form data setting if the API < 26.
3773 if (Build.VERSION.SDK_INT < 26) {
3774 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3777 // Reset the pinned variables.
3778 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
3780 // Set third-party cookies status if API >= 21.
3781 if (Build.VERSION.SDK_INT >= 21) {
3782 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
3785 // 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.
3786 // <https://redmine.stoutner.com/issues/160>
3787 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3788 // Get the array position of the user agent name.
3789 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3791 // Set the user agent.
3792 switch (userAgentArrayPosition) {
3793 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3794 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3795 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3798 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3799 // Set the user agent to `""`, which uses the default value.
3800 nestedScrollWebView.getSettings().setUserAgentString("");
3803 case SETTINGS_CUSTOM_USER_AGENT:
3804 // Set the default custom user agent.
3805 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3809 // Get the user agent string from the user agent data array
3810 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3814 // Set the loading of webpage images.
3815 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3817 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
3818 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
3821 // Close the domains database helper.
3822 domainsDatabaseHelper.close();
3824 // Update the privacy icons.
3825 updatePrivacyIcons(true);
3828 // Reload the website if returning from the Domains activity.
3829 if (reloadWebsite) {
3830 nestedScrollWebView.reload();
3833 // Return the user agent changed status.
3834 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
3837 private void applyProxyThroughOrbot(boolean reloadWebsite) {
3838 // Get a handle for the shared preferences.
3839 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3841 // Get the search preferences.
3842 String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
3843 String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
3844 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3845 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3846 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3847 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3849 // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
3850 ActionBar actionBar = getSupportActionBar();
3852 // Remove the incorrect lint warning later that the action bar might be null.
3853 assert actionBar != null;
3855 // Set the homepage, search, and proxy options.
3856 if (proxyThroughOrbot) { // Set the Tor options.
3857 // Set `torHomepageString` as `homepage`.
3858 homepage = torHomepageString;
3860 // If formattedUrlString is null assign the homepage to it.
3861 if (formattedUrlString == null) {
3862 formattedUrlString = homepage;
3865 // Set the search URL.
3866 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
3867 searchURL = torSearchCustomUrlString;
3868 } else { // Use the string from the pre-built list.
3869 searchURL = torSearchString;
3872 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
3873 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
3875 // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context.
3877 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
3879 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
3882 // Check to see if Orbot is ready.
3883 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
3884 // Set `waitingForOrbot`.
3885 waitingForOrbot = true;
3887 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
3888 currentWebView.getSettings().setUseWideViewPort(false);
3890 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
3891 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
3892 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
3893 // Reload the website.
3894 currentWebView.reload();
3896 } else { // Set the non-Tor options.
3897 // Set `homepageString` as `homepage`.
3898 homepage = homepageString;
3900 // If formattedUrlString is null assign the homepage to it.
3901 if (formattedUrlString == null) {
3902 formattedUrlString = homepage;
3905 // Set the search URL.
3906 if (searchString.equals("Custom URL")) { // Get the custom URL string.
3907 searchURL = searchCustomUrlString;
3908 } else { // Use the string from the pre-built list.
3909 searchURL = searchString;
3912 // Reset the proxy to default. The host is `""` and the port is `"0"`.
3913 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
3915 // Set the default `appBar` background. `this` refers to the context.
3917 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
3919 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
3922 // Reset `waitingForOrbot.
3923 waitingForOrbot = false;
3925 // Reload the WebViews if requested.
3926 if (reloadWebsite) {
3927 // Reload the WebViews.
3928 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3929 // Get the WebView tab fragment.
3930 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3932 // Get the fragment view.
3933 View fragmentView = webViewTabFragment.getView();
3935 // Only reload the WebViews if they exist.
3936 if (fragmentView != null) {
3937 // Get the nested scroll WebView from the tab fragment.
3938 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3940 // Reload the WebView.
3941 nestedScrollWebView.reload();
3948 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
3949 // Only update the privacy icons if the options menu has already been populated.
3950 if (optionsMenu != null) {
3951 // Get handles for the menu items.
3952 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
3953 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
3954 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
3955 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
3957 // Update the privacy icon.
3958 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
3959 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
3960 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled.
3961 privacyMenuItem.setIcon(R.drawable.warning);
3962 } else { // All the dangerous features are disabled.
3963 privacyMenuItem.setIcon(R.drawable.privacy_mode);
3966 // Update the first-party cookies icon.
3967 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
3968 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
3969 } else { // First-party cookies are disabled.
3971 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
3973 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
3977 // Update the DOM storage icon.
3978 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
3979 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
3980 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
3982 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
3984 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
3986 } else { // JavaScript is disabled, so DOM storage is ghosted.
3988 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
3990 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
3994 // Update the refresh icon.
3996 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
3998 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4001 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4002 if (runInvalidateOptionsMenu) {
4003 invalidateOptionsMenu();
4008 private void openUrlWithExternalApp(String url) {
4009 // Create a download intent. Not specifying the action type will display the maximum number of options.
4010 Intent downloadIntent = new Intent();
4012 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4013 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4015 // Flag the intent to open in a new task.
4016 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4018 // Show the chooser.
4019 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4022 private void highlightUrlText() {
4023 // Get a handle for the URL edit text.
4024 EditText urlEditText = findViewById(R.id.url_edittext);
4026 // Only highlight the URL text if the box is not currently selected.
4027 if (!urlEditText.hasFocus()) {
4028 // Get the URL string.
4029 String urlString = urlEditText.getText().toString();
4031 // Highlight the URL according to the protocol.
4032 if (urlString.startsWith("file://")) { // This is a file URL.
4033 // De-emphasize only the protocol.
4034 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4035 } else if (urlString.startsWith("content://")) {
4036 // De-emphasize only the protocol.
4037 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4038 } else { // This is a web URL.
4039 // Get the index of the `/` immediately after the domain name.
4040 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4042 // Create a base URL string.
4045 // Get the base URL.
4046 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4047 // Get the base URL.
4048 baseUrl = urlString.substring(0, endOfDomainName);
4049 } else { // There are no characters after the base URL.
4050 // Set the base URL to be the entire URL string.
4051 baseUrl = urlString;
4054 // Get the index of the last `.` in the domain.
4055 int lastDotIndex = baseUrl.lastIndexOf(".");
4057 // Get the index of the penultimate `.` in the domain.
4058 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4060 // Markup the beginning of the URL.
4061 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4062 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4064 // De-emphasize subdomains.
4065 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4066 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4068 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4069 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4070 // De-emphasize the protocol and the additional subdomains.
4071 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4072 } else { // There is only one subdomain in the domain name.
4073 // De-emphasize only the protocol.
4074 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4078 // De-emphasize the text after the domain name.
4079 if (endOfDomainName > 0) {
4080 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4086 private void loadBookmarksFolder() {
4087 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4088 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4090 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4091 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4093 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4094 // Inflate the individual item layout. `false` does not attach it to the root.
4095 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4099 public void bindView(View view, Context context, Cursor cursor) {
4100 // Get handles for the views.
4101 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4102 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4104 // Get the favorite icon byte array from the cursor.
4105 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4107 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4108 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4110 // Display the bitmap in `bookmarkFavoriteIcon`.
4111 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4113 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4114 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4115 bookmarkNameTextView.setText(bookmarkNameString);
4117 // Make the font bold for folders.
4118 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4119 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4120 } else { // Reset the font to default for normal bookmarks.
4121 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4126 // Populate the `ListView` with the adapter.
4127 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4129 // Set the bookmarks drawer title.
4130 if (currentBookmarksFolder.isEmpty()) {
4131 bookmarksTitleTextView.setText(R.string.bookmarks);
4133 bookmarksTitleTextView.setText(currentBookmarksFolder);
4137 private void openWithApp(String url) {
4138 // Create the open with intent with `ACTION_VIEW`.
4139 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4141 // Set the URI but not the MIME type. This should open all available apps.
4142 openWithAppIntent.setData(Uri.parse(url));
4144 // Flag the intent to open in a new task.
4145 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4147 // Show the chooser.
4148 startActivity(openWithAppIntent);
4151 private void openWithBrowser(String url) {
4152 // Create the open with intent with `ACTION_VIEW`.
4153 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4155 // Set the URI and the MIME type. `"text/html"` should load browser options.
4156 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4158 // Flag the intent to open in a new task.
4159 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4161 // Show the chooser.
4162 startActivity(openWithBrowserIntent);
4165 public void addTab(View view) {
4166 // Get a handle for the tab layout and the view pager.
4167 TabLayout tabLayout = findViewById(R.id.tablayout);
4168 ViewPager webViewPager = findViewById(R.id.webviewpager);
4170 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4171 int newTabNumber = tabLayout.getTabCount();
4174 tabLayout.addTab(tabLayout.newTab());
4177 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4179 // Remove the lint warning below that the current tab might be null.
4180 assert newTab != null;
4182 // Set a custom view on the new tab.
4183 newTab.setCustomView(R.layout.custom_tab_view);
4185 // Add the new WebView page.
4186 webViewPagerAdapter.addPage(newTabNumber, webViewPager);
4189 private void setCurrentWebView(int pageNumber) {
4190 // Get handles for the URL views.
4191 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4192 EditText urlEditText = findViewById(R.id.url_edittext);
4194 // Get the WebView tab fragment.
4195 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4197 // Get the fragment view.
4198 View fragmentView = webViewTabFragment.getView();
4200 // Remove the incorrect lint warning below that the fragment view might be null.
4201 assert fragmentView != null;
4203 // Store the current WebView.
4204 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4206 // Update the privacy icons. `true` redraws the icons in the app bar.
4207 updatePrivacyIcons(true);
4209 // Store the current formatted URL string.
4210 formattedUrlString = currentWebView.getUrl();
4212 // Clear the focus from the URL text box.
4213 urlEditText.clearFocus();
4215 // Hide the soft keyboard.
4216 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4218 // Display the current URL in the URL text box.
4219 urlEditText.setText(formattedUrlString);
4221 // Highlight the URL text.
4224 // Set the background to indicate the domain settings status.
4225 if (currentWebView.getDomainSettingsApplied()) {
4226 // 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.
4228 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4230 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4233 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4238 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar) {
4239 // Get handles for the activity views.
4240 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4241 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4242 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4243 ActionBar actionBar = getSupportActionBar();
4244 EditText urlEditText = findViewById(R.id.url_edittext);
4245 TabLayout tabLayout = findViewById(R.id.tablayout);
4246 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4248 // Remove the incorrect lint warnings below that the some of the views might be null.
4249 assert actionBar != null;
4251 // Get a handle for the activity
4252 Activity activity = this;
4254 // Get a handle for the shared preferences.
4255 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4257 // Get the relevant preferences.
4258 boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4260 // Initialize the favorite icon.
4261 nestedScrollWebView.initializeFavoriteIcon();
4263 // Set the app bar scrolling.
4264 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4266 // Allow pinch to zoom.
4267 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4269 // Hide zoom controls.
4270 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4272 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4273 if (Build.VERSION.SDK_INT >= 21) {
4274 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4277 // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
4278 nestedScrollWebView.getSettings().setUseWideViewPort(true);
4280 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4281 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4283 // Explicitly disable geolocation.
4284 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4286 // Create a double-tap gesture detector to toggle full-screen mode.
4287 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4288 // Override `onDoubleTap()`. All other events are handled using the default settings.
4290 public boolean onDoubleTap(MotionEvent event) {
4291 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4292 // Toggle the full screen browsing mode tracker.
4293 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4295 // Toggle the full screen browsing mode.
4296 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4297 // Hide the app bar if specified.
4302 // Hide the banner ad in the free flavor.
4303 if (BuildConfig.FLAVOR.contentEquals("free")) {
4304 AdHelper.hideAd(findViewById(R.id.adview));
4307 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4308 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4310 /* Hide the system bars.
4311 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4312 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4313 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4314 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4316 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4317 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4318 } else { // Switch to normal viewing mode.
4319 // Show the app bar.
4322 // Show the banner ad in the free flavor.
4323 if (BuildConfig.FLAVOR.contentEquals("free")) {
4325 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4328 // Remove the `SYSTEM_UI` flags from the root frame layout.
4329 rootFrameLayout.setSystemUiVisibility(0);
4331 // Add the translucent status flag.
4332 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4335 // Consume the double-tap.
4337 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4343 // Pass all touch events on the WebView through the double-tap gesture detector.
4344 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4345 // Call `performClick()` on the view, which is required for accessibility.
4346 view.performClick();
4348 // Send the event to the gesture detector.
4349 return doubleTapGestureDetector.onTouchEvent(event);
4352 // Register the WebView for a context menu. This is used to see link targets and download images.
4353 registerForContextMenu(nestedScrollWebView);
4355 // Allow the downloading of files.
4356 nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4357 // Check if the download should be processed by an external app.
4358 if (downloadWithExternalApp) { // Download with an external app.
4359 // Create a download intent. Not specifying the action type will display the maximum number of options.
4360 Intent downloadIntent = new Intent();
4362 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4363 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4365 // Flag the intent to open in a new task.
4366 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4368 // Show the chooser.
4369 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4370 } else { // Download with Android's download manager.
4371 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4372 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4373 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4375 // Store the variables for future use by `onRequestPermissionsResult()`.
4377 downloadContentDisposition = contentDisposition;
4378 downloadContentLength = contentLength;
4380 // Show a dialog if the user has previously denied the permission.
4381 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4382 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4383 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4385 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4386 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4387 } else { // Show the permission request directly.
4388 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4389 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4391 } else { // The storage permission has already been granted.
4392 // Get a handle for the download file alert dialog.
4393 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4395 // Show the download file alert dialog.
4396 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4401 // Update the find on page count.
4402 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4403 // Get a handle for `findOnPageCountTextView`.
4404 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4407 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4408 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4409 // Set `findOnPageCountTextView` to `0/0`.
4410 findOnPageCountTextView.setText(R.string.zero_of_zero);
4411 } else if (isDoneCounting) { // There are matches.
4412 // `activeMatchOrdinal` is zero-based.
4413 int activeMatch = activeMatchOrdinal + 1;
4415 // Build the match string.
4416 String matchString = activeMatch + "/" + numberOfMatches;
4418 // Set `findOnPageCountTextView`.
4419 findOnPageCountTextView.setText(matchString);
4424 // Set the web chrome client.
4425 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4426 // Update the progress bar when a page is loading.
4428 public void onProgressChanged(WebView view, int progress) {
4429 // Inject the night mode CSS if night mode is enabled.
4431 // `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
4432 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
4433 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
4434 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4435 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4436 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4437 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4438 // Initialize a handler to display `mainWebView`.
4439 Handler displayWebViewHandler = new Handler();
4441 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4442 Runnable displayWebViewRunnable = () -> {
4443 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
4444 if (progressBar.getVisibility() == View.GONE) {
4445 nestedScrollWebView.setVisibility(View.VISIBLE);
4449 // Displaying of `mainWebView` after 500 milliseconds.
4450 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4454 // Update the progress bar.
4455 progressBar.setProgress(progress);
4457 // Set the visibility of the progress bar.
4458 if (progress < 100) {
4459 // Show the progress bar.
4460 progressBar.setVisibility(View.VISIBLE);
4462 // Hide the progress bar.
4463 progressBar.setVisibility(View.GONE);
4465 // Display `mainWebView` if night mode is disabled.
4466 // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not
4467 // currently enabled.
4469 nestedScrollWebView.setVisibility(View.VISIBLE);
4472 //Stop the swipe to refresh indicator if it is running
4473 swipeRefreshLayout.setRefreshing(false);
4477 // Set the favorite icon when it changes.
4479 public void onReceivedIcon(WebView view, Bitmap icon) {
4480 // Only update the favorite icon if the website has finished loading.
4481 if (progressBar.getVisibility() == View.GONE) {
4482 // Store the new favorite icon.
4483 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
4485 // Get the current page position.
4486 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4488 // Get the current tab.
4489 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4491 // Remove the lint warning below that the current tab might be null.
4494 // Get the custom view from the tab.
4495 View tabView = tab.getCustomView();
4497 // Remove the incorrect warning below that the current tab view might be null.
4498 assert tabView != null;
4500 // Get the favorite icon image view from the tab.
4501 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
4503 // Display the favorite icon in the tab.
4504 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4508 // Save a copy of the title when it changes.
4510 public void onReceivedTitle(WebView view, String title) {
4511 // Get the current page position.
4512 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4514 // Get the current tab.
4515 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4517 // Only populate the title text view if the tab has been fully created.
4519 // Get the custom view from the tab.
4520 View tabView = tab.getCustomView();
4522 // Remove the incorrect warning below that the current tab view might be null.
4523 assert tabView != null;
4525 // Get the title text view from the tab.
4526 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
4528 // Set the title as the tab text.
4529 tabTitleTextView.setText(title);
4533 // Enter full screen video.
4535 public void onShowCustomView(View video, CustomViewCallback callback) {
4536 // Get a handle for the full screen video frame layout.
4537 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4539 // Set the full screen video flag.
4540 displayingFullScreenVideo = true;
4542 // Pause the ad if this is the free flavor.
4543 if (BuildConfig.FLAVOR.contentEquals("free")) {
4544 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4545 AdHelper.pauseAd(findViewById(R.id.adview));
4548 // Hide the keyboard.
4549 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4551 // Hide the main content relative layout.
4552 mainContentRelativeLayout.setVisibility(View.GONE);
4554 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4555 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4557 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4558 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4560 /* Hide the system bars.
4561 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4562 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4563 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4564 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4566 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4567 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4569 // Disable the sliding drawers.
4570 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4572 // Add the video view to the full screen video frame layout.
4573 fullScreenVideoFrameLayout.addView(video);
4575 // Show the full screen video frame layout.
4576 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4579 // Exit full screen video.
4581 public void onHideCustomView() {
4582 // Get a handle for the full screen video frame layout.
4583 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4585 // Unset the full screen video flag.
4586 displayingFullScreenVideo = false;
4588 // Remove all the views from the full screen video frame layout.
4589 fullScreenVideoFrameLayout.removeAllViews();
4591 // Hide the full screen video frame layout.
4592 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4594 // Enable the sliding drawers.
4595 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4597 // Show the main content relative layout.
4598 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4600 // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4601 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4602 // Hide the app bar if specified.
4607 // Hide the banner ad in the free flavor.
4608 if (BuildConfig.FLAVOR.contentEquals("free")) {
4609 AdHelper.hideAd(findViewById(R.id.adview));
4612 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4613 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4615 /* Hide the system bars.
4616 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4617 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4618 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4619 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4621 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4622 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4623 } else { // Switch to normal viewing mode.
4624 // Remove the `SYSTEM_UI` flags from the root frame layout.
4625 rootFrameLayout.setSystemUiVisibility(0);
4627 // Add the translucent status flag.
4628 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4631 // Reload the ad for the free flavor if not in full screen mode.
4632 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4634 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4640 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4641 // Show the file chooser if the device is running API >= 21.
4642 if (Build.VERSION.SDK_INT >= 21) {
4643 // Store the file path callback.
4644 fileChooserCallback = filePathCallback;
4646 // Create an intent to open a chooser based ont the file chooser parameters.
4647 Intent fileChooserIntent = fileChooserParams.createIntent();
4649 // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4650 startActivityForResult(fileChooserIntent, 0);
4656 nestedScrollWebView.setWebViewClient(new WebViewClient() {
4657 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4658 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4659 @SuppressWarnings("deprecation")
4661 public boolean shouldOverrideUrlLoading(WebView view, String url) {
4662 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
4663 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
4664 formattedUrlString = "";
4666 // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
4667 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
4669 // Check if the user agent has changed.
4670 if (userAgentChanged) {
4671 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
4672 nestedScrollWebView.loadUrl(url, customHeaders);
4674 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4677 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4680 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
4681 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4682 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4684 // Parse the url and set it as the data for the intent.
4685 emailIntent.setData(Uri.parse(url));
4687 // Open the email program in a new task instead of as part of Privacy Browser.
4688 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4691 startActivity(emailIntent);
4693 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4695 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
4696 // Open the dialer and load the phone number, but wait for the user to place the call.
4697 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4699 // Add the phone number to the intent.
4700 dialIntent.setData(Uri.parse(url));
4702 // Open the dialer in a new task instead of as part of Privacy Browser.
4703 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4706 startActivity(dialIntent);
4708 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4710 } else { // Load a system chooser to select an app that can handle the URL.
4711 // Open an app that can handle the URL.
4712 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4714 // Add the URL to the intent.
4715 genericIntent.setData(Uri.parse(url));
4717 // List all apps that can handle the URL instead of just opening the first one.
4718 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4720 // Open the app in a new task instead of as part of Privacy Browser.
4721 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4723 // Start the app or display a snackbar if no app is available to handle the URL.
4725 startActivity(genericIntent);
4726 } catch (ActivityNotFoundException exception) {
4727 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
4730 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4735 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4736 @SuppressWarnings("deprecation")
4738 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4739 // Get a handle for the navigation view.
4740 NavigationView navigationView = findViewById(R.id.navigationview);
4742 // Get a handle for the navigation menu.
4743 Menu navigationMenu = navigationView.getMenu();
4745 // Get a handle for the navigation requests menu item. The menu is 0 based.
4746 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
4748 // Create an empty web resource response to be used if the resource request is blocked.
4749 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4751 // Reset the whitelist results tracker.
4752 String[] whitelistResultStringArray = null;
4754 // Initialize the third party request tracker.
4755 boolean isThirdPartyRequest = false;
4757 // Initialize the current domain string.
4758 String currentDomain = "";
4760 // Nobody is happy when comparing null strings.
4761 if (!(formattedUrlString == null) && !(url == null)) {
4762 // Get the domain strings to URIs.
4763 Uri currentDomainUri = Uri.parse(formattedUrlString);
4764 Uri requestDomainUri = Uri.parse(url);
4766 // Get the domain host names.
4767 String currentBaseDomain = currentDomainUri.getHost();
4768 String requestBaseDomain = requestDomainUri.getHost();
4770 // Update the current domain variable.
4771 currentDomain = currentBaseDomain;
4773 // Only compare the current base domain and the request base domain if neither is null.
4774 if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
4775 // Determine the current base domain.
4776 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4777 // Remove the first subdomain.
4778 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4781 // Determine the request base domain.
4782 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4783 // Remove the first subdomain.
4784 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4787 // Update the third party request tracker.
4788 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4792 // Get the current WebView page position.
4793 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4795 // Determine if the WebView is currently displayed.
4796 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
4798 // Block third-party requests if enabled.
4799 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
4800 // Add the result to the resource requests.
4801 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_THIRD_PARTY, url});
4803 // Increment the blocked requests counters.
4804 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4805 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
4807 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4808 if (webViewDisplayed) {
4809 // Updating the UI must be run from the UI thread.
4810 activity.runOnUiThread(() -> {
4811 // Update the menu item titles.
4812 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4813 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4814 blockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
4815 getString(R.string.block_all_third_party_requests));
4819 // Return an empty web resource response.
4820 return emptyWebResourceResponse;
4823 // Check UltraPrivacy if it is enabled.
4824 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)) {
4825 // Check the URL against UltraPrivacy.
4826 String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
4828 // Process the UltraPrivacy results.
4829 if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
4830 // Add the result to the resource requests.
4831 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4832 ultraPrivacyResults[5]});
4834 // Increment the blocked requests counters.
4835 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4836 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY);
4838 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4839 if (webViewDisplayed) {
4840 // Updating the UI must be run from the UI thread.
4841 activity.runOnUiThread(() -> {
4842 // Update the menu item titles.
4843 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4844 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4845 ultraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
4849 // The resource request was blocked. Return an empty web resource response.
4850 return emptyWebResourceResponse;
4851 } else if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
4852 // Add a whitelist entry to the resource requests array.
4853 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4854 ultraPrivacyResults[5]});
4856 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
4861 // Check EasyList if it is enabled.
4862 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)) {
4863 // Check the URL against EasyList.
4864 String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
4866 // Process the EasyList results.
4867 if (easyListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
4868 // Add the result to the resource requests.
4869 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
4871 // Increment the blocked requests counters.
4872 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4873 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_LIST);
4875 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4876 if (webViewDisplayed) {
4877 // Updating the UI must be run from the UI thread.
4878 activity.runOnUiThread(() -> {
4879 // Update the menu item titles.
4880 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4881 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4882 easyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
4886 // The resource request was blocked. Return an empty web resource response.
4887 return emptyWebResourceResponse;
4888 } else if (easyListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
4889 // Update the whitelist result string array tracker.
4890 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
4894 // Check EasyPrivacy if it is enabled.
4895 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)) {
4896 // Check the URL against EasyPrivacy.
4897 String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
4899 // Process the EasyPrivacy results.
4900 if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
4901 // Add the result to the resource requests.
4902 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
4903 easyPrivacyResults[5]});
4905 // Increment the blocked requests counters.
4906 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4907 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY);
4909 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4910 if (webViewDisplayed) {
4911 // Updating the UI must be run from the UI thread.
4912 activity.runOnUiThread(() -> {
4913 // Update the menu item titles.
4914 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4915 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4916 easyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
4920 // The resource request was blocked. Return an empty web resource response.
4921 return emptyWebResourceResponse;
4922 } else if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
4923 // Update the whitelist result string array tracker.
4924 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
4928 // Check Fanboy’s Annoyance List if it is enabled.
4929 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
4930 // Check the URL against Fanboy's Annoyance List.
4931 String[] fanboysAnnoyanceListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
4933 // Process the Fanboy's Annoyance List results.
4934 if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
4935 // Add the result to the resource requests.
4936 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4937 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
4939 // Increment the blocked requests counters.
4940 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4941 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
4943 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4944 if (webViewDisplayed) {
4945 // Updating the UI must be run from the UI thread.
4946 activity.runOnUiThread(() -> {
4947 // Update the menu item titles.
4948 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4949 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4950 fanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
4951 getString(R.string.fanboys_annoyance_list));
4955 // The resource request was blocked. Return an empty web resource response.
4956 return emptyWebResourceResponse;
4957 } else if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
4958 // Update the whitelist result string array tracker.
4959 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4960 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
4962 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
4963 // Check the URL against Fanboy's Annoyance List.
4964 String[] fanboysSocialListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
4966 // Process the Fanboy's Social Blocking List results.
4967 if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
4968 // Add the result to the resource requests.
4969 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
4970 fanboysSocialListResults[4], fanboysSocialListResults[5]});
4972 // Increment the blocked requests counters.
4973 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4974 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
4976 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4977 if (webViewDisplayed) {
4978 // Updating the UI must be run from the UI thread.
4979 activity.runOnUiThread(() -> {
4980 // Update the menu item titles.
4981 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4982 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4983 fanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
4984 getString(R.string.fanboys_social_blocking_list));
4988 // The resource request was blocked. Return an empty web resource response.
4989 return emptyWebResourceResponse;
4990 } else if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
4991 // Update the whitelist result string array tracker.
4992 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
4993 fanboysSocialListResults[4], fanboysSocialListResults[5]};
4997 // Add the request to the log because it hasn't been processed by any of the previous checks.
4998 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
4999 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5000 } else { // The request didn't match any blocklist entry. Log it as a default request.
5001 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_DEFAULT, url});
5004 // The resource request has not been blocked. `return null` loads the requested resource.
5008 // Handle HTTP authentication requests.
5010 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5011 // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`.
5012 httpAuthHandler = handler;
5014 // Display the HTTP authentication dialog.
5015 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
5016 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5020 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5021 // Reset the list of resource requests.
5022 nestedScrollWebView.clearResourceRequests();
5024 // Reset the requests counters.
5025 nestedScrollWebView.resetRequestsCounters();
5027 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5029 nestedScrollWebView.setVisibility(View.INVISIBLE);
5032 // Hide the keyboard.
5033 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5035 // Check to see if Privacy Browser is waiting on Orbot.
5036 if (!waitingForOrbot) { // Process the URL.
5037 // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
5038 formattedUrlString = url;
5040 // Display the formatted URL text.
5041 urlEditText.setText(formattedUrlString);
5043 // Apply text highlighting to `urlTextBox`.
5046 // Get a URI for the current URL.
5047 Uri currentUri = Uri.parse(formattedUrlString);
5049 // Reset the list of host IP addresses.
5050 nestedScrollWebView.clearCurrentIpAddresses();
5052 // Get the IP addresses for the host.
5053 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5055 // Apply any custom domain settings if the URL was loaded by navigating history.
5056 if (navigatingHistory) {
5057 // Apply the domain settings.
5058 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5060 // Reset `navigatingHistory`.
5061 navigatingHistory = false;
5063 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5064 if (userAgentChanged) {
5065 loadUrl(formattedUrlString);
5069 // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.)
5070 if (refreshMenuItem != null) {
5072 refreshMenuItem.setTitle(R.string.stop);
5074 // If the icon is displayed in the AppBar, set it according to the theme.
5075 if (displayAdditionalAppBarIcons) {
5077 refreshMenuItem.setIcon(R.drawable.close_dark);
5079 refreshMenuItem.setIcon(R.drawable.close_light);
5086 // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
5088 public void onPageFinished(WebView view, String url) {
5089 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5090 if (!waitingForOrbot) {
5091 // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5092 nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5095 // Flush any cookies to persistent storage. `CookieManager` has become very lazy about flushing cookies in recent versions.
5096 if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) {
5097 CookieManager.getInstance().flush();
5100 // Update the Refresh menu item if it has been created.
5101 if (refreshMenuItem != null) {
5102 // Reset the Refresh title.
5103 refreshMenuItem.setTitle(R.string.refresh);
5105 // If the icon is displayed in the AppBar, reset it according to the theme.
5106 if (displayAdditionalAppBarIcons) {
5108 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5110 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5116 // Clear the cache and history if Incognito Mode is enabled.
5117 if (incognitoModeEnabled) {
5118 // Clear the cache. `true` includes disk files.
5119 nestedScrollWebView.clearCache(true);
5121 // Clear the back/forward history.
5122 nestedScrollWebView.clearHistory();
5124 // Manually delete cache folders.
5126 // Delete the main cache directory.
5127 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5129 // Delete the secondary `Service Worker` cache directory.
5130 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5131 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5132 } catch (IOException e) {
5133 // Do nothing if an error is thrown.
5137 // Update the URL text box and apply domain settings if not waiting on Orbot.
5138 if (!waitingForOrbot) {
5139 // Check to see if `WebView` has set `url` to be `about:blank`.
5140 if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
5141 // Set `formattedUrlString` to `""`.
5142 formattedUrlString = "";
5144 urlEditText.setText(formattedUrlString);
5146 // Request focus for `urlTextBox`.
5147 urlEditText.requestFocus();
5149 // Display the keyboard.
5150 inputMethodManager.showSoftInput(urlEditText, 0);
5152 // Apply the domain settings. This clears any settings from the previous domain.
5153 applyDomainSettings(nestedScrollWebView, formattedUrlString, true, false);
5154 } else { // `WebView` has loaded a webpage.
5155 // Set the formatted URL string. Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly.
5156 formattedUrlString = nestedScrollWebView.getUrl();
5158 // Only update the URL text box if the user is not typing in it.
5159 if (!urlEditText.hasFocus()) {
5160 // Display the formatted URL text.
5161 urlEditText.setText(formattedUrlString);
5163 // Apply text highlighting to `urlTextBox`.
5168 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5169 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5170 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5171 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5176 // Handle SSL Certificate errors.
5178 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5179 // Get the current website SSL certificate.
5180 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5182 // Extract the individual pieces of information from the current website SSL certificate.
5183 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5184 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5185 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5186 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5187 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5188 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5189 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5190 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5192 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5193 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5194 // Get the pinned SSL certificate.
5195 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5197 // Extract the arrays from the array list.
5198 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5199 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5201 // Check if the current SSL certificate matches the pinned certificate.
5202 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5203 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5204 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5205 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5207 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5210 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5211 // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
5212 sslErrorHandler = handler; // TODO. We need to pass this in instead of using a static variable. Because multiple could be displayed at once from different tabs.
5214 // Display the SSL error `AlertDialog`.
5215 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
5216 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5221 // Check to see if this is the first page.
5222 if (pageNumber == 0) {
5223 // Set this nested scroll WebView as the current WebView.
5224 currentWebView = nestedScrollWebView;
5226 // Apply the app settings from the shared preferences.
5229 // Load the website if not waiting for Orbot to connect.
5230 if (!waitingForOrbot) {
5231 loadUrl(formattedUrlString);