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 // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`.
226 private String homepage; // TODO ?
228 // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`.
229 private String searchURL; // TODO ?
231 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
232 private Menu optionsMenu;
234 // TODO. This could probably be removed.
235 // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
236 private BlockListHelper blockListHelper;
238 // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
239 private ArrayList<List<String[]>> easyList;
240 private ArrayList<List<String[]>> easyPrivacy;
241 private ArrayList<List<String[]>> fanboysAnnoyanceList;
242 private ArrayList<List<String[]>> fanboysSocialList;
243 private ArrayList<List<String[]>> ultraPrivacy;
245 // The blocklist menu items are used in `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, and `initializeWebView()`. // TODO.
246 private MenuItem blocklistsMenuItem;
247 private MenuItem easyListMenuItem;
248 private MenuItem easyPrivacyMenuItem;
249 private MenuItem fanboysAnnoyanceListMenuItem;
250 private MenuItem fanboysSocialBlockingListMenuItem;
251 private MenuItem ultraPrivacyMenuItem;
252 private MenuItem blockAllThirdPartyRequestsMenuItem;
254 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
255 private String webViewDefaultUserAgent;
257 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
258 private boolean proxyThroughOrbot;
260 // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
261 private boolean incognitoModeEnabled; // TODO.
263 // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
264 private boolean fullScreenBrowsingModeEnabled; // TODO.
266 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
267 private boolean inFullScreenBrowsingMode;
269 // Hide app bar is used in `onCreate()` and `applyAppSettings()`.
270 private boolean hideAppBar; // TODO.
272 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
273 private boolean reapplyDomainSettingsOnRestart;
275 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
276 private boolean reapplyAppSettingsOnRestart;
278 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
279 private boolean displayingFullScreenVideo;
281 // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`.
282 private boolean downloadWithExternalApp; // TODO.
284 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
285 private BroadcastReceiver orbotStatusBroadcastReceiver;
287 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
288 private boolean waitingForOrbot;
290 // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`.
291 private Boolean domainSettingsJavaScriptEnabled; // TODO.
293 // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`.
294 private String waitingForOrbotHtmlString; // TODO.
296 // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`.
297 private String privateDataDirectoryString; // TODO.
299 // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`.
300 private boolean displayAdditionalAppBarIcons; // TODO.
302 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
303 private ActionBarDrawerToggle actionBarDrawerToggle; // TODO.
305 // The color spans are used in `onCreate()` and `highlightUrlText()`.
306 private ForegroundColorSpan redColorSpan;
307 private ForegroundColorSpan initialGrayColorSpan;
308 private ForegroundColorSpan finalGrayColorSpan;
310 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
311 private int drawerHeaderPaddingLeftAndRight;
312 private int drawerHeaderPaddingTop;
313 private int drawerHeaderPaddingBottom;
315 // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
316 private SslErrorHandler sslErrorHandler; // TODO.
318 // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`.
319 private static HttpAuthHandler httpAuthHandler; // TODO.
321 // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
322 private InputMethodManager inputMethodManager; // TODO.
324 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
325 // and `loadBookmarksFolder()`.
326 private BookmarksDatabaseHelper bookmarksDatabaseHelper; // TODO.
328 // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
329 private ListView bookmarksListView; // TODO.
331 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
332 private Cursor bookmarksCursor;
334 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
335 private CursorAdapter bookmarksCursorAdapter;
337 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
338 private String oldFolderNameString;
340 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
341 private ValueCallback<Uri[]> fileChooserCallback;
343 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
344 private String downloadUrl;
345 private String downloadContentDisposition;
346 private long downloadContentLength;
348 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
349 private String downloadImageUrl;
351 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
352 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
353 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
356 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
357 @SuppressLint("ClickableViewAccessibility")
358 protected void onCreate(Bundle savedInstanceState) {
359 // Get a handle for the shared preferences.
360 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
362 // Get the theme and screenshot preferences.
363 darkTheme = sharedPreferences.getBoolean("dark_theme", false);
364 allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
366 // Disable screenshots if not allowed.
367 if (!allowScreenshots) {
368 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
371 // Set the activity theme.
373 setTheme(R.style.PrivacyBrowserDark);
375 setTheme(R.style.PrivacyBrowserLight);
378 // Run the default commands.
379 super.onCreate(savedInstanceState);
381 // Set the content view.
382 setContentView(R.layout.main_framelayout);
384 // Get handles for the input method manager and toolbar.
385 inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
386 Toolbar toolbar = findViewById(R.id.toolbar);
388 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
389 setSupportActionBar(toolbar);
390 ActionBar actionBar = getSupportActionBar();
392 // This is needed to get rid of the Android Studio warning that the action bar might be null.
393 assert actionBar != null;
395 // Add the custom layout, which shows the URL text bar.
396 actionBar.setCustomView(R.layout.url_app_bar);
397 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
399 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
400 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
401 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
402 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
404 // Get handles for the URL views.
405 EditText urlEditText = findViewById(R.id.url_edittext);
407 // Remove the formatting from `urlTextBar` when the user is editing the text.
408 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
409 if (hasFocus) { // The user is editing the URL text box.
410 // Remove the highlighting.
411 urlEditText.getText().removeSpan(redColorSpan);
412 urlEditText.getText().removeSpan(initialGrayColorSpan);
413 urlEditText.getText().removeSpan(finalGrayColorSpan);
414 } else { // The user has stopped editing the URL text box.
415 // Move to the beginning of the string.
416 urlEditText.setSelection(0);
418 // Reapply the highlighting.
423 // Set the go button on the keyboard to load the URL in `urlTextBox`.
424 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
425 // If the event is a key-down event on the `enter` button, load the URL.
426 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
427 // Load the URL into the mainWebView and consume the event.
428 loadUrlFromTextBox();
430 // If the enter key was pressed, consume the event.
433 // If any other key was pressed, do not consume the event.
438 // Set `waitingForOrbotHTMLString`.
439 waitingForOrbotHtmlString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
441 // Initialize the Orbot status and the waiting for Orbot trackers.
442 orbotStatus = "unknown";
443 waitingForOrbot = false;
445 // Create an Orbot status `BroadcastReceiver`.
446 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
448 public void onReceive(Context context, Intent intent) {
449 // Store the content of the status message in `orbotStatus`.
450 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
452 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
453 if (orbotStatus.equals("ON") && waitingForOrbot) {
454 // Reset `waitingForOrbot`.
455 waitingForOrbot = false;
457 // Load `formattedUrlString
458 loadUrl(formattedUrlString);
463 // Register `orbotStatusBroadcastReceiver` on `this` context.
464 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
466 // Instantiate the block list helper.
467 blockListHelper = new BlockListHelper();
469 // Parse the block lists.
470 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
471 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
472 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
473 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
474 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
476 // Store the list versions.
477 easyListVersion = easyList.get(0).get(0)[0];
478 easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
479 fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
480 fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
481 ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
483 // Get handles for views that need to be modified.
484 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
485 NavigationView navigationView = findViewById(R.id.navigationview);
486 TabLayout tabLayout = findViewById(R.id.tablayout);
487 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
488 ViewPager webViewPager = findViewById(R.id.webviewpager);
489 bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
490 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
491 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
492 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
493 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
495 // Listen for touches on the navigation menu.
496 navigationView.setNavigationItemSelectedListener(this);
498 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
499 Menu navigationMenu = navigationView.getMenu();
500 MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
501 MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
502 MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
503 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
504 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
506 // Initialize the web view pager adapter.
507 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
509 // Set the pager adapter on the web view pager.
510 webViewPager.setAdapter(webViewPagerAdapter);
512 // Store up to 100 tabs in memory.
513 webViewPager.setOffscreenPageLimit(100);
515 // Update the web view pager every time a tab is modified.
516 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
518 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
523 public void onPageSelected(int position) {
524 // Set the current WebView.
525 setCurrentWebView(position);
527 // 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.
528 if (tabLayout.getSelectedTabPosition() != position) {
529 // Get a handle for the corresponding tab.
530 TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
532 // Assert that the corresponding tab is not null.
533 assert correspondingTab != null;
535 // Select the corresponding tab.
536 correspondingTab.select();
541 public void onPageScrollStateChanged(int state) {
546 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
547 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
549 public void onTabSelected(TabLayout.Tab tab) {
550 // Select the same page in the view pager.
551 webViewPager.setCurrentItem(tab.getPosition());
555 public void onTabUnselected(TabLayout.Tab tab) {
560 public void onTabReselected(TabLayout.Tab tab) {
561 // Instantiate the View SSL Certificate dialog.
562 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteOrDefaultIcon());
564 // Display the View SSL Certificate dialog.
565 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
569 // Add the first tab.
572 // 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.
573 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
575 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
576 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
577 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
578 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
580 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
581 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
582 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
583 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
586 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
587 launchBookmarksActivityFab.setOnClickListener(v -> {
588 // Store the current WebView url and title in the bookmarks activity. // TODO.
589 BookmarksActivity.currentWebViewUrl = currentWebView.getUrl();
590 BookmarksActivity.currentWebViewTitle = currentWebView.getTitle();
592 // Get a copy of the favorite icon bitmap.
593 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
595 // Create a favorite icon byte array output stream.
596 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
598 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
599 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
601 // Convert the favorite icon byte array stream to a byte array.
602 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
604 // Create an intent to launch the bookmarks activity.
605 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
607 // Add the extra information to the intent.
608 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
609 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
612 startActivity(bookmarksIntent);
615 // Set the create new bookmark folder FAB to display an alert dialog.
616 createBookmarkFolderFab.setOnClickListener(v -> {
617 // Create a create bookmark folder dialog.
618 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
620 // Show the create bookmark folder dialog.
621 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
624 // Set the create new bookmark FAB to display an alert dialog.
625 createBookmarkFab.setOnClickListener(view -> {
626 // Instantiate the create bookmark dialog.
627 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
629 // Display the create bookmark dialog.
630 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
633 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
634 findOnPageEditText.addTextChangedListener(new TextWatcher() {
636 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
641 public void onTextChanged(CharSequence s, int start, int before, int count) {
646 public void afterTextChanged(Editable s) {
647 // Search for the text in `mainWebView`.
648 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
652 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
653 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
654 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
655 // Hide the soft keyboard.
656 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
658 // Consume the event.
660 } else { // A different key was pressed.
661 // Do not consume the event.
666 // Implement swipe to refresh.
667 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
669 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
670 swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
672 // Set the swipe to refresh color according to the theme.
674 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
675 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
677 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
680 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
681 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
682 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
684 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
685 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
687 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
688 currentBookmarksFolder = "";
690 // Load the home folder, which is `""` in the database.
691 loadBookmarksFolder();
693 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
694 // Convert the id from long to int to match the format of the bookmarks database.
695 int databaseID = (int) id;
697 // Get the bookmark cursor for this ID and move it to the first row.
698 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
699 bookmarkCursor.moveToFirst();
701 // Act upon the bookmark according to the type.
702 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
703 // Store the new folder name in `currentBookmarksFolder`.
704 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
706 // Load the new folder.
707 loadBookmarksFolder();
708 } else { // The selected bookmark is not a folder.
709 // Load the bookmark URL.
710 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
712 // Close the bookmarks drawer.
713 drawerLayout.closeDrawer(GravityCompat.END);
716 // Close the `Cursor`.
717 bookmarkCursor.close();
720 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
721 // Convert the database ID from `long` to `int`.
722 int databaseId = (int) id;
724 // Find out if the selected bookmark is a folder.
725 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
728 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
729 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
731 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
732 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
733 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
735 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
736 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
737 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
740 // Consume the event.
744 // Get the status bar pixel size.
745 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
746 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
748 // Get the resource density.
749 float screenDensity = getResources().getDisplayMetrics().density;
751 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
752 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
753 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
754 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
756 // The drawer listener is used to update the navigation menu.`
757 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
759 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
763 public void onDrawerOpened(@NonNull View drawerView) {
767 public void onDrawerClosed(@NonNull View drawerView) {
771 public void onDrawerStateChanged(int newState) {
772 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
773 // Get handles for the drawer headers.
774 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
775 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
777 // 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.
778 if (navigationHeaderTextView != null) {
779 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
782 // 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.
783 if (bookmarksHeaderTextView != null) {
784 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
787 // Update the navigation menu items.
788 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
789 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
790 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
791 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
792 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
794 // Hide the keyboard (if displayed).
795 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
797 // 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.
798 urlEditText.clearFocus();
799 currentWebView.clearFocus();
804 // Create the hamburger icon at the start of the AppBar.
805 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
807 // 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).
808 customHeaders.put("X-Requested-With", "");
810 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
811 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
813 // Store the application's private data directory.
814 privateDataDirectoryString = getApplicationInfo().dataDir;
815 // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
817 // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
818 inFullScreenBrowsingMode = false;
820 // Initialize the privacy settings variables.
821 firstPartyCookiesEnabled = false;
823 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
824 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
826 // Get a handle for the WebView.
827 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
829 // Store the default user agent.
830 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
832 // Destroy the bare WebView.
833 bareWebView.destroy();
835 // Get the intent that started the app.
836 Intent launchingIntent = getIntent();
838 // Get the information from the intent.
839 String launchingIntentAction = launchingIntent.getAction();
840 Uri launchingIntentUriData = launchingIntent.getData();
842 // If the intent action is a web search, perform the search.
843 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
844 // Create an encoded URL string.
845 String encodedUrlString;
847 // Sanitize the search input and convert it to a search.
849 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
850 } catch (UnsupportedEncodingException exception) {
851 encodedUrlString = "";
854 // Add the base search URL.
855 formattedUrlString = searchURL + encodedUrlString;
856 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
857 // Set the formatted URL string.
858 formattedUrlString = launchingIntentUriData.toString();
863 protected void onNewIntent(Intent intent) {
864 // Get the information from the intent.
865 String intentAction = intent.getAction();
866 Uri intentUriData = intent.getData();
868 // 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.
869 if (intentUriData != null) {
870 // Sets the new intent as the activity intent, which replaces the one that originally started the app.
876 // If the intent action is a web search, perform the search.
877 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
878 // Create an encoded URL string.
879 String encodedUrlString;
881 // Sanitize the search input and convert it to a search.
883 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
884 } catch (UnsupportedEncodingException exception) {
885 encodedUrlString = "";
888 // Add the base search URL.
889 formattedUrlString = searchURL + encodedUrlString;
890 } else { // The intent should contain a URL.
891 // Set the formatted URL string.
892 formattedUrlString = intentUriData.toString();
896 loadUrl(formattedUrlString);
898 // Get a handle for the drawer layout.
899 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
901 // Close the navigation drawer if it is open.
902 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
903 drawerLayout.closeDrawer(GravityCompat.START);
906 // Close the bookmarks drawer if it is open.
907 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
908 drawerLayout.closeDrawer(GravityCompat.END);
911 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
912 currentWebView.requestFocus();
917 public void onRestart() {
918 // Run the default commands.
921 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
922 if (proxyThroughOrbot) {
923 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
924 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
926 // Send the intent to the Orbot package.
927 orbotIntent.setPackage("org.torproject.android");
930 sendBroadcast(orbotIntent);
933 // Apply the app settings if returning from the Settings activity.
934 if (reapplyAppSettingsOnRestart) {
935 // Apply the app settings.
938 // Reload the webpage if displaying of images has been disabled in the Settings activity.
939 if (reloadOnRestart) {
940 // Reload the WebViews.
941 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
942 // Get the WebView tab fragment.
943 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
945 // Get the fragment view.
946 View fragmentView = webViewTabFragment.getView();
948 // Only reload the WebViews if they exist.
949 if (fragmentView != null) {
950 // Get the nested scroll WebView from the tab fragment.
951 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
953 // Reload the WebView. This doesn't seem to work if for WebViews that aren't visible.
954 nestedScrollWebView.reload();
958 // Reset `reloadOnRestartBoolean`.
959 reloadOnRestart = false;
962 // Reset the return from settings flag.
963 reapplyAppSettingsOnRestart = false;
966 // TODO apply to all the tabs.
967 // Apply the domain settings if returning from the Domains activity.
968 if (reapplyDomainSettingsOnRestart) {
969 // Reapply the domain settings.
970 applyDomainSettings(currentWebView, formattedUrlString, false, true);
972 // Reset `reapplyDomainSettingsOnRestart`.
973 reapplyDomainSettingsOnRestart = false;
976 // Load the URL on restart to apply changes to night mode.
977 if (loadUrlOnRestart) {
978 // Load the current `formattedUrlString`.
979 loadUrl(formattedUrlString);
981 // Reset `loadUrlOnRestart.
982 loadUrlOnRestart = false;
985 // Update the bookmarks drawer if returning from the Bookmarks activity.
986 if (restartFromBookmarksActivity) {
987 // Get a handle for the drawer layout.
988 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
990 // Close the bookmarks drawer.
991 drawerLayout.closeDrawer(GravityCompat.END);
993 // Reload the bookmarks drawer.
994 loadBookmarksFolder();
996 // Reset `restartFromBookmarksActivity`.
997 restartFromBookmarksActivity = false;
1000 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
1001 updatePrivacyIcons(true);
1004 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
1006 public void onResume() {
1007 // Run the default commands.
1010 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1011 // Get the WebView tab fragment.
1012 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1014 // Get the fragment view.
1015 View fragmentView = webViewTabFragment.getView();
1017 // Only resume the WebViews if they exist (they won't when the app is first created).
1018 if (fragmentView != null) {
1019 // Get the nested scroll WebView from the tab fragment.
1020 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1022 // Resume the nested scroll WebView JavaScript timers.
1023 nestedScrollWebView.resumeTimers();
1025 // Resume the nested scroll WebView.
1026 nestedScrollWebView.onResume();
1030 // Display a message to the user if waiting for Orbot.
1031 if (waitingForOrbot && !orbotStatus.equals("ON")) {
1032 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
1033 currentWebView.getSettings().setUseWideViewPort(false);
1035 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
1036 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
1039 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
1040 // Get a handle for the root frame layouts.
1041 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1043 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1044 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1046 /* Hide the system bars.
1047 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1048 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1049 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1050 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1052 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1053 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1054 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
1056 AdHelper.resumeAd(findViewById(R.id.adview));
1061 public void onPause() {
1062 // Run the default commands.
1065 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1066 // Get the WebView tab fragment.
1067 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1069 // Get the fragment view.
1070 View fragmentView = webViewTabFragment.getView();
1072 // Only pause the WebViews if they exist (they won't when the app is first created).
1073 if (fragmentView != null) {
1074 // Get the nested scroll WebView from the tab fragment.
1075 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1077 // Pause the nested scroll WebView.
1078 nestedScrollWebView.onPause();
1080 // Pause the nested scroll WebView JavaScript timers.
1081 nestedScrollWebView.pauseTimers();
1085 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1086 if (BuildConfig.FLAVOR.contentEquals("free")) {
1088 AdHelper.pauseAd(findViewById(R.id.adview));
1093 public void onDestroy() {
1094 // Unregister the Orbot status broadcast receiver.
1095 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1097 // Close the bookmarks cursor and database.
1098 bookmarksCursor.close();
1099 bookmarksDatabaseHelper.close();
1101 // Run the default commands.
1106 public boolean onCreateOptionsMenu(Menu menu) {
1107 // Inflate the menu. This adds items to the action bar if it is present.
1108 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1110 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
1113 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1114 updatePrivacyIcons(false);
1116 // Get handles for the menu items.
1117 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1118 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1119 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1120 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1121 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1122 MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
1123 blocklistsMenuItem = menu.findItem(R.id.blocklists);
1124 easyListMenuItem = menu.findItem(R.id.easylist);
1125 easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1126 fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1127 fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1128 ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1129 blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1130 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1132 // Only display third-party cookies if API >= 21
1133 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1135 // Only display the form data menu items if the API < 26.
1136 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1137 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1139 // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
1140 clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
1142 // Only show Ad Consent if this is the free flavor.
1143 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1145 // Get the shared preference values.
1146 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1148 // Get the status of the additional AppBar icons.
1149 displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1151 // 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.
1152 if (displayAdditionalAppBarIcons) {
1153 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1154 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1155 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1156 } else { //Do not display the additional icons.
1157 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1158 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1159 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1162 // Replace Refresh with Stop if a URL is already loading.
1163 if (currentWebView != null && currentWebView.getProgress() != 100) {
1165 refreshMenuItem.setTitle(R.string.stop);
1167 // If the icon is displayed in the AppBar, set it according to the theme.
1168 if (displayAdditionalAppBarIcons) {
1170 refreshMenuItem.setIcon(R.drawable.close_dark);
1172 refreshMenuItem.setIcon(R.drawable.close_light);
1181 public boolean onPrepareOptionsMenu(Menu menu) {
1182 // Get handles for the menu items.
1183 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1184 MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1185 MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1186 MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1187 MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1188 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1189 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1190 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1191 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1192 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1193 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1194 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1195 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1196 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1198 // Get a handle for the cookie manager.
1199 CookieManager cookieManager = CookieManager.getInstance();
1201 // Initialize the current user agent string and the font size.
1202 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1205 // 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.
1206 if (currentWebView != null) {
1207 // Set the add or edit domain text.
1208 if (currentWebView.getDomainSettingsApplied()) {
1209 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1211 addOrEditDomain.setTitle(R.string.add_domain_settings);
1214 // Get the current user agent from the WebView.
1215 currentUserAgent = currentWebView.getSettings().getUserAgentString();
1217 // Get the current font size from the
1218 fontSize = currentWebView.getSettings().getTextZoom();
1220 // Set the status of the menu item checkboxes.
1221 domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1222 saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26.
1223 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1224 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1225 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1226 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1227 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1228 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1229 swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1230 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1231 nightModeMenuItem.setChecked(currentWebView.getNightMode());
1233 // Initialize the display names for the blocklists with the number of blocked requests.
1234 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1235 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1236 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1237 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1238 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1239 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1240 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1243 // Set the status of the menu item checkboxes.
1244 firstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
1245 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1247 // Only modify third-party cookies if the API >= 21.
1248 if (Build.VERSION.SDK_INT >= 21) {
1249 // Set the status of the third-party cookies checkbox.
1250 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1252 // Enable third-party cookies if first-party cookies are enabled.
1253 thirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
1256 // Enable DOM Storage if JavaScript is enabled.
1257 domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1259 // Enable Clear Cookies if there are any.
1260 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1262 // Get a count of the number of files in the Local Storage directory.
1263 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1264 int localStorageDirectoryNumberOfFiles = 0;
1265 if (localStorageDirectory.exists()) {
1266 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1269 // Get a count of the number of files in the IndexedDB directory.
1270 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1271 int indexedDBDirectoryNumberOfFiles = 0;
1272 if (indexedDBDirectory.exists()) {
1273 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1276 // Enable Clear DOM Storage if there is any.
1277 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1279 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1280 if (Build.VERSION.SDK_INT < 26) {
1281 // Get the WebView database.
1282 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1284 // Enable the clear form data menu item if there is anything to clear.
1285 clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1288 // Enable Clear Data if any of the submenu items are enabled.
1289 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1291 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1292 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1294 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1295 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1296 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1297 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1298 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1299 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1300 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1301 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1302 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1303 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1304 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1305 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1306 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1307 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1308 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1309 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1310 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1311 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1312 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1313 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1314 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1315 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1316 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1317 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1318 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1319 } else { // Custom user agent.
1320 menu.findItem(R.id.user_agent_custom).setChecked(true);
1323 // Instantiate the font size title and the selected font size menu item.
1324 String fontSizeTitle;
1325 MenuItem selectedFontSizeMenuItem;
1327 // Prepare the font size title and current size menu item.
1330 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1331 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1335 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1336 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1340 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1341 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1345 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1346 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1350 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1351 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1355 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1356 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1360 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1361 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1365 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1366 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1370 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1371 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1375 // Set the font size title and select the current size menu item.
1376 fontSizeMenuItem.setTitle(fontSizeTitle);
1377 selectedFontSizeMenuItem.setChecked(true);
1379 // Run all the other default commands.
1380 super.onPrepareOptionsMenu(menu);
1382 // Display the menu.
1387 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1388 @SuppressLint("SetJavaScriptEnabled")
1389 // removeAllCookies is deprecated, but it is required for API < 21.
1390 @SuppressWarnings("deprecation")
1391 public boolean onOptionsItemSelected(MenuItem menuItem) {
1392 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1393 if (inFullScreenBrowsingMode) {
1394 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1395 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1397 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1399 /* Hide the system bars.
1400 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1401 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1402 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1403 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1405 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1406 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1409 // Get the selected menu item ID.
1410 int menuItemId = menuItem.getItemId();
1412 // Get a handle for the shared preferences.
1413 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1415 // Get a handle for the cookie manager.
1416 CookieManager cookieManager = CookieManager.getInstance();
1418 // Run the commands that correlate to the selected menu item.
1419 switch (menuItemId) {
1420 case R.id.toggle_javascript:
1421 // Toggle the JavaScript status.
1422 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1424 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1425 updatePrivacyIcons(true);
1427 // Display a `Snackbar`.
1428 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
1429 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1430 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled.
1431 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1432 } else { // Privacy mode.
1433 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1436 // Reload the current WebView.
1437 currentWebView.reload();
1440 case R.id.add_or_edit_domain:
1441 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1442 // Reapply the domain settings on returning to `MainWebViewActivity`.
1443 reapplyDomainSettingsOnRestart = true;
1444 currentWebView.resetCurrentDomainName();
1446 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
1447 // Store the current SSL certificate and IP addresses in the domains activity.
1448 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1449 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1451 // Create an intent to launch the domains activity.
1452 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1454 // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
1455 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1456 domainsIntent.putExtra("close_on_back", true);
1459 startActivity(domainsIntent);
1460 } else { // Add a new domain.
1461 // Apply the new domain settings on returning to `MainWebViewActivity`.
1462 reapplyDomainSettingsOnRestart = true;
1463 currentWebView.resetCurrentDomainName();
1465 // Get the current domain
1466 Uri currentUri = Uri.parse(formattedUrlString);
1467 String currentDomain = currentUri.getHost();
1469 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1470 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1472 // Create the domain and store the database ID.
1473 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1475 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
1476 // Store the current SSL certificate and IP addresses in the domains activity.
1477 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1478 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1480 // Create an intent to launch the domains activity.
1481 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1483 // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
1484 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1485 domainsIntent.putExtra("close_on_back", true);
1488 startActivity(domainsIntent);
1492 case R.id.toggle_first_party_cookies:
1493 // Switch the status of firstPartyCookiesEnabled.
1494 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
1496 // Update the menu checkbox.
1497 menuItem.setChecked(firstPartyCookiesEnabled);
1499 // Apply the new cookie status.
1500 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1502 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1503 updatePrivacyIcons(true);
1505 // Display a `Snackbar`.
1506 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
1507 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1508 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1509 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1510 } else { // Privacy mode.
1511 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1514 // Reload the current WebView.
1515 currentWebView.reload();
1518 case R.id.toggle_third_party_cookies:
1519 if (Build.VERSION.SDK_INT >= 21) {
1520 // Switch the status of thirdPartyCookiesEnabled.
1521 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1523 // Update the menu checkbox.
1524 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1526 // Display a snackbar.
1527 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1528 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1530 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1533 // Reload the current WebView.
1534 currentWebView.reload();
1535 } // Else do nothing because SDK < 21.
1538 case R.id.toggle_dom_storage:
1539 // Toggle the status of domStorageEnabled.
1540 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1542 // Update the menu checkbox.
1543 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1545 // Update the privacy icon. `true` refreshes the app bar icons.
1546 updatePrivacyIcons(true);
1548 // Display a snackbar.
1549 if (currentWebView.getSettings().getDomStorageEnabled()) {
1550 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1552 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1555 // Reload the current WebView.
1556 currentWebView.reload();
1559 // Form data can be removed once the minimum API >= 26.
1560 case R.id.toggle_save_form_data:
1561 // Switch the status of saveFormDataEnabled.
1562 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1564 // Update the menu checkbox.
1565 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1567 // Display a snackbar.
1568 if (currentWebView.getSettings().getSaveFormData()) {
1569 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1571 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1574 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1575 updatePrivacyIcons(true);
1577 // Reload the current WebView.
1578 currentWebView.reload();
1581 case R.id.clear_cookies:
1582 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1583 .setAction(R.string.undo, v -> {
1584 // Do nothing because everything will be handled by `onDismissed()` below.
1586 .addCallback(new Snackbar.Callback() {
1587 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1589 public void onDismissed(Snackbar snackbar, int event) {
1591 // The user pushed the undo button.
1592 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1596 // The snackbar was dismissed without the undo button being pushed.
1598 // `cookieManager.removeAllCookie()` varies by SDK.
1599 if (Build.VERSION.SDK_INT < 21) {
1600 cookieManager.removeAllCookie();
1602 cookieManager.removeAllCookies(null);
1610 case R.id.clear_dom_storage:
1611 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1612 .setAction(R.string.undo, v -> {
1613 // Do nothing because everything will be handled by `onDismissed()` below.
1615 .addCallback(new Snackbar.Callback() {
1616 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1618 public void onDismissed(Snackbar snackbar, int event) {
1620 // The user pushed the undo button.
1621 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1625 // The snackbar was dismissed without the undo button being pushed.
1627 // Delete the DOM Storage.
1628 WebStorage webStorage = WebStorage.getInstance();
1629 webStorage.deleteAllData();
1631 // Initialize a handler to manually delete the DOM storage files and directories.
1632 Handler deleteDomStorageHandler = new Handler();
1634 // Setup a runnable to manually delete the DOM storage files and directories.
1635 Runnable deleteDomStorageRunnable = () -> {
1637 // Get a handle for the runtime.
1638 Runtime runtime = Runtime.getRuntime();
1640 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1641 Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1643 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1644 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1645 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1646 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1647 Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1649 // Wait for the processes to finish.
1650 deleteLocalStorageProcess.waitFor();
1651 deleteIndexProcess.waitFor();
1652 deleteQuotaManagerProcess.waitFor();
1653 deleteQuotaManagerJournalProcess.waitFor();
1654 deleteDatabasesProcess.waitFor();
1655 } catch (Exception exception) {
1656 // Do nothing if an error is thrown.
1660 // Manually delete the DOM storage files after 200 milliseconds.
1661 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1668 // Form data can be remove once the minimum API >= 26.
1669 case R.id.clear_form_data:
1670 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1671 .setAction(R.string.undo, v -> {
1672 // Do nothing because everything will be handled by `onDismissed()` below.
1674 .addCallback(new Snackbar.Callback() {
1675 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1677 public void onDismissed(Snackbar snackbar, int event) {
1679 // The user pushed the undo button.
1680 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1684 // The snackbar was dismissed without the `Undo` button being pushed.
1686 // Delete the form data.
1687 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1688 mainWebViewDatabase.clearFormData();
1696 // Toggle the EasyList status.
1697 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1699 // Update the menu checkbox.
1700 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1702 // Reload the current WebView.
1703 currentWebView.reload();
1706 case R.id.easyprivacy:
1707 // Toggle the EasyPrivacy status.
1708 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1710 // Update the menu checkbox.
1711 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1713 // Reload the current WebView.
1714 currentWebView.reload();
1717 case R.id.fanboys_annoyance_list:
1718 // Toggle Fanboy's Annoyance List status.
1719 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1721 // Update the menu checkbox.
1722 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1724 // Update the staus of Fanboy's Social Blocking List.
1725 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1726 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1728 // Reload the current WebView.
1729 currentWebView.reload();
1732 case R.id.fanboys_social_blocking_list:
1733 // Toggle Fanboy's Social Blocking List status.
1734 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1736 // Update the menu checkbox.
1737 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1739 // Reload the current WebView.
1740 currentWebView.reload();
1743 case R.id.ultraprivacy:
1744 // Toggle the UltraPrivacy status.
1745 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1747 // Update the menu checkbox.
1748 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1750 // Reload the current WebView.
1751 currentWebView.reload();
1754 case R.id.block_all_third_party_requests:
1755 //Toggle the third-party requests blocker status.
1756 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1758 // Update the menu checkbox.
1759 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1761 // Reload the current WebView.
1762 currentWebView.reload();
1765 case R.id.user_agent_privacy_browser:
1766 // Update the user agent.
1767 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1769 // Reload the current WebView.
1770 currentWebView.reload();
1773 case R.id.user_agent_webview_default:
1774 // Update the user agent.
1775 currentWebView.getSettings().setUserAgentString("");
1777 // Reload the current WebView.
1778 currentWebView.reload();
1781 case R.id.user_agent_firefox_on_android:
1782 // Update the user agent.
1783 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1785 // Reload the current WebView.
1786 currentWebView.reload();
1789 case R.id.user_agent_chrome_on_android:
1790 // Update the user agent.
1791 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1793 // Reload the current WebView.
1794 currentWebView.reload();
1797 case R.id.user_agent_safari_on_ios:
1798 // Update the user agent.
1799 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1801 // Reload the current WebView.
1802 currentWebView.reload();
1805 case R.id.user_agent_firefox_on_linux:
1806 // Update the user agent.
1807 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1809 // Reload the current WebView.
1810 currentWebView.reload();
1813 case R.id.user_agent_chromium_on_linux:
1814 // Update the user agent.
1815 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1817 // Reload the current WebView.
1818 currentWebView.reload();
1821 case R.id.user_agent_firefox_on_windows:
1822 // Update the user agent.
1823 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1825 // Reload the current WebView.
1826 currentWebView.reload();
1829 case R.id.user_agent_chrome_on_windows:
1830 // Update the user agent.
1831 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1833 // Reload the current WebView.
1834 currentWebView.reload();
1837 case R.id.user_agent_edge_on_windows:
1838 // Update the user agent.
1839 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1841 // Reload the current WebView.
1842 currentWebView.reload();
1845 case R.id.user_agent_internet_explorer_on_windows:
1846 // Update the user agent.
1847 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1849 // Reload the current WebView.
1850 currentWebView.reload();
1853 case R.id.user_agent_safari_on_macos:
1854 // Update the user agent.
1855 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1857 // Reload the current WebView.
1858 currentWebView.reload();
1861 case R.id.user_agent_custom:
1862 // Update the user agent.
1863 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1865 // Reload the current WebView.
1866 currentWebView.reload();
1869 case R.id.font_size_twenty_five_percent:
1870 currentWebView.getSettings().setTextZoom(25);
1873 case R.id.font_size_fifty_percent:
1874 currentWebView.getSettings().setTextZoom(50);
1877 case R.id.font_size_seventy_five_percent:
1878 currentWebView.getSettings().setTextZoom(75);
1881 case R.id.font_size_one_hundred_percent:
1882 currentWebView.getSettings().setTextZoom(100);
1885 case R.id.font_size_one_hundred_twenty_five_percent:
1886 currentWebView.getSettings().setTextZoom(125);
1889 case R.id.font_size_one_hundred_fifty_percent:
1890 currentWebView.getSettings().setTextZoom(150);
1893 case R.id.font_size_one_hundred_seventy_five_percent:
1894 currentWebView.getSettings().setTextZoom(175);
1897 case R.id.font_size_two_hundred_percent:
1898 currentWebView.getSettings().setTextZoom(200);
1901 case R.id.swipe_to_refresh:
1902 // Toggle the stored status of swipe to refresh.
1903 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1905 // Get a handle for the swipe refresh layout.
1906 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1908 // Update the swipe refresh layout.
1909 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
1910 if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, the status of the scroll refresh listener is continuously updated by the on scroll change listener.
1911 // Only enable the swipe refresh layout if the WebView is scrolled to the top.
1912 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1913 } else { // For API < 23, the swipe refresh layout is always enabled.
1914 // Enable the swipe refresh layout.
1915 swipeRefreshLayout.setEnabled(true);
1917 } else { // Swipe to refresh is disabled.
1918 // Disable the swipe refresh layout.
1919 swipeRefreshLayout.setEnabled(false);
1923 case R.id.display_images:
1924 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1925 // Disable loading of images.
1926 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1928 // Reload the website to remove existing images.
1929 currentWebView.reload();
1930 } else { // Images are not currently loaded automatically.
1931 // Enable loading of images. Missing images will be loaded without the need for a reload.
1932 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1936 case R.id.night_mode:
1937 // Toggle night mode.
1938 currentWebView.setNightMode(!currentWebView.getNightMode());
1940 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1941 if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript.
1942 // Enable JavaScript.
1943 currentWebView.getSettings().setJavaScriptEnabled(true);
1944 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
1945 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1946 currentWebView.getSettings().setJavaScriptEnabled(domainSettingsJavaScriptEnabled); // TODO.
1947 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
1948 // Apply the JavaScript preference.
1949 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1952 // Update the privacy icons.
1953 updatePrivacyIcons(false);
1955 // Reload the website.
1956 currentWebView.reload();
1959 case R.id.find_on_page:
1960 // Get a handle for the views.
1961 Toolbar toolbar = findViewById(R.id.toolbar);
1962 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1963 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1965 // Hide the toolbar.
1966 toolbar.setVisibility(View.GONE);
1968 // Show the find on page linear layout.
1969 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1971 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1972 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1973 findOnPageEditText.postDelayed(() -> {
1974 // Set the focus on `findOnPageEditText`.
1975 findOnPageEditText.requestFocus();
1977 // Display the keyboard. `0` sets no input flags.
1978 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1982 case R.id.view_source:
1983 // Create an intent to launch the view source activity.
1984 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1986 // Add the user agent as an extra to the intent.
1987 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1990 startActivity(viewSourceIntent);
1993 case R.id.share_url:
1994 // Setup the share string.
1995 String shareString = currentWebView.getTitle() + " – " + formattedUrlString;
1997 // Create the share intent.
1998 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1999 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2000 shareIntent.setType("text/plain");
2003 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2007 // Get a `PrintManager` instance.
2008 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2010 // Create a print document adapter form the current WebView.
2011 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2013 // Remove the lint error below that `printManager` might be `null`.
2014 assert printManager != null;
2016 // Print the document. The print attributes are `null`.
2017 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2020 case R.id.open_with_app:
2021 openWithApp(formattedUrlString);
2024 case R.id.open_with_browser:
2025 openWithBrowser(formattedUrlString);
2028 case R.id.add_to_homescreen:
2029 // Instantiate the create home screen shortcut dialog.
2030 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, currentWebView.getFavoriteOrDefaultIcon());
2032 // Show the create home screen shortcut dialog.
2033 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2036 case R.id.proxy_through_orbot:
2037 // Toggle the proxy through Orbot variable.
2038 proxyThroughOrbot = !proxyThroughOrbot;
2040 // Apply the proxy through Orbot settings.
2041 applyProxyThroughOrbot(true);
2045 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2046 // Reload the current WebView.
2047 currentWebView.reload();
2048 } else { // The stop button was pushed.
2049 // Stop the loading of the WebView.
2050 currentWebView.stopLoading();
2054 case R.id.ad_consent:
2055 // Display the ad consent dialog.
2056 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2057 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2061 // Don't consume the event.
2062 return super.onOptionsItemSelected(menuItem);
2066 // removeAllCookies is deprecated, but it is required for API < 21.
2067 @SuppressWarnings("deprecation")
2069 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2070 // Get the menu item ID.
2071 int menuItemId = menuItem.getItemId();
2073 // Run the commands that correspond to the selected menu item.
2074 switch (menuItemId) {
2075 case R.id.close_tab:
2076 // Get a handle for the tab layout and the view pager.
2077 TabLayout tabLayout = findViewById(R.id.tablayout);
2078 ViewPager webViewPager = findViewById(R.id.webviewpager);
2080 // Get the current tab number.
2081 int currentTabNumber = tabLayout.getSelectedTabPosition();
2083 // Delete the current tab.
2084 tabLayout.removeTabAt(currentTabNumber);
2086 // 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.
2087 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
2088 setCurrentWebView(currentTabNumber);
2092 case R.id.clear_and_exit:
2093 // Close the bookmarks cursor and database.
2094 bookmarksCursor.close();
2095 bookmarksDatabaseHelper.close();
2097 // Get a handle for the shared preferences.
2098 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2100 // Get the status of the clear everything preference.
2101 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2103 // Get a handle for the runtime.
2104 Runtime runtime = Runtime.getRuntime();
2107 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2108 // The command to remove cookies changed slightly in API 21.
2109 if (Build.VERSION.SDK_INT >= 21) {
2110 CookieManager.getInstance().removeAllCookies(null);
2112 CookieManager.getInstance().removeAllCookie();
2115 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2117 // Two commands must be used because `Runtime.exec()` does not like `*`.
2118 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2119 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2121 // Wait until the processes have finished.
2122 deleteCookiesProcess.waitFor();
2123 deleteCookiesJournalProcess.waitFor();
2124 } catch (Exception exception) {
2125 // Do nothing if an error is thrown.
2129 // Clear DOM storage.
2130 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2131 // Ask `WebStorage` to clear the DOM storage.
2132 WebStorage webStorage = WebStorage.getInstance();
2133 webStorage.deleteAllData();
2135 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2137 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2138 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2140 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2141 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2142 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2143 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2144 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2146 // Wait until the processes have finished.
2147 deleteLocalStorageProcess.waitFor();
2148 deleteIndexProcess.waitFor();
2149 deleteQuotaManagerProcess.waitFor();
2150 deleteQuotaManagerJournalProcess.waitFor();
2151 deleteDatabaseProcess.waitFor();
2152 } catch (Exception exception) {
2153 // Do nothing if an error is thrown.
2157 // Clear form data if the API < 26.
2158 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2159 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2160 webViewDatabase.clearFormData();
2162 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2164 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2165 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2166 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2168 // Wait until the processes have finished.
2169 deleteWebDataProcess.waitFor();
2170 deleteWebDataJournalProcess.waitFor();
2171 } catch (Exception exception) {
2172 // Do nothing if an error is thrown.
2177 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2178 // Clear the cache from each WebView.
2179 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2180 // Get the WebView tab fragment.
2181 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2183 // Get the fragment view.
2184 View fragmentView = webViewTabFragment.getView();
2186 // Only clear the cache if the WebView exists.
2187 if (fragmentView != null) {
2188 // Get the nested scroll WebView from the tab fragment.
2189 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2191 // Clear the cache for this WebView.
2192 nestedScrollWebView.clearCache(true);
2196 // Manually delete the cache directories.
2198 // Delete the main cache directory.
2199 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2201 // Delete the secondary `Service Worker` cache directory.
2202 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2203 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2205 // Wait until the processes have finished.
2206 deleteCacheProcess.waitFor();
2207 deleteServiceWorkerProcess.waitFor();
2208 } catch (Exception exception) {
2209 // Do nothing if an error is thrown.
2213 // Wipe out each WebView.
2214 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2215 // Get the WebView tab fragment.
2216 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2218 // Get the fragment view.
2219 View fragmentView = webViewTabFragment.getView();
2221 // Only wipe out the WebView if it exists.
2222 if (fragmentView != null) {
2223 // Get the nested scroll WebView from the tab fragment.
2224 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2226 // Clear SSL certificate preferences for this WebView.
2227 nestedScrollWebView.clearSslPreferences();
2229 // Clear the back/forward history for this WebView.
2230 nestedScrollWebView.clearHistory();
2232 // Destroy the internal state of `mainWebView`.
2233 nestedScrollWebView.destroy();
2237 // Clear the formatted URL string.
2238 formattedUrlString = null;
2240 // Clear the custom headers.
2241 customHeaders.clear();
2243 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2244 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2245 if (clearEverything) {
2247 // Delete the folder.
2248 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2250 // Wait until the process has finished.
2251 deleteAppWebviewProcess.waitFor();
2252 } catch (Exception exception) {
2253 // Do nothing if an error is thrown.
2257 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2258 if (Build.VERSION.SDK_INT >= 21) {
2259 finishAndRemoveTask();
2264 // Remove the terminated program from RAM. The status code is `0`.
2273 if (currentWebView.canGoBack()) {
2274 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2275 formattedUrlString = "";
2277 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2278 navigatingHistory = true;
2280 // Load the previous website in the history.
2281 currentWebView.goBack();
2286 if (currentWebView.canGoForward()) {
2287 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2288 formattedUrlString = "";
2290 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2291 navigatingHistory = true;
2293 // Load the next website in the history.
2294 currentWebView.goForward();
2299 // Get the `WebBackForwardList`.
2300 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2302 // Show the URL history dialog and name this instance `R.string.history`.
2303 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2304 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2308 // Populate the resource requests.
2309 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2311 // Create an intent to launch the Requests activity.
2312 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2314 // Add the block third-party requests status to the intent.
2315 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2318 startActivity(requestsIntent);
2321 case R.id.downloads:
2322 // Launch the system Download Manager.
2323 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2325 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2326 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2328 startActivity(downloadManagerIntent);
2332 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2333 reapplyDomainSettingsOnRestart = true;
2334 currentWebView.resetCurrentDomainName(); // TODO. Do this for all tabs.
2336 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
2337 // Store the current SSL certificate and IP addresses in the domains activity.
2338 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
2339 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
2341 // Launch the domains activity.
2342 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2343 startActivity(domainsIntent);
2347 // Set the flag to reapply app settings on restart when returning from Settings.
2348 reapplyAppSettingsOnRestart = true;
2350 // Set the flag to reapply the domain settings on restart when returning from Settings.
2351 reapplyDomainSettingsOnRestart = true;
2352 currentWebView.resetCurrentDomainName(); // TODO. Do this for all tabs.
2354 // Launch the settings activity.
2355 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2356 startActivity(settingsIntent);
2359 case R.id.import_export:
2360 // Launch the import/export activity.
2361 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2362 startActivity(importExportIntent);
2366 // Launch the logcat activity.
2367 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2368 startActivity(logcatIntent);
2372 // Launch `GuideActivity`.
2373 Intent guideIntent = new Intent(this, GuideActivity.class);
2374 startActivity(guideIntent);
2378 // Launch `AboutActivity`.
2379 Intent aboutIntent = new Intent(this, AboutActivity.class);
2380 startActivity(aboutIntent);
2384 // Get a handle for the drawer layout.
2385 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2387 // Close the navigation drawer.
2388 drawerLayout.closeDrawer(GravityCompat.START);
2393 public void onPostCreate(Bundle savedInstanceState) {
2394 // Run the default commands.
2395 super.onPostCreate(savedInstanceState);
2397 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2398 actionBarDrawerToggle.syncState();
2402 public void onConfigurationChanged(Configuration newConfig) {
2403 // Run the default commands.
2404 super.onConfigurationChanged(newConfig);
2406 // Get the status bar pixel size.
2407 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2408 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2410 // Get the resource density.
2411 float screenDensity = getResources().getDisplayMetrics().density;
2413 // Recalculate the drawer header padding.
2414 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2415 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2416 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2418 // Reload the ad for the free flavor if not in full screen mode.
2419 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2420 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2421 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2424 // `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:
2425 // https://code.google.com/p/android/issues/detail?id=20493#c8
2426 // ActivityCompat.invalidateOptionsMenu(this);
2430 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2431 // Store the `HitTestResult`.
2432 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2435 final String imageUrl;
2436 final String linkUrl;
2438 // Get a handle for the the clipboard and fragment managers.
2439 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2440 FragmentManager fragmentManager = getSupportFragmentManager();
2442 // Remove the lint errors below that `clipboardManager` might be `null`.
2443 assert clipboardManager != null;
2445 switch (hitTestResult.getType()) {
2446 // `SRC_ANCHOR_TYPE` is a link.
2447 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2448 // Get the target URL.
2449 linkUrl = hitTestResult.getExtra();
2451 // Set the target URL as the title of the `ContextMenu`.
2452 menu.setHeaderTitle(linkUrl);
2454 // Add a Load URL entry.
2455 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
2460 // Add a Copy URL entry.
2461 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2462 // Save the link URL in a `ClipData`.
2463 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2465 // Set the `ClipData` as the clipboard's primary clip.
2466 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2470 // Add a Download URL entry.
2471 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2472 // Check if the download should be processed by an external app.
2473 if (downloadWithExternalApp) { // Download with an external app.
2474 openUrlWithExternalApp(linkUrl);
2475 } else { // Download with Android's download manager.
2476 // Check to see if the storage permission has already been granted.
2477 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2478 // Store the variables for future use by `onRequestPermissionsResult()`.
2479 downloadUrl = linkUrl;
2480 downloadContentDisposition = "none";
2481 downloadContentLength = -1;
2483 // Show a dialog if the user has previously denied the permission.
2484 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2485 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2486 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2488 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2489 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2490 } else { // Show the permission request directly.
2491 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2492 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2494 } else { // The storage permission has already been granted.
2495 // Get a handle for the download file alert dialog.
2496 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2498 // Show the download file alert dialog.
2499 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2505 // Add an Open with App entry.
2506 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2507 openWithApp(linkUrl);
2511 // Add an Open with Browser entry.
2512 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2513 openWithBrowser(linkUrl);
2517 // Add a Cancel entry, which by default closes the context menu.
2518 menu.add(R.string.cancel);
2521 case WebView.HitTestResult.EMAIL_TYPE:
2522 // Get the target URL.
2523 linkUrl = hitTestResult.getExtra();
2525 // Set the target URL as the title of the `ContextMenu`.
2526 menu.setHeaderTitle(linkUrl);
2528 // Add a Write Email entry.
2529 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2530 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2531 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2533 // Parse the url and set it as the data for the `Intent`.
2534 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2536 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2537 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2540 startActivity(emailIntent);
2544 // Add a Copy Email Address entry.
2545 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2546 // Save the email address in a `ClipData`.
2547 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2549 // Set the `ClipData` as the clipboard's primary clip.
2550 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2554 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2555 menu.add(R.string.cancel);
2558 // `IMAGE_TYPE` is an image.
2559 case WebView.HitTestResult.IMAGE_TYPE:
2560 // Get the image URL.
2561 imageUrl = hitTestResult.getExtra();
2563 // Set the image URL as the title of the `ContextMenu`.
2564 menu.setHeaderTitle(imageUrl);
2566 // Add a View Image entry.
2567 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2572 // Add a Download Image entry.
2573 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2574 // Check if the download should be processed by an external app.
2575 if (downloadWithExternalApp) { // Download with an external app.
2576 openUrlWithExternalApp(imageUrl);
2577 } else { // Download with Android's download manager.
2578 // Check to see if the storage permission has already been granted.
2579 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2580 // Store the image URL for use by `onRequestPermissionResult()`.
2581 downloadImageUrl = imageUrl;
2583 // Show a dialog if the user has previously denied the permission.
2584 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2585 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2586 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2588 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2589 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2590 } else { // Show the permission request directly.
2591 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2592 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2594 } else { // The storage permission has already been granted.
2595 // Get a handle for the download image alert dialog.
2596 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2598 // Show the download image alert dialog.
2599 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2605 // Add a Copy URL entry.
2606 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2607 // Save the image URL in a `ClipData`.
2608 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2610 // Set the `ClipData` as the clipboard's primary clip.
2611 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2615 // Add an Open with App entry.
2616 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2617 openWithApp(imageUrl);
2621 // Add an Open with Browser entry.
2622 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2623 openWithBrowser(imageUrl);
2627 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2628 menu.add(R.string.cancel);
2632 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2633 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2634 // Get the image URL.
2635 imageUrl = hitTestResult.getExtra();
2637 // Set the image URL as the title of the `ContextMenu`.
2638 menu.setHeaderTitle(imageUrl);
2640 // Add a `View Image` entry.
2641 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2646 // Add a `Download Image` entry.
2647 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2648 // Check if the download should be processed by an external app.
2649 if (downloadWithExternalApp) { // Download with an external app.
2650 openUrlWithExternalApp(imageUrl);
2651 } else { // Download with Android's download manager.
2652 // Check to see if the storage permission has already been granted.
2653 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2654 // Store the image URL for use by `onRequestPermissionResult()`.
2655 downloadImageUrl = imageUrl;
2657 // Show a dialog if the user has previously denied the permission.
2658 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2659 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2660 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2662 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2663 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2664 } else { // Show the permission request directly.
2665 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2666 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2668 } else { // The storage permission has already been granted.
2669 // Get a handle for the download image alert dialog.
2670 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2672 // Show the download image alert dialog.
2673 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2679 // Add a `Copy URL` entry.
2680 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2681 // Save the image URL in a `ClipData`.
2682 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2684 // Set the `ClipData` as the clipboard's primary clip.
2685 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2689 // Add an Open with App entry.
2690 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2691 openWithApp(imageUrl);
2695 // Add an Open with Browser entry.
2696 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2697 openWithBrowser(imageUrl);
2701 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2702 menu.add(R.string.cancel);
2708 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2709 // Get the views from the dialog fragment.
2710 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2711 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2713 // Extract the strings from the edit texts.
2714 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2715 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2717 // Create a favorite icon byte array output stream.
2718 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2720 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2721 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2723 // Convert the favorite icon byte array stream to a byte array.
2724 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2726 // Display the new bookmark below the current items in the (0 indexed) list.
2727 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2729 // Create the bookmark.
2730 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2732 // Update the bookmarks cursor with the current contents of this folder.
2733 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2735 // Update the list view.
2736 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2738 // Scroll to the new bookmark.
2739 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2743 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2744 // Get handles for the views in the dialog fragment.
2745 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2746 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2747 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2749 // Get new folder name string.
2750 String folderNameString = createFolderNameEditText.getText().toString();
2752 // Create a folder icon bitmap.
2753 Bitmap folderIconBitmap;
2755 // Set the folder icon bitmap according to the dialog.
2756 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2757 // Get the default folder icon drawable.
2758 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2760 // Convert the folder icon drawable to a bitmap drawable.
2761 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2763 // Convert the folder icon bitmap drawable to a bitmap.
2764 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2765 } else { // Use the WebView favorite icon.
2766 // Copy the favorite icon bitmap to the folder icon bitmap.
2767 folderIconBitmap = favoriteIconBitmap;
2770 // Create a folder icon byte array output stream.
2771 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2773 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2774 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2776 // Convert the folder icon byte array stream to a byte array.
2777 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2779 // Move all the bookmarks down one in the display order.
2780 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2781 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2782 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2785 // Create the folder, which will be placed at the top of the `ListView`.
2786 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2788 // Update the bookmarks cursor with the current contents of this folder.
2789 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2791 // Update the `ListView`.
2792 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2794 // Scroll to the new folder.
2795 bookmarksListView.setSelection(0);
2799 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2800 // Get handles for the views from `dialogFragment`.
2801 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2802 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2803 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2805 // Store the bookmark strings.
2806 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2807 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2809 // Update the bookmark.
2810 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2811 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2812 } else { // Update the bookmark using the `WebView` favorite icon.
2813 // Create a favorite icon byte array output stream.
2814 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2816 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2817 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2819 // Convert the favorite icon byte array stream to a byte array.
2820 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2822 // Update the bookmark and the favorite icon.
2823 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2826 // Update the bookmarks cursor with the current contents of this folder.
2827 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2829 // Update the list view.
2830 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2834 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2835 // Get handles for the views from `dialogFragment`.
2836 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2837 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2838 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2839 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2841 // Get the new folder name.
2842 String newFolderNameString = editFolderNameEditText.getText().toString();
2844 // Check if the favorite icon has changed.
2845 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2846 // Update the name in the database.
2847 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2848 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2849 // Create the new folder icon Bitmap.
2850 Bitmap folderIconBitmap;
2852 // Populate the new folder icon bitmap.
2853 if (defaultFolderIconRadioButton.isChecked()) {
2854 // Get the default folder icon drawable.
2855 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2857 // Convert the folder icon drawable to a bitmap drawable.
2858 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2860 // Convert the folder icon bitmap drawable to a bitmap.
2861 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2862 } else { // Use the `WebView` favorite icon.
2863 // Copy the favorite icon bitmap to the folder icon bitmap.
2864 folderIconBitmap = favoriteIconBitmap;
2867 // Create a folder icon byte array output stream.
2868 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2870 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2871 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2873 // Convert the folder icon byte array stream to a byte array.
2874 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2876 // Update the folder icon in the database.
2877 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2878 } else { // The folder icon and the name have changed.
2879 // Get the new folder icon `Bitmap`.
2880 Bitmap folderIconBitmap;
2881 if (defaultFolderIconRadioButton.isChecked()) {
2882 // Get the default folder icon drawable.
2883 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2885 // Convert the folder icon drawable to a bitmap drawable.
2886 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2888 // Convert the folder icon bitmap drawable to a bitmap.
2889 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2890 } else { // Use the `WebView` favorite icon.
2891 // Copy the favorite icon bitmap to the folder icon bitmap.
2892 folderIconBitmap = favoriteIconBitmap;
2895 // Create a folder icon byte array output stream.
2896 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2898 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2899 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2901 // Convert the folder icon byte array stream to a byte array.
2902 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2904 // Update the folder name and icon in the database.
2905 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2908 // Update the bookmarks cursor with the current contents of this folder.
2909 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2911 // Update the `ListView`.
2912 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2916 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2917 switch (downloadType) {
2918 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2919 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2920 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2923 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2924 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2925 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2931 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2932 // Get a handle for the fragment manager.
2933 FragmentManager fragmentManager = getSupportFragmentManager();
2935 switch (requestCode) {
2936 case DOWNLOAD_FILE_REQUEST_CODE:
2937 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2938 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2940 // On API 23, displaying the fragment must be delayed or the app will crash.
2941 if (Build.VERSION.SDK_INT == 23) {
2942 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2944 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2947 // Reset the download variables.
2949 downloadContentDisposition = "";
2950 downloadContentLength = 0;
2953 case DOWNLOAD_IMAGE_REQUEST_CODE:
2954 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2955 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
2957 // On API 23, displaying the fragment must be delayed or the app will crash.
2958 if (Build.VERSION.SDK_INT == 23) {
2959 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2961 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2964 // Reset the image URL variable.
2965 downloadImageUrl = "";
2971 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
2972 // Download the image if it has an HTTP or HTTPS URI.
2973 if (imageUrl.startsWith("http")) {
2974 // Get a handle for the system `DOWNLOAD_SERVICE`.
2975 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2977 // Parse `imageUrl`.
2978 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
2980 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
2981 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2982 if (firstPartyCookiesEnabled) {
2983 // Get the cookies for `imageUrl`.
2984 String cookies = CookieManager.getInstance().getCookie(imageUrl);
2986 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
2987 downloadRequest.addRequestHeader("Cookie", cookies);
2990 // Get the file name from the dialog fragment.
2991 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
2992 String imageName = downloadImageNameEditText.getText().toString();
2994 // Specify the download location.
2995 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
2996 // Download to the public download directory.
2997 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
2998 } else { // External write permission denied.
2999 // Download to the app's external download directory.
3000 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3003 // Allow `MediaScanner` to index the download if it is a media file.
3004 downloadRequest.allowScanningByMediaScanner();
3006 // Add the URL as the description for the download.
3007 downloadRequest.setDescription(imageUrl);
3009 // Show the download notification after the download is completed.
3010 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3012 // Remove the lint warning below that `downloadManager` might be `null`.
3013 assert downloadManager != null;
3015 // Initiate the download.
3016 downloadManager.enqueue(downloadRequest);
3017 } else { // The image is not an HTTP or HTTPS URI.
3018 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3023 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3024 // Download the file if it has an HTTP or HTTPS URI.
3025 if (downloadUrl.startsWith("http")) {
3026 // Get a handle for the system `DOWNLOAD_SERVICE`.
3027 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3029 // Parse `downloadUrl`.
3030 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3032 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
3033 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3034 if (firstPartyCookiesEnabled) {
3035 // Get the cookies for `downloadUrl`.
3036 String cookies = CookieManager.getInstance().getCookie(downloadUrl);
3038 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3039 downloadRequest.addRequestHeader("Cookie", cookies);
3042 // Get the file name from the dialog fragment.
3043 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3044 String fileName = downloadFileNameEditText.getText().toString();
3046 // Specify the download location.
3047 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3048 // Download to the public download directory.
3049 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3050 } else { // External write permission denied.
3051 // Download to the app's external download directory.
3052 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3055 // Allow `MediaScanner` to index the download if it is a media file.
3056 downloadRequest.allowScanningByMediaScanner();
3058 // Add the URL as the description for the download.
3059 downloadRequest.setDescription(downloadUrl);
3061 // Show the download notification after the download is completed.
3062 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3064 // Remove the lint warning below that `downloadManager` might be `null`.
3065 assert downloadManager != null;
3067 // Initiate the download.
3068 downloadManager.enqueue(downloadRequest);
3069 } else { // The download is not an HTTP or HTTPS URI.
3070 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3075 public void onHttpAuthenticationCancel() {
3076 // Cancel the `HttpAuthHandler`.
3077 httpAuthHandler.cancel();
3081 public void onHttpAuthenticationProceed(DialogFragment dialogFragment) {
3082 // Get handles for the `EditTexts`.
3083 EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3084 EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3086 // Proceed with the HTTP authentication.
3087 httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3091 public void onSslErrorCancel() { // TODO. How to handle this with multiple tabs? There could be multiple errors at once.
3092 sslErrorHandler.cancel();
3096 public void onSslErrorProceed() { // TODO. How to handle this with multiple tabs? There could be multiple errors at once.
3097 sslErrorHandler.proceed();
3101 public void onPinnedMismatchBack() { // TODO. Move this logic to the dialog.
3102 if (currentWebView.canGoBack()) { // There is a back page in the history.
3103 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3104 formattedUrlString = ""; // TODO.
3106 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3107 navigatingHistory = true; // TODO.
3110 currentWebView.goBack();
3111 } else { // There are no pages to go back to.
3112 // Load a blank page
3118 public void onPinnedMismatchProceed() { // TODO. Move this logic to the dialog.
3119 // Do not check the pinned information for this domain again until the domain changes.
3120 currentWebView.setIgnorePinnedDomainInformation(true);
3124 public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3125 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3126 formattedUrlString = "";
3128 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3129 navigatingHistory = true;
3131 // Load the history entry.
3132 currentWebView.goBackOrForward(moveBackOrForwardSteps);
3136 public void onClearHistory() {
3137 // Clear the history.
3138 currentWebView.clearHistory();
3141 // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3143 public void onBackPressed() {
3144 // Get a handle for the drawer layout.
3145 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3147 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
3148 // Close the navigation drawer.
3149 drawerLayout.closeDrawer(GravityCompat.START);
3150 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
3151 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
3152 // close the bookmarks drawer.
3153 drawerLayout.closeDrawer(GravityCompat.END);
3154 } else { // A subfolder is displayed.
3155 // Place the former parent folder in `currentFolder`.
3156 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3158 // Load the new folder.
3159 loadBookmarksFolder();
3162 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
3163 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3164 formattedUrlString = "";
3166 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3167 navigatingHistory = true;
3170 currentWebView.goBack();
3171 } else { // There isn't anything to do in Privacy Browser.
3172 // Pass `onBackPressed()` to the system.
3173 super.onBackPressed();
3177 // 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.
3179 public void onActivityResult(int requestCode, int resultCode, Intent data) {
3180 // File uploads only work on API >= 21.
3181 if (Build.VERSION.SDK_INT >= 21) {
3182 // Pass the file to the WebView.
3183 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3187 private void loadUrlFromTextBox() {
3188 // Get a handle for the URL edit text.
3189 EditText urlEditText = findViewById(R.id.url_edittext);
3191 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
3192 String unformattedUrlString = urlEditText.getText().toString().trim();
3194 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
3195 if (unformattedUrlString.startsWith("content://")) {
3196 // Load the entire content URL.
3197 formattedUrlString = unformattedUrlString;
3198 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3199 || unformattedUrlString.startsWith("file://")) {
3200 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
3201 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3202 unformattedUrlString = "https://" + unformattedUrlString;
3205 // Initialize `unformattedUrl`.
3206 URL unformattedUrl = null;
3208 // 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.
3210 unformattedUrl = new URL(unformattedUrlString);
3211 } catch (MalformedURLException e) {
3212 e.printStackTrace();
3215 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3216 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3217 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3218 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3219 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3220 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3223 Uri.Builder formattedUri = new Uri.Builder();
3224 formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3226 // Decode `formattedUri` as a `String` in `UTF-8`.
3228 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
3229 } catch (UnsupportedEncodingException exception) {
3230 // Load a blank string.
3231 formattedUrlString = "";
3233 } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
3234 // Load a blank string.
3235 formattedUrlString = "";
3236 } else { // Search for the contents of the URL box.
3237 // Create an encoded URL String.
3238 String encodedUrlString;
3240 // Sanitize the search input.
3242 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3243 } catch (UnsupportedEncodingException exception) {
3244 encodedUrlString = "";
3247 // Add the base search URL.
3248 formattedUrlString = searchURL + encodedUrlString;
3251 // 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.
3252 urlEditText.clearFocus();
3255 loadUrl(formattedUrlString);
3258 private void loadUrl(String url) {// Apply any custom domain settings.
3259 // Set the URL as the formatted URL string so that checking third-party requests works correctly.
3260 formattedUrlString = url;
3262 // Apply the domain settings.
3263 applyDomainSettings(currentWebView, url, true, false);
3266 currentWebView.loadUrl(url, customHeaders);
3269 public void findPreviousOnPage(View view) {
3270 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
3271 currentWebView.findNext(false);
3274 public void findNextOnPage(View view) {
3275 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3276 currentWebView.findNext(true);
3279 public void closeFindOnPage(View view) {
3280 // Get a handle for the views.
3281 Toolbar toolbar = findViewById(R.id.toolbar);
3282 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3283 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3285 // Delete the contents of `find_on_page_edittext`.
3286 findOnPageEditText.setText(null);
3288 // Clear the highlighted phrases.
3289 currentWebView.clearMatches();
3291 // Hide the find on page linear layout.
3292 findOnPageLinearLayout.setVisibility(View.GONE);
3294 // Show the toolbar.
3295 toolbar.setVisibility(View.VISIBLE);
3297 // Hide the keyboard.
3298 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3301 private void applyAppSettings() {
3302 // Get a handle for the shared preferences.
3303 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3305 // Store the values from the shared preferences in variables.
3306 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3307 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3308 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3309 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3310 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3311 downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
3313 // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
3314 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3315 ActionBar actionBar = getSupportActionBar();
3317 // Remove the incorrect lint warnings below that the action bar might be null.
3318 assert actionBar != null;
3320 // Apply the proxy through Orbot settings.
3321 applyProxyThroughOrbot(false);
3323 // Set Do Not Track status.
3324 if (doNotTrackEnabled) {
3325 customHeaders.put("DNT", "1");
3327 customHeaders.remove("DNT");
3330 // Set the app bar scrolling for each WebView.
3331 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3332 // Get the WebView tab fragment.
3333 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3335 // Get the fragment view.
3336 View fragmentView = webViewTabFragment.getView();
3338 // Only modify the WebViews if they exist.
3339 if (fragmentView != null) {
3340 // Get the nested scroll WebView from the tab fragment.
3341 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3343 // Set the app bar scrolling.
3344 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
3348 // Update the full screen browsing mode settings.
3349 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3350 // Update the visibility of the app bar, which might have changed in the settings.
3357 // Hide the banner ad in the free flavor.
3358 if (BuildConfig.FLAVOR.contentEquals("free")) {
3359 AdHelper.hideAd(findViewById(R.id.adview));
3362 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3363 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3365 /* Hide the system bars.
3366 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3367 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3368 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3369 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3371 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3372 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3373 } else { // Privacy Browser is not in full screen browsing mode.
3374 // 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.
3375 inFullScreenBrowsingMode = false;
3377 // Show the app bar.
3380 // Show the banner ad in the free flavor.
3381 if (BuildConfig.FLAVOR.contentEquals("free")) {
3382 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3383 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3386 // Remove the `SYSTEM_UI` flags from the root frame layout.
3387 rootFrameLayout.setSystemUiVisibility(0);
3389 // Add the translucent status flag.
3390 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3395 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3396 @SuppressLint("SetJavaScriptEnabled")
3397 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
3398 // Store a copy of the current user agent to track changes for the return boolean.
3399 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3401 // Parse the URL into a URI.
3402 Uri uri = Uri.parse(url);
3404 // Extract the domain from `uri`.
3405 String newHostName = uri.getHost();
3407 // Strings don't like to be null.
3408 if (newHostName == null) {
3412 // 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.
3413 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName)) {
3414 // Set the new host name as the current domain name.
3415 nestedScrollWebView.setCurrentDomainName(newHostName);
3417 // Reset the ignoring of pinned domain information.
3418 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3420 // Clear any pinned SSL certificate or IP addresses.
3421 nestedScrollWebView.clearPinnedSslCertificate();
3422 nestedScrollWebView.clearPinnedIpAddresses();
3424 // Reset the favorite icon if specified.
3425 if (resetFavoriteIcon) {
3426 // Initialize the favorite icon.
3427 nestedScrollWebView.initializeFavoriteIcon();
3429 // Get a handle for the tab layout.
3430 TabLayout tabLayout = findViewById(R.id.tablayout);
3432 // Get the current tab.
3433 TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition()); // TODO. We need to get the tab for this WebView, which might not be the current tab.
3435 // Remove the warning below that the current tab might be null.
3436 assert currentTab != null;
3438 // Get the current tab custom view.
3439 View currentTabCustomView = currentTab.getCustomView();
3441 // Remove the warning below that the current tab custom view might be null.
3442 assert currentTabCustomView != null;
3444 // Get the current tab favorite icon image view.
3445 ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
3447 // Set the default favorite icon as the favorite icon for this tab.
3448 currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3451 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3452 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3454 // Get a full cursor from `domainsDatabaseHelper`.
3455 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3457 // Initialize `domainSettingsSet`.
3458 Set<String> domainSettingsSet = new HashSet<>();
3460 // Get the domain name column index.
3461 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3463 // Populate `domainSettingsSet`.
3464 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3465 // Move `domainsCursor` to the current row.
3466 domainNameCursor.moveToPosition(i);
3468 // Store the domain name in `domainSettingsSet`.
3469 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3472 // Close `domainNameCursor.
3473 domainNameCursor.close();
3475 // Initialize the domain name in database variable.
3476 String domainNameInDatabase = null;
3478 // Check the hostname against the domain settings set.
3479 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3480 // Record the domain name in the database.
3481 domainNameInDatabase = newHostName;
3483 // Set the domain settings applied tracker to true.
3484 nestedScrollWebView.setDomainSettingsApplied(true);
3485 } else { // The hostname is not contained in the domain settings set.
3486 // Set the domain settings applied tracker to false.
3487 nestedScrollWebView.setDomainSettingsApplied(false);
3490 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3491 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3492 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3493 // Set the domain settings applied tracker to true.
3494 nestedScrollWebView.setDomainSettingsApplied(true);
3496 // Store the applied domain names as it appears in the database.
3497 domainNameInDatabase = "*." + newHostName;
3500 // Strip out the lowest subdomain of of the host name.
3501 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3505 // Get a handle for the shared preferences.
3506 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3508 // Store the general preference information.
3509 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3510 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3511 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3512 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3514 // Get a handle for the cookie manager.
3515 CookieManager cookieManager = CookieManager.getInstance();
3517 // Get handles for the views.
3518 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3519 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3521 // Initialize the user agent array adapter and string array.
3522 ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3523 String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3525 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3526 // Get a cursor for the current host and move it to the first position.
3527 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3528 currentDomainSettingsCursor.moveToFirst();
3530 // Get the settings from the cursor.
3531 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3532 boolean domainJavaScriptEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3533 firstPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); // TODO.
3534 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3535 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3536 // Form data can be removed once the minimum API >= 26.
3537 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3538 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST,
3539 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3540 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY,
3541 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3542 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3543 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3544 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3545 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3546 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY,
3547 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3548 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3549 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3550 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3551 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3552 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3553 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3554 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3555 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3556 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3557 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3558 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3559 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3560 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3561 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3562 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3563 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3565 // Create the pinned SSL date variables.
3566 Date pinnedSslStartDate;
3567 Date pinnedSslEndDate;
3569 // 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.
3570 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3571 pinnedSslStartDate = null;
3573 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3576 // 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.
3577 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3578 pinnedSslEndDate = null;
3580 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3583 // If there is a pinned SSL certificate, store it in the WebView.
3584 if (pinnedSslCertificate) {
3585 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3586 pinnedSslStartDate, pinnedSslEndDate);
3589 // If there is a pinned IP address, store it in the WebView.
3590 if (pinnedIpAddresses) {
3591 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3594 // Set night mode according to the night mode int.
3595 switch (nightModeInt) {
3596 case DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT:
3597 // Set night mode according to the current default.
3598 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3601 case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3602 // Enable night mode.
3603 nestedScrollWebView.setNightMode(true);
3606 case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3607 // Disable night mode.
3608 nestedScrollWebView.setNightMode(false);
3613 // Store the domain JavaScript status. This is used by the options menu night mode toggle.
3614 domainSettingsJavaScriptEnabled = domainJavaScriptEnabled;
3616 // Enable JavaScript if night mode is enabled.
3617 if (nestedScrollWebView.getNightMode()) {
3618 // Enable JavaScript.
3619 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3621 // Set JavaScript according to the domain settings.
3622 nestedScrollWebView.getSettings().setJavaScriptEnabled(domainJavaScriptEnabled);
3625 // Close `currentHostDomainSettingsCursor`.
3626 currentDomainSettingsCursor.close();
3628 // Apply the domain settings.
3629 cookieManager.setAcceptCookie(firstPartyCookiesEnabled); //TODO This could be bad.
3631 // Apply the form data setting if the API < 26.
3632 if (Build.VERSION.SDK_INT < 26) {
3633 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3636 // Apply the font size.
3637 if (fontSize == 0) { // Apply the default font size.
3638 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3639 } else { // Apply the specified font size.
3640 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3643 // Set third-party cookies status if API >= 21.
3644 if (Build.VERSION.SDK_INT >= 21) {
3645 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3648 // 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.
3649 // <https://redmine.stoutner.com/issues/160>
3650 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3651 // Set the user agent.
3652 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3653 // Get the array position of the default user agent name.
3654 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3656 // Set the user agent according to the system default.
3657 switch (defaultUserAgentArrayPosition) {
3658 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3659 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3660 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3663 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3664 // Set the user agent to `""`, which uses the default value.
3665 nestedScrollWebView.getSettings().setUserAgentString("");
3668 case SETTINGS_CUSTOM_USER_AGENT:
3669 // Set the default custom user agent.
3670 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3674 // Get the user agent string from the user agent data array
3675 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3677 } else { // Set the user agent according to the stored name.
3678 // Get the array position of the user agent name.
3679 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3681 switch (userAgentArrayPosition) {
3682 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3683 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3686 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3687 // Set the user agent to `""`, which uses the default value.
3688 nestedScrollWebView.getSettings().setUserAgentString("");
3692 // Get the user agent string from the user agent data array.
3693 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3698 // Set swipe to refresh.
3699 switch (swipeToRefreshInt) {
3700 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3701 // Store the swipe to refresh status in the nested scroll WebView.
3702 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3704 // Apply swipe to refresh according to the default. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3705 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3708 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3709 // Store the swipe to refresh status in the nested scroll WebView.
3710 nestedScrollWebView.setSwipeToRefresh(true);
3712 // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3713 swipeRefreshLayout.setEnabled(true);
3716 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3717 // Store the swipe to refresh status in the nested scroll WebView.
3718 nestedScrollWebView.setSwipeToRefresh(false);
3720 // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3721 swipeRefreshLayout.setEnabled(false);
3724 // Set the loading of webpage images.
3725 switch (displayWebpageImagesInt) {
3726 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3727 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3730 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3731 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3734 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3735 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3739 // 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.
3741 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3743 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3745 } else { // The new URL does not have custom domain settings. Load the defaults.
3746 // Store the values from the shared preferences.
3747 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3748 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false); // TODO.
3749 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3750 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3751 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3752 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, sharedPreferences.getBoolean("easylist", true));
3753 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3754 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3755 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3756 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3757 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3758 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3760 // Enable JavaScript if night mode is enabled.
3761 if (nestedScrollWebView.getNightMode()) {
3762 // Enable JavaScript.
3763 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3765 // Set JavaScript according to the domain settings.
3766 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3769 // Apply the default settings.
3770 cookieManager.setAcceptCookie(firstPartyCookiesEnabled); // TODO.
3771 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3773 // Store the swipe to refresh status in the nested scroll WebView.
3774 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3776 // Apply swipe to refresh according to the default.
3777 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3779 // Apply the form data setting if the API < 26.
3780 if (Build.VERSION.SDK_INT < 26) {
3781 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3784 // Reset the pinned variables.
3785 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
3787 // Set third-party cookies status if API >= 21.
3788 if (Build.VERSION.SDK_INT >= 21) {
3789 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
3792 // 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.
3793 // <https://redmine.stoutner.com/issues/160>
3794 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3795 // Get the array position of the user agent name.
3796 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3798 // Set the user agent.
3799 switch (userAgentArrayPosition) {
3800 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3801 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3802 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3805 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3806 // Set the user agent to `""`, which uses the default value.
3807 nestedScrollWebView.getSettings().setUserAgentString("");
3810 case SETTINGS_CUSTOM_USER_AGENT:
3811 // Set the default custom user agent.
3812 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3816 // Get the user agent string from the user agent data array
3817 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3821 // Set the loading of webpage images.
3822 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3824 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
3825 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
3828 // Close the domains database helper.
3829 domainsDatabaseHelper.close();
3831 // Update the privacy icons.
3832 updatePrivacyIcons(true);
3835 // Reload the website if returning from the Domains activity.
3836 if (reloadWebsite) {
3837 nestedScrollWebView.reload();
3840 // Return the user agent changed status.
3841 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
3844 private void applyProxyThroughOrbot(boolean reloadWebsite) {
3845 // Get a handle for the shared preferences.
3846 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3848 // Get the search preferences.
3849 String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
3850 String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
3851 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3852 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3853 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3854 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3856 // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
3857 ActionBar actionBar = getSupportActionBar();
3859 // Remove the incorrect lint warning later that the action bar might be null.
3860 assert actionBar != null;
3862 // Set the homepage, search, and proxy options.
3863 if (proxyThroughOrbot) { // Set the Tor options.
3864 // Set `torHomepageString` as `homepage`.
3865 homepage = torHomepageString;
3867 // If formattedUrlString is null assign the homepage to it.
3868 if (formattedUrlString == null) {
3869 formattedUrlString = homepage;
3872 // Set the search URL.
3873 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
3874 searchURL = torSearchCustomUrlString;
3875 } else { // Use the string from the pre-built list.
3876 searchURL = torSearchString;
3879 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
3880 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
3882 // Set the `appBar` background to indicate proxying through Orbot is enabled.
3884 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
3886 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
3889 // Check to see if Orbot is ready.
3890 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
3891 // Set `waitingForOrbot`.
3892 waitingForOrbot = true;
3894 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
3895 currentWebView.getSettings().setUseWideViewPort(false);
3897 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
3898 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
3899 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
3900 // Reload the website.
3901 currentWebView.reload();
3903 } else { // Set the non-Tor options.
3904 // Set `homepageString` as `homepage`.
3905 homepage = homepageString;
3907 // If formattedUrlString is null assign the homepage to it.
3908 if (formattedUrlString == null) {
3909 formattedUrlString = homepage;
3912 // Set the search URL.
3913 if (searchString.equals("Custom URL")) { // Get the custom URL string.
3914 searchURL = searchCustomUrlString;
3915 } else { // Use the string from the pre-built list.
3916 searchURL = searchString;
3919 // Reset the proxy to default. The host is `""` and the port is `"0"`.
3920 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
3922 // Set the default `appBar` background.
3924 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
3926 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
3929 // Reset `waitingForOrbot.
3930 waitingForOrbot = false;
3932 // Reload the WebViews if requested.
3933 if (reloadWebsite) {
3934 // Reload the WebViews.
3935 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3936 // Get the WebView tab fragment.
3937 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3939 // Get the fragment view.
3940 View fragmentView = webViewTabFragment.getView();
3942 // Only reload the WebViews if they exist.
3943 if (fragmentView != null) {
3944 // Get the nested scroll WebView from the tab fragment.
3945 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3947 // Reload the WebView.
3948 nestedScrollWebView.reload();
3955 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
3956 // Only update the privacy icons if the options menu has already been populated.
3957 if (optionsMenu != null) {
3958 // Get handles for the menu items.
3959 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
3960 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
3961 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
3962 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
3964 // Update the privacy icon.
3965 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
3966 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
3967 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled.
3968 privacyMenuItem.setIcon(R.drawable.warning);
3969 } else { // All the dangerous features are disabled.
3970 privacyMenuItem.setIcon(R.drawable.privacy_mode);
3973 // Update the first-party cookies icon.
3974 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
3975 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
3976 } else { // First-party cookies are disabled.
3978 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
3980 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
3984 // Update the DOM storage icon.
3985 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
3986 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
3987 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
3989 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
3991 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
3993 } else { // JavaScript is disabled, so DOM storage is ghosted.
3995 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
3997 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4001 // Update the refresh icon.
4003 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4005 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4008 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4009 if (runInvalidateOptionsMenu) {
4010 invalidateOptionsMenu();
4015 private void openUrlWithExternalApp(String url) {
4016 // Create a download intent. Not specifying the action type will display the maximum number of options.
4017 Intent downloadIntent = new Intent();
4019 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4020 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4022 // Flag the intent to open in a new task.
4023 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4025 // Show the chooser.
4026 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4029 private void highlightUrlText() {
4030 // Get a handle for the URL edit text.
4031 EditText urlEditText = findViewById(R.id.url_edittext);
4033 // Only highlight the URL text if the box is not currently selected.
4034 if (!urlEditText.hasFocus()) {
4035 // Get the URL string.
4036 String urlString = urlEditText.getText().toString();
4038 // Highlight the URL according to the protocol.
4039 if (urlString.startsWith("file://")) { // This is a file URL.
4040 // De-emphasize only the protocol.
4041 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4042 } else if (urlString.startsWith("content://")) {
4043 // De-emphasize only the protocol.
4044 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4045 } else { // This is a web URL.
4046 // Get the index of the `/` immediately after the domain name.
4047 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4049 // Create a base URL string.
4052 // Get the base URL.
4053 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4054 // Get the base URL.
4055 baseUrl = urlString.substring(0, endOfDomainName);
4056 } else { // There are no characters after the base URL.
4057 // Set the base URL to be the entire URL string.
4058 baseUrl = urlString;
4061 // Get the index of the last `.` in the domain.
4062 int lastDotIndex = baseUrl.lastIndexOf(".");
4064 // Get the index of the penultimate `.` in the domain.
4065 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4067 // Markup the beginning of the URL.
4068 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4069 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4071 // De-emphasize subdomains.
4072 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4073 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4075 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4076 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4077 // De-emphasize the protocol and the additional subdomains.
4078 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4079 } else { // There is only one subdomain in the domain name.
4080 // De-emphasize only the protocol.
4081 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4085 // De-emphasize the text after the domain name.
4086 if (endOfDomainName > 0) {
4087 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4093 private void loadBookmarksFolder() {
4094 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4095 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4097 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4098 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4100 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4101 // Inflate the individual item layout. `false` does not attach it to the root.
4102 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4106 public void bindView(View view, Context context, Cursor cursor) {
4107 // Get handles for the views.
4108 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4109 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4111 // Get the favorite icon byte array from the cursor.
4112 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4114 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4115 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4117 // Display the bitmap in `bookmarkFavoriteIcon`.
4118 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4120 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4121 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4122 bookmarkNameTextView.setText(bookmarkNameString);
4124 // Make the font bold for folders.
4125 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4126 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4127 } else { // Reset the font to default for normal bookmarks.
4128 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4133 // Populate the `ListView` with the adapter.
4134 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4136 // Get a handle for the bookmarks title text view.
4137 TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4139 // Set the bookmarks drawer title.
4140 if (currentBookmarksFolder.isEmpty()) {
4141 bookmarksTitleTextView.setText(R.string.bookmarks);
4143 bookmarksTitleTextView.setText(currentBookmarksFolder);
4147 private void openWithApp(String url) {
4148 // Create the open with intent with `ACTION_VIEW`.
4149 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4151 // Set the URI but not the MIME type. This should open all available apps.
4152 openWithAppIntent.setData(Uri.parse(url));
4154 // Flag the intent to open in a new task.
4155 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4157 // Show the chooser.
4158 startActivity(openWithAppIntent);
4161 private void openWithBrowser(String url) {
4162 // Create the open with intent with `ACTION_VIEW`.
4163 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4165 // Set the URI and the MIME type. `"text/html"` should load browser options.
4166 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4168 // Flag the intent to open in a new task.
4169 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4171 // Show the chooser.
4172 startActivity(openWithBrowserIntent);
4175 public void addTab(View view) {
4176 // Get a handle for the tab layout and the view pager.
4177 TabLayout tabLayout = findViewById(R.id.tablayout);
4178 ViewPager webViewPager = findViewById(R.id.webviewpager);
4180 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4181 int newTabNumber = tabLayout.getTabCount();
4184 tabLayout.addTab(tabLayout.newTab());
4187 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4189 // Remove the lint warning below that the current tab might be null.
4190 assert newTab != null;
4192 // Set a custom view on the new tab.
4193 newTab.setCustomView(R.layout.custom_tab_view);
4195 // Add the new WebView page.
4196 webViewPagerAdapter.addPage(newTabNumber, webViewPager);
4199 private void setCurrentWebView(int pageNumber) {
4200 // Get handles for the URL views.
4201 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4202 EditText urlEditText = findViewById(R.id.url_edittext);
4203 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4205 //Stop the swipe to refresh indicator if it is running
4206 swipeRefreshLayout.setRefreshing(false);
4208 // Get the WebView tab fragment.
4209 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4211 // Get the fragment view.
4212 View fragmentView = webViewTabFragment.getView();
4214 // Remove the incorrect lint warning below that the fragment view might be null.
4215 assert fragmentView != null;
4217 // Store the current WebView.
4218 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4220 // Update the status of swipe to refresh.
4221 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
4222 if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, swipe refresh layout is continuously updated with an on scroll change listener and only enabled if the WebView is scrolled to the top.
4223 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.
4224 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4226 // Enable the swipe refresh layout.
4227 swipeRefreshLayout.setEnabled(true);
4229 } else { // Swipe to refresh is disabled.
4230 // Disable the swipe refresh layout.
4231 swipeRefreshLayout.setEnabled(false);
4234 // Update the privacy icons. `true` redraws the icons in the app bar.
4235 updatePrivacyIcons(true);
4237 // Store the current formatted URL string.
4238 formattedUrlString = currentWebView.getUrl();
4240 // Clear the focus from the URL text box.
4241 urlEditText.clearFocus();
4243 // Hide the soft keyboard.
4244 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4246 // Display the current URL in the URL text box.
4247 urlEditText.setText(formattedUrlString);
4249 // Highlight the URL text.
4252 // Set the background to indicate the domain settings status.
4253 if (currentWebView.getDomainSettingsApplied()) {
4254 // 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.
4256 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4258 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4261 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4266 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar) {
4267 // Get handles for the activity views.
4268 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4269 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4270 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4271 ActionBar actionBar = getSupportActionBar();
4272 EditText urlEditText = findViewById(R.id.url_edittext);
4273 TabLayout tabLayout = findViewById(R.id.tablayout);
4274 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4276 // Remove the incorrect lint warnings below that the some of the views might be null.
4277 assert actionBar != null;
4279 // Get a handle for the activity
4280 Activity activity = this;
4282 // Get a handle for the shared preferences.
4283 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4285 // Get the relevant preferences.
4286 boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4288 // Initialize the favorite icon.
4289 nestedScrollWebView.initializeFavoriteIcon();
4291 // Set the app bar scrolling.
4292 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4294 // Allow pinch to zoom.
4295 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4297 // Hide zoom controls.
4298 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4300 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4301 if (Build.VERSION.SDK_INT >= 21) {
4302 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4305 // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
4306 nestedScrollWebView.getSettings().setUseWideViewPort(true);
4308 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4309 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4311 // Explicitly disable geolocation.
4312 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4314 // Create a double-tap gesture detector to toggle full-screen mode.
4315 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4316 // Override `onDoubleTap()`. All other events are handled using the default settings.
4318 public boolean onDoubleTap(MotionEvent event) {
4319 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4320 // Toggle the full screen browsing mode tracker.
4321 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4323 // Toggle the full screen browsing mode.
4324 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4325 // Hide the app bar if specified.
4330 // Hide the banner ad in the free flavor.
4331 if (BuildConfig.FLAVOR.contentEquals("free")) {
4332 AdHelper.hideAd(findViewById(R.id.adview));
4335 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4336 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4338 /* Hide the system bars.
4339 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4340 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4341 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4342 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4344 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4345 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4346 } else { // Switch to normal viewing mode.
4347 // Show the app bar.
4350 // Show the banner ad in the free flavor.
4351 if (BuildConfig.FLAVOR.contentEquals("free")) {
4353 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4356 // Remove the `SYSTEM_UI` flags from the root frame layout.
4357 rootFrameLayout.setSystemUiVisibility(0);
4359 // Add the translucent status flag.
4360 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4363 // Consume the double-tap.
4365 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4371 // Pass all touch events on the WebView through the double-tap gesture detector.
4372 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4373 // Call `performClick()` on the view, which is required for accessibility.
4374 view.performClick();
4376 // Send the event to the gesture detector.
4377 return doubleTapGestureDetector.onTouchEvent(event);
4380 // Register the WebView for a context menu. This is used to see link targets and download images.
4381 registerForContextMenu(nestedScrollWebView);
4383 // Allow the downloading of files.
4384 nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4385 // Check if the download should be processed by an external app.
4386 if (downloadWithExternalApp) { // Download with an external app.
4387 // Create a download intent. Not specifying the action type will display the maximum number of options.
4388 Intent downloadIntent = new Intent();
4390 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4391 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4393 // Flag the intent to open in a new task.
4394 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4396 // Show the chooser.
4397 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4398 } else { // Download with Android's download manager.
4399 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4400 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4401 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4403 // Store the variables for future use by `onRequestPermissionsResult()`.
4405 downloadContentDisposition = contentDisposition;
4406 downloadContentLength = contentLength;
4408 // Show a dialog if the user has previously denied the permission.
4409 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4410 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4411 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4413 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4414 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4415 } else { // Show the permission request directly.
4416 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4417 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4419 } else { // The storage permission has already been granted.
4420 // Get a handle for the download file alert dialog.
4421 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4423 // Show the download file alert dialog.
4424 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4429 // Update the find on page count.
4430 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4431 // Get a handle for `findOnPageCountTextView`.
4432 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4435 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4436 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4437 // Set `findOnPageCountTextView` to `0/0`.
4438 findOnPageCountTextView.setText(R.string.zero_of_zero);
4439 } else if (isDoneCounting) { // There are matches.
4440 // `activeMatchOrdinal` is zero-based.
4441 int activeMatch = activeMatchOrdinal + 1;
4443 // Build the match string.
4444 String matchString = activeMatch + "/" + numberOfMatches;
4446 // Set `findOnPageCountTextView`.
4447 findOnPageCountTextView.setText(matchString);
4452 if (Build.VERSION.SDK_INT >= 23) {
4453 nestedScrollWebView.setOnScrollChangeListener((View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) -> {
4454 // Update the status of swipe to refresh if it is enabled.
4455 if (nestedScrollWebView.getSwipeToRefresh()) {
4456 // Only enable swipe to refresh if the WebView is scrolled to the top.
4457 swipeRefreshLayout.setEnabled(scrollY == 0);
4462 // Set the web chrome client.
4463 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4464 // Update the progress bar when a page is loading.
4466 public void onProgressChanged(WebView view, int progress) {
4467 // Inject the night mode CSS if night mode is enabled.
4468 if (nestedScrollWebView.getNightMode()) {
4469 // `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
4470 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
4471 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
4472 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4473 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4474 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4475 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4476 // Initialize a handler to display `mainWebView`.
4477 Handler displayWebViewHandler = new Handler();
4479 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4480 Runnable displayWebViewRunnable = () -> {
4481 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
4482 if (progressBar.getVisibility() == View.GONE) {
4483 nestedScrollWebView.setVisibility(View.VISIBLE);
4487 // Displaying of `mainWebView` after 500 milliseconds.
4488 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4492 // Update the progress bar.
4493 progressBar.setProgress(progress);
4495 // Set the visibility of the progress bar.
4496 if (progress < 100) {
4497 // Show the progress bar.
4498 progressBar.setVisibility(View.VISIBLE);
4500 // Hide the progress bar.
4501 progressBar.setVisibility(View.GONE);
4503 // Display the nested scroll WebView if night mode is disabled.
4504 // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
4505 // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
4506 if (!nestedScrollWebView.getNightMode()) {
4507 nestedScrollWebView.setVisibility(View.VISIBLE);
4510 //Stop the swipe to refresh indicator if it is running
4511 swipeRefreshLayout.setRefreshing(false);
4515 // Set the favorite icon when it changes.
4517 public void onReceivedIcon(WebView view, Bitmap icon) {
4518 // Only update the favorite icon if the website has finished loading.
4519 if (progressBar.getVisibility() == View.GONE) {
4520 // Store the new favorite icon.
4521 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
4523 // Get the current page position.
4524 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4526 // Get the current tab.
4527 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4529 // Remove the lint warning below that the current tab might be null.
4532 // Get the custom view from the tab.
4533 View tabView = tab.getCustomView();
4535 // Remove the incorrect warning below that the current tab view might be null.
4536 assert tabView != null;
4538 // Get the favorite icon image view from the tab.
4539 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
4541 // Display the favorite icon in the tab.
4542 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4546 // Save a copy of the title when it changes.
4548 public void onReceivedTitle(WebView view, String title) {
4549 // Get the current page position.
4550 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4552 // Get the current tab.
4553 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4555 // Only populate the title text view if the tab has been fully created.
4557 // Get the custom view from the tab.
4558 View tabView = tab.getCustomView();
4560 // Remove the incorrect warning below that the current tab view might be null.
4561 assert tabView != null;
4563 // Get the title text view from the tab.
4564 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
4566 // Set the title as the tab text.
4567 tabTitleTextView.setText(title);
4571 // Enter full screen video.
4573 public void onShowCustomView(View video, CustomViewCallback callback) {
4574 // Get a handle for the full screen video frame layout.
4575 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4577 // Set the full screen video flag.
4578 displayingFullScreenVideo = true;
4580 // Pause the ad if this is the free flavor.
4581 if (BuildConfig.FLAVOR.contentEquals("free")) {
4582 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4583 AdHelper.pauseAd(findViewById(R.id.adview));
4586 // Hide the keyboard.
4587 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4589 // Hide the main content relative layout.
4590 mainContentRelativeLayout.setVisibility(View.GONE);
4592 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4593 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4595 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4596 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4598 /* Hide the system bars.
4599 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4600 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4601 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4602 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4604 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4605 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4607 // Disable the sliding drawers.
4608 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4610 // Add the video view to the full screen video frame layout.
4611 fullScreenVideoFrameLayout.addView(video);
4613 // Show the full screen video frame layout.
4614 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4617 // Exit full screen video.
4619 public void onHideCustomView() {
4620 // Get a handle for the full screen video frame layout.
4621 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
4623 // Unset the full screen video flag.
4624 displayingFullScreenVideo = false;
4626 // Remove all the views from the full screen video frame layout.
4627 fullScreenVideoFrameLayout.removeAllViews();
4629 // Hide the full screen video frame layout.
4630 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4632 // Enable the sliding drawers.
4633 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4635 // Show the main content relative layout.
4636 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4638 // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4639 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4640 // Hide the app bar if specified.
4645 // Hide the banner ad in the free flavor.
4646 if (BuildConfig.FLAVOR.contentEquals("free")) {
4647 AdHelper.hideAd(findViewById(R.id.adview));
4650 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4651 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4653 /* Hide the system bars.
4654 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4655 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4656 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4657 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4659 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4660 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4661 } else { // Switch to normal viewing mode.
4662 // Remove the `SYSTEM_UI` flags from the root frame layout.
4663 rootFrameLayout.setSystemUiVisibility(0);
4665 // Add the translucent status flag.
4666 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4669 // Reload the ad for the free flavor if not in full screen mode.
4670 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4672 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4678 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4679 // Show the file chooser if the device is running API >= 21.
4680 if (Build.VERSION.SDK_INT >= 21) {
4681 // Store the file path callback.
4682 fileChooserCallback = filePathCallback;
4684 // Create an intent to open a chooser based ont the file chooser parameters.
4685 Intent fileChooserIntent = fileChooserParams.createIntent();
4687 // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4688 startActivityForResult(fileChooserIntent, 0);
4694 nestedScrollWebView.setWebViewClient(new WebViewClient() {
4695 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4696 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4697 @SuppressWarnings("deprecation")
4699 public boolean shouldOverrideUrlLoading(WebView view, String url) {
4700 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
4701 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
4702 formattedUrlString = "";
4704 // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
4705 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
4707 // Check if the user agent has changed.
4708 if (userAgentChanged) {
4709 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
4710 nestedScrollWebView.loadUrl(url, customHeaders);
4712 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4715 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4718 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
4719 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4720 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4722 // Parse the url and set it as the data for the intent.
4723 emailIntent.setData(Uri.parse(url));
4725 // Open the email program in a new task instead of as part of Privacy Browser.
4726 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4729 startActivity(emailIntent);
4731 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4733 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
4734 // Open the dialer and load the phone number, but wait for the user to place the call.
4735 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4737 // Add the phone number to the intent.
4738 dialIntent.setData(Uri.parse(url));
4740 // Open the dialer in a new task instead of as part of Privacy Browser.
4741 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4744 startActivity(dialIntent);
4746 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4748 } else { // Load a system chooser to select an app that can handle the URL.
4749 // Open an app that can handle the URL.
4750 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4752 // Add the URL to the intent.
4753 genericIntent.setData(Uri.parse(url));
4755 // List all apps that can handle the URL instead of just opening the first one.
4756 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4758 // Open the app in a new task instead of as part of Privacy Browser.
4759 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4761 // Start the app or display a snackbar if no app is available to handle the URL.
4763 startActivity(genericIntent);
4764 } catch (ActivityNotFoundException exception) {
4765 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
4768 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4773 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4774 @SuppressWarnings("deprecation")
4776 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4777 // Get a handle for the navigation view.
4778 NavigationView navigationView = findViewById(R.id.navigationview);
4780 // Get a handle for the navigation menu.
4781 Menu navigationMenu = navigationView.getMenu();
4783 // Get a handle for the navigation requests menu item. The menu is 0 based.
4784 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
4786 // Create an empty web resource response to be used if the resource request is blocked.
4787 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4789 // Reset the whitelist results tracker.
4790 String[] whitelistResultStringArray = null;
4792 // Initialize the third party request tracker.
4793 boolean isThirdPartyRequest = false;
4795 // Initialize the current domain string.
4796 String currentDomain = "";
4798 // Nobody is happy when comparing null strings.
4799 if (!(formattedUrlString == null) && !(url == null)) {
4800 // Get the domain strings to URIs.
4801 Uri currentDomainUri = Uri.parse(formattedUrlString);
4802 Uri requestDomainUri = Uri.parse(url);
4804 // Get the domain host names.
4805 String currentBaseDomain = currentDomainUri.getHost();
4806 String requestBaseDomain = requestDomainUri.getHost();
4808 // Update the current domain variable.
4809 currentDomain = currentBaseDomain;
4811 // Only compare the current base domain and the request base domain if neither is null.
4812 if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
4813 // Determine the current base domain.
4814 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4815 // Remove the first subdomain.
4816 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4819 // Determine the request base domain.
4820 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4821 // Remove the first subdomain.
4822 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4825 // Update the third party request tracker.
4826 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4830 // Get the current WebView page position.
4831 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4833 // Determine if the WebView is currently displayed.
4834 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
4836 // Block third-party requests if enabled.
4837 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
4838 // Add the result to the resource requests.
4839 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_THIRD_PARTY, url});
4841 // Increment the blocked requests counters.
4842 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4843 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
4845 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4846 if (webViewDisplayed) {
4847 // Updating the UI must be run from the UI thread.
4848 activity.runOnUiThread(() -> {
4849 // Update the menu item titles.
4850 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4851 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4852 blockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
4853 getString(R.string.block_all_third_party_requests));
4857 // Return an empty web resource response.
4858 return emptyWebResourceResponse;
4861 // Check UltraPrivacy if it is enabled.
4862 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)) {
4863 // Check the URL against UltraPrivacy.
4864 String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
4866 // Process the UltraPrivacy results.
4867 if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
4868 // Add the result to the resource requests.
4869 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4870 ultraPrivacyResults[5]});
4872 // Increment the blocked requests counters.
4873 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4874 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY);
4876 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4877 if (webViewDisplayed) {
4878 // Updating the UI must be run from the UI thread.
4879 activity.runOnUiThread(() -> {
4880 // Update the menu item titles.
4881 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4882 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4883 ultraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
4887 // The resource request was blocked. Return an empty web resource response.
4888 return emptyWebResourceResponse;
4889 } else if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
4890 // Add a whitelist entry to the resource requests array.
4891 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4892 ultraPrivacyResults[5]});
4894 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
4899 // Check EasyList if it is enabled.
4900 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)) {
4901 // Check the URL against EasyList.
4902 String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
4904 // Process the EasyList results.
4905 if (easyListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
4906 // Add the result to the resource requests.
4907 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
4909 // Increment the blocked requests counters.
4910 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4911 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_LIST);
4913 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4914 if (webViewDisplayed) {
4915 // Updating the UI must be run from the UI thread.
4916 activity.runOnUiThread(() -> {
4917 // Update the menu item titles.
4918 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4919 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4920 easyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
4924 // The resource request was blocked. Return an empty web resource response.
4925 return emptyWebResourceResponse;
4926 } else if (easyListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
4927 // Update the whitelist result string array tracker.
4928 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
4932 // Check EasyPrivacy if it is enabled.
4933 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)) {
4934 // Check the URL against EasyPrivacy.
4935 String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
4937 // Process the EasyPrivacy results.
4938 if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
4939 // Add the result to the resource requests.
4940 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
4941 easyPrivacyResults[5]});
4943 // Increment the blocked requests counters.
4944 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4945 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY);
4947 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4948 if (webViewDisplayed) {
4949 // Updating the UI must be run from the UI thread.
4950 activity.runOnUiThread(() -> {
4951 // Update the menu item titles.
4952 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4953 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4954 easyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
4958 // The resource request was blocked. Return an empty web resource response.
4959 return emptyWebResourceResponse;
4960 } else if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
4961 // Update the whitelist result string array tracker.
4962 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
4966 // Check Fanboy’s Annoyance List if it is enabled.
4967 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
4968 // Check the URL against Fanboy's Annoyance List.
4969 String[] fanboysAnnoyanceListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
4971 // Process the Fanboy's Annoyance List results.
4972 if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
4973 // Add the result to the resource requests.
4974 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4975 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
4977 // Increment the blocked requests counters.
4978 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4979 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
4981 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4982 if (webViewDisplayed) {
4983 // Updating the UI must be run from the UI thread.
4984 activity.runOnUiThread(() -> {
4985 // Update the menu item titles.
4986 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4987 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4988 fanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
4989 getString(R.string.fanboys_annoyance_list));
4993 // The resource request was blocked. Return an empty web resource response.
4994 return emptyWebResourceResponse;
4995 } else if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
4996 // Update the whitelist result string array tracker.
4997 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4998 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5000 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5001 // Check the URL against Fanboy's Annoyance List.
5002 String[] fanboysSocialListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5004 // Process the Fanboy's Social Blocking List results.
5005 if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5006 // Add the result to the resource requests.
5007 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5008 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5010 // Increment the blocked requests counters.
5011 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5012 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5014 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5015 if (webViewDisplayed) {
5016 // Updating the UI must be run from the UI thread.
5017 activity.runOnUiThread(() -> {
5018 // Update the menu item titles.
5019 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5020 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5021 fanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5022 getString(R.string.fanboys_social_blocking_list));
5026 // The resource request was blocked. Return an empty web resource response.
5027 return emptyWebResourceResponse;
5028 } else if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5029 // Update the whitelist result string array tracker.
5030 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5031 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5035 // Add the request to the log because it hasn't been processed by any of the previous checks.
5036 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5037 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5038 } else { // The request didn't match any blocklist entry. Log it as a default request.
5039 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_DEFAULT, url});
5042 // The resource request has not been blocked. `return null` loads the requested resource.
5046 // Handle HTTP authentication requests.
5048 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5049 // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`.
5050 httpAuthHandler = handler;
5052 // Display the HTTP authentication dialog.
5053 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
5054 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5058 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5059 // Reset the list of resource requests.
5060 nestedScrollWebView.clearResourceRequests();
5062 // Reset the requests counters.
5063 nestedScrollWebView.resetRequestsCounters();
5065 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5066 if (nestedScrollWebView.getNightMode()) {
5067 nestedScrollWebView.setVisibility(View.INVISIBLE);
5070 // Hide the keyboard.
5071 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5073 // Check to see if Privacy Browser is waiting on Orbot.
5074 if (!waitingForOrbot) { // Process the URL.
5075 // 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.
5076 formattedUrlString = url;
5078 // Display the formatted URL text.
5079 urlEditText.setText(formattedUrlString);
5081 // Apply text highlighting to `urlTextBox`.
5084 // Get a URI for the current URL.
5085 Uri currentUri = Uri.parse(formattedUrlString);
5087 // Reset the list of host IP addresses.
5088 nestedScrollWebView.clearCurrentIpAddresses();
5090 // Get the IP addresses for the host.
5091 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5093 // Apply any custom domain settings if the URL was loaded by navigating history.
5094 if (navigatingHistory) {
5095 // Apply the domain settings.
5096 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5098 // Reset `navigatingHistory`.
5099 navigatingHistory = false;
5101 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5102 if (userAgentChanged) {
5103 loadUrl(formattedUrlString);
5107 // Replace Refresh with Stop if the options menu has been created. (The WebView typically begins loading before the menu items are instantiated.)
5108 if (optionsMenu != null) {
5109 // Get a handle for the refresh menu item.
5110 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5113 refreshMenuItem.setTitle(R.string.stop);
5115 // If the icon is displayed in the AppBar, set it according to the theme.
5116 if (displayAdditionalAppBarIcons) {
5118 refreshMenuItem.setIcon(R.drawable.close_dark);
5120 refreshMenuItem.setIcon(R.drawable.close_light);
5127 // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
5129 public void onPageFinished(WebView view, String url) {
5130 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5131 if (!waitingForOrbot) {
5132 // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5133 nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5136 // Flush any cookies to persistent storage. `CookieManager` has become very lazy about flushing cookies in recent versions.
5137 if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) {
5138 CookieManager.getInstance().flush();
5141 // Update the Refresh menu item if the options menu has been created.
5142 if (optionsMenu != null) {
5143 // Get a handle for the refresh menu item.
5144 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5146 // Reset the Refresh title.
5147 refreshMenuItem.setTitle(R.string.refresh);
5149 // If the icon is displayed in the AppBar, reset it according to the theme.
5150 if (displayAdditionalAppBarIcons) {
5152 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5154 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5160 // Clear the cache and history if Incognito Mode is enabled.
5161 if (incognitoModeEnabled) {
5162 // Clear the cache. `true` includes disk files.
5163 nestedScrollWebView.clearCache(true);
5165 // Clear the back/forward history.
5166 nestedScrollWebView.clearHistory();
5168 // Manually delete cache folders.
5170 // Delete the main cache directory.
5171 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5173 // Delete the secondary `Service Worker` cache directory.
5174 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5175 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5176 } catch (IOException e) {
5177 // Do nothing if an error is thrown.
5181 // Update the URL text box and apply domain settings if not waiting on Orbot.
5182 if (!waitingForOrbot) {
5183 // Check to see if `WebView` has set `url` to be `about:blank`.
5184 if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
5185 // Set `formattedUrlString` to `""`.
5186 formattedUrlString = "";
5188 // Display the hint in the URL edit text.
5189 urlEditText.setText("");
5191 // Request focus for `urlTextBox`.
5192 urlEditText.requestFocus();
5194 // Display the keyboard.
5195 inputMethodManager.showSoftInput(urlEditText, 0);
5197 // Hide the WebView, which causes the default background color to be displayed according to the theme.
5198 nestedScrollWebView.setVisibility(View.GONE);
5200 // Apply the domain settings. This clears any settings from the previous domain.
5201 applyDomainSettings(nestedScrollWebView, formattedUrlString, true, false);
5202 } else { // `WebView` has loaded a webpage.
5203 // 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.
5204 formattedUrlString = nestedScrollWebView.getUrl();
5206 // Only update the URL text box if the user is not typing in it.
5207 if (!urlEditText.hasFocus()) {
5208 // Display the formatted URL text.
5209 urlEditText.setText(formattedUrlString);
5211 // Apply text highlighting to `urlTextBox`.
5216 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5217 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5218 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5219 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5224 // Handle SSL Certificate errors.
5226 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5227 // Get the current website SSL certificate.
5228 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5230 // Extract the individual pieces of information from the current website SSL certificate.
5231 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5232 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5233 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5234 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5235 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5236 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5237 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5238 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5240 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5241 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5242 // Get the pinned SSL certificate.
5243 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5245 // Extract the arrays from the array list.
5246 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5247 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5249 // Check if the current SSL certificate matches the pinned certificate.
5250 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5251 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5252 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5253 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5255 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5258 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5259 // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
5260 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.
5262 // Display the SSL error `AlertDialog`.
5263 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
5264 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5269 // Check to see if this is the first page.
5270 if (pageNumber == 0) {
5271 // Set this nested scroll WebView as the current WebView.
5272 currentWebView = nestedScrollWebView;
5274 // Apply the app settings from the shared preferences.
5277 // Load the website if not waiting for Orbot to connect.
5278 if (!waitingForOrbot) {
5279 loadUrl(formattedUrlString);