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.Dialog;
28 import android.app.DownloadManager;
29 import android.app.SearchManager;
30 import android.content.ActivityNotFoundException;
31 import android.content.BroadcastReceiver;
32 import android.content.ClipData;
33 import android.content.ClipboardManager;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.SharedPreferences;
38 import android.content.pm.PackageManager;
39 import android.content.res.Configuration;
40 import android.database.Cursor;
41 import android.graphics.Bitmap;
42 import android.graphics.BitmapFactory;
43 import android.graphics.Typeface;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.net.http.SslCertificate;
48 import android.net.http.SslError;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.os.Message;
54 import android.preference.PreferenceManager;
55 import android.print.PrintDocumentAdapter;
56 import android.print.PrintManager;
57 import android.text.Editable;
58 import android.text.Spanned;
59 import android.text.TextWatcher;
60 import android.text.style.ForegroundColorSpan;
61 import android.util.Patterns;
62 import android.view.ContextMenu;
63 import android.view.GestureDetector;
64 import android.view.KeyEvent;
65 import android.view.Menu;
66 import android.view.MenuItem;
67 import android.view.MotionEvent;
68 import android.view.View;
69 import android.view.ViewGroup;
70 import android.view.WindowManager;
71 import android.view.inputmethod.InputMethodManager;
72 import android.webkit.CookieManager;
73 import android.webkit.HttpAuthHandler;
74 import android.webkit.SslErrorHandler;
75 import android.webkit.ValueCallback;
76 import android.webkit.WebChromeClient;
77 import android.webkit.WebResourceResponse;
78 import android.webkit.WebSettings;
79 import android.webkit.WebStorage;
80 import android.webkit.WebView;
81 import android.webkit.WebViewClient;
82 import android.webkit.WebViewDatabase;
83 import android.widget.ArrayAdapter;
84 import android.widget.CursorAdapter;
85 import android.widget.EditText;
86 import android.widget.FrameLayout;
87 import android.widget.ImageView;
88 import android.widget.LinearLayout;
89 import android.widget.ListView;
90 import android.widget.ProgressBar;
91 import android.widget.RadioButton;
92 import android.widget.RelativeLayout;
93 import android.widget.TextView;
95 import androidx.annotation.NonNull;
96 import androidx.appcompat.app.ActionBar;
97 import androidx.appcompat.app.ActionBarDrawerToggle;
98 import androidx.appcompat.app.AppCompatActivity;
99 import androidx.appcompat.widget.Toolbar;
100 import androidx.coordinatorlayout.widget.CoordinatorLayout;
101 import androidx.core.app.ActivityCompat;
102 import androidx.core.content.ContextCompat;
103 import androidx.core.view.GravityCompat;
104 import androidx.drawerlayout.widget.DrawerLayout;
105 import androidx.fragment.app.DialogFragment;
106 import androidx.fragment.app.FragmentManager;
107 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
108 import androidx.viewpager.widget.ViewPager;
110 import com.google.android.material.appbar.AppBarLayout;
111 import com.google.android.material.floatingactionbutton.FloatingActionButton;
112 import com.google.android.material.navigation.NavigationView;
113 import com.google.android.material.snackbar.Snackbar;
114 import com.google.android.material.tabs.TabLayout;
116 import com.stoutner.privacybrowser.BuildConfig;
117 import com.stoutner.privacybrowser.R;
118 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
119 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
120 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
121 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
122 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
123 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
124 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
125 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
126 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
127 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
128 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
129 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
130 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
131 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
132 import com.stoutner.privacybrowser.dialogs.SaveWebpageImageDialog;
133 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
134 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
135 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
136 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
137 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
138 import com.stoutner.privacybrowser.helpers.AdHelper;
139 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
140 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
141 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
142 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
143 import com.stoutner.privacybrowser.helpers.FileNameHelper;
144 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
145 import com.stoutner.privacybrowser.views.NestedScrollWebView;
147 import java.io.ByteArrayInputStream;
148 import java.io.ByteArrayOutputStream;
150 import java.io.IOException;
151 import java.io.UnsupportedEncodingException;
152 import java.net.MalformedURLException;
154 import java.net.URLDecoder;
155 import java.net.URLEncoder;
156 import java.util.ArrayList;
157 import java.util.Date;
158 import java.util.HashMap;
159 import java.util.HashSet;
160 import java.util.List;
161 import java.util.Map;
162 import java.util.Set;
164 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
165 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
166 DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
167 EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageImageDialog.SaveWebpageImageListener,
168 StoragePermissionDialog.StoragePermissionDialogListener, WebViewTabFragment.NewTabListener {
170 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
171 public static String orbotStatus;
173 // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`.
174 public static WebViewPagerAdapter webViewPagerAdapter;
176 // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`. They are used in `onRestart()`.
177 public static boolean loadUrlOnRestart;
178 public static String urlToLoadOnRestart;
180 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
181 public static boolean restartFromBookmarksActivity;
183 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
184 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
185 public static String currentBookmarksFolder;
187 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
188 public final static int UNRECOGNIZED_USER_AGENT = -1;
189 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
190 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
191 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
192 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
193 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
195 // Start activity for result request codes.
196 private final int FILE_UPLOAD_REQUEST_CODE = 0;
197 public final static int BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 1;
200 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
201 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
202 private NestedScrollWebView currentWebView;
204 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
205 private final Map<String, String> customHeaders = new HashMap<>();
207 // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
208 private String searchURL;
210 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
211 private Menu optionsMenu;
213 // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
214 private ArrayList<List<String[]>> easyList;
215 private ArrayList<List<String[]>> easyPrivacy;
216 private ArrayList<List<String[]>> fanboysAnnoyanceList;
217 private ArrayList<List<String[]>> fanboysSocialList;
218 private ArrayList<List<String[]>> ultraList;
219 private ArrayList<List<String[]>> ultraPrivacy;
221 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
222 private String webViewDefaultUserAgent;
224 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
225 private boolean proxyThroughOrbot;
227 // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
228 private boolean incognitoModeEnabled;
230 // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
231 private boolean fullScreenBrowsingModeEnabled;
233 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
234 private boolean inFullScreenBrowsingMode;
236 // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`.
237 private boolean hideAppBar;
238 private boolean scrollAppBar;
240 // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`.
241 private boolean loadingNewIntent;
243 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
244 private boolean reapplyDomainSettingsOnRestart;
246 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
247 private boolean reapplyAppSettingsOnRestart;
249 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
250 private boolean displayingFullScreenVideo;
252 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
253 private BroadcastReceiver orbotStatusBroadcastReceiver;
255 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
256 private boolean waitingForOrbot;
258 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
259 private ActionBarDrawerToggle actionBarDrawerToggle;
261 // The color spans are used in `onCreate()` and `highlightUrlText()`.
262 private ForegroundColorSpan redColorSpan;
263 private ForegroundColorSpan initialGrayColorSpan;
264 private ForegroundColorSpan finalGrayColorSpan;
266 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
267 private int drawerHeaderPaddingLeftAndRight;
268 private int drawerHeaderPaddingTop;
269 private int drawerHeaderPaddingBottom;
271 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
272 // and `loadBookmarksFolder()`.
273 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
275 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
276 private Cursor bookmarksCursor;
278 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
279 private CursorAdapter bookmarksCursorAdapter;
281 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
282 private String oldFolderNameString;
284 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
285 private ValueCallback<Uri[]> fileChooserCallback;
287 // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
288 private int defaultProgressViewStartOffset;
289 private int defaultProgressViewEndOffset;
291 // The swipe refresh layout top padding is used when exiting full screen browsing mode. It is used in an inner class in `initializeWebView()`.
292 private int swipeRefreshLayoutPaddingTop;
294 // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`.
295 private boolean sanitizeGoogleAnalytics;
296 private boolean sanitizeFacebookClickIds;
297 private boolean sanitizeTwitterAmpRedirects;
299 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
300 private String downloadUrl;
301 private String downloadContentDisposition;
302 private long downloadContentLength;
304 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
305 private String downloadImageUrl;
307 // The save website image file path string is used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
308 private String saveWebsiteImageFilePath;
310 // The permission result request codes are used in `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, `onSaveWebpageImage()`,
311 // `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
312 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
313 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
314 private final int SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 3;
317 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
318 @SuppressLint("ClickableViewAccessibility")
319 protected void onCreate(Bundle savedInstanceState) {
320 if (Build.VERSION.SDK_INT >= 21) {
321 WebView.enableSlowWholeDocumentDraw();
324 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
325 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
327 // Get a handle for the shared preferences.
328 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
330 // Get the theme and screenshot preferences.
331 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
332 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
334 // Disable screenshots if not allowed.
335 if (!allowScreenshots) {
336 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
339 // Set the activity theme.
341 setTheme(R.style.PrivacyBrowserDark);
343 setTheme(R.style.PrivacyBrowserLight);
346 // Run the default commands.
347 super.onCreate(savedInstanceState);
349 // Set the content view.
350 setContentView(R.layout.main_framelayout);
352 // Get handles for the views that need to be modified.
353 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
354 Toolbar toolbar = findViewById(R.id.toolbar);
355 ViewPager webViewPager = findViewById(R.id.webviewpager);
357 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
358 setSupportActionBar(toolbar);
360 // Get a handle for the action bar.
361 ActionBar actionBar = getSupportActionBar();
363 // This is needed to get rid of the Android Studio warning that the action bar might be null.
364 assert actionBar != null;
366 // Add the custom layout, which shows the URL text bar.
367 actionBar.setCustomView(R.layout.url_app_bar);
368 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
370 // Initially disable the sliding drawers. They will be enabled once the blocklists are loaded.
371 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
373 // Create the hamburger icon at the start of the AppBar.
374 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
376 // Initialize the web view pager adapter.
377 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
379 // Set the pager adapter on the web view pager.
380 webViewPager.setAdapter(webViewPagerAdapter);
382 // Store up to 100 tabs in memory.
383 webViewPager.setOffscreenPageLimit(100);
385 // Populate the blocklists.
386 new PopulateBlocklists(this, this).execute();
390 protected void onNewIntent(Intent intent) {
391 // Replace the intent that started the app with this one.
394 // Process the intent here if Privacy Browser is fully initialized. If the process has been killed by the system while sitting in the background, this will be handled in `initializeWebView()`.
395 if (ultraPrivacy != null) {
396 // Get the information from the intent.
397 String intentAction = intent.getAction();
398 Uri intentUriData = intent.getData();
400 // Determine if this is a web search.
401 boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
403 // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null.
404 if (intentUriData != null || isWebSearch) {
405 // Get the shared preferences.
406 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
408 // Create a URL string.
411 // If the intent action is a web search, perform the search.
413 // Create an encoded URL string.
414 String encodedUrlString;
416 // Sanitize the search input and convert it to a search.
418 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
419 } catch (UnsupportedEncodingException exception) {
420 encodedUrlString = "";
423 // Add the base search URL.
424 url = searchURL + encodedUrlString;
425 } else { // The intent should contain a URL.
426 // Set the intent data as the URL.
427 url = intentUriData.toString();
430 // Add a new tab if specified in the preferences.
431 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab.
432 // Set the loading new intent flag.
433 loadingNewIntent = true;
437 } else { // Load the URL in the current tab.
442 // Get a handle for the drawer layout.
443 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
445 // Close the navigation drawer if it is open.
446 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
447 drawerLayout.closeDrawer(GravityCompat.START);
450 // Close the bookmarks drawer if it is open.
451 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
452 drawerLayout.closeDrawer(GravityCompat.END);
459 public void onRestart() {
460 // Run the default commands.
463 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
464 if (proxyThroughOrbot) {
465 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
466 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
468 // Send the intent to the Orbot package.
469 orbotIntent.setPackage("org.torproject.android");
472 sendBroadcast(orbotIntent);
475 // Apply the app settings if returning from the Settings activity.
476 if (reapplyAppSettingsOnRestart) {
477 // Reset the reapply app settings on restart tracker.
478 reapplyAppSettingsOnRestart = false;
480 // Apply the app settings.
484 // Apply the domain settings if returning from the settings or domains activity.
485 if (reapplyDomainSettingsOnRestart) {
486 // Reset the reapply domain settings on restart tracker.
487 reapplyDomainSettingsOnRestart = false;
489 // Reapply the domain settings for each tab.
490 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
491 // Get the WebView tab fragment.
492 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
494 // Get the fragment view.
495 View fragmentView = webViewTabFragment.getView();
497 // Only reload the WebViews if they exist.
498 if (fragmentView != null) {
499 // Get the nested scroll WebView from the tab fragment.
500 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
502 // Reset the current domain name so the domain settings will be reapplied.
503 nestedScrollWebView.resetCurrentDomainName();
505 // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
506 if (nestedScrollWebView.getUrl() != null) {
507 applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
513 // Load the URL on restart (used when loading a bookmark).
514 if (loadUrlOnRestart) {
515 // Load the specified URL.
516 loadUrl(urlToLoadOnRestart);
518 // Reset the load on restart tracker.
519 loadUrlOnRestart = false;
522 // Update the bookmarks drawer if returning from the Bookmarks activity.
523 if (restartFromBookmarksActivity) {
524 // Get a handle for the drawer layout.
525 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
527 // Close the bookmarks drawer.
528 drawerLayout.closeDrawer(GravityCompat.END);
530 // Reload the bookmarks drawer.
531 loadBookmarksFolder();
533 // Reset `restartFromBookmarksActivity`.
534 restartFromBookmarksActivity = false;
537 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
538 updatePrivacyIcons(true);
541 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
543 public void onResume() {
544 // Run the default commands.
547 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
548 // Get the WebView tab fragment.
549 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
551 // Get the fragment view.
552 View fragmentView = webViewTabFragment.getView();
554 // Only resume the WebViews if they exist (they won't when the app is first created).
555 if (fragmentView != null) {
556 // Get the nested scroll WebView from the tab fragment.
557 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
559 // Resume the nested scroll WebView JavaScript timers.
560 nestedScrollWebView.resumeTimers();
562 // Resume the nested scroll WebView.
563 nestedScrollWebView.onResume();
567 // Display a message to the user if waiting for Orbot.
568 if (waitingForOrbot && !orbotStatus.equals("ON")) {
569 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
570 currentWebView.getSettings().setUseWideViewPort(false);
572 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
573 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
576 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
577 // Get a handle for the root frame layouts.
578 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
580 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
581 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
583 /* Hide the system bars.
584 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
585 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
586 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
587 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
589 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
590 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
591 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
593 AdHelper.resumeAd(findViewById(R.id.adview));
598 public void onPause() {
599 // Run the default commands.
602 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
603 // Get the WebView tab fragment.
604 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
606 // Get the fragment view.
607 View fragmentView = webViewTabFragment.getView();
609 // Only pause the WebViews if they exist (they won't when the app is first created).
610 if (fragmentView != null) {
611 // Get the nested scroll WebView from the tab fragment.
612 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
614 // Pause the nested scroll WebView.
615 nestedScrollWebView.onPause();
617 // Pause the nested scroll WebView JavaScript timers.
618 nestedScrollWebView.pauseTimers();
622 // Pause the ad or it will continue to consume resources in the background on the free flavor.
623 if (BuildConfig.FLAVOR.contentEquals("free")) {
625 AdHelper.pauseAd(findViewById(R.id.adview));
630 public void onDestroy() {
631 // Unregister the Orbot status broadcast receiver.
632 this.unregisterReceiver(orbotStatusBroadcastReceiver);
634 // Close the bookmarks cursor and database.
635 bookmarksCursor.close();
636 bookmarksDatabaseHelper.close();
638 // Run the default commands.
643 public boolean onCreateOptionsMenu(Menu menu) {
644 // Inflate the menu. This adds items to the action bar if it is present.
645 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
647 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
650 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
651 updatePrivacyIcons(false);
653 // Get handles for the menu items.
654 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
655 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
656 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
657 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
658 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
659 MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
660 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
662 // Only display third-party cookies if API >= 21
663 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
665 // Only display the form data menu items if the API < 26.
666 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
667 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
669 // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
670 clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
672 // Only show Ad Consent if this is the free flavor.
673 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
675 // Get the shared preferences.
676 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
678 // Get the dark theme and app bar preferences..
679 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
680 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
682 // 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.
683 if (displayAdditionalAppBarIcons) {
684 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
685 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
686 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
687 } else { //Do not display the additional icons.
688 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
689 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
690 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
693 // Replace Refresh with Stop if a URL is already loading.
694 if (currentWebView != null && currentWebView.getProgress() != 100) {
696 refreshMenuItem.setTitle(R.string.stop);
698 // If the icon is displayed in the AppBar, set it according to the theme.
699 if (displayAdditionalAppBarIcons) {
701 refreshMenuItem.setIcon(R.drawable.close_dark);
703 refreshMenuItem.setIcon(R.drawable.close_light);
713 public boolean onPrepareOptionsMenu(Menu menu) {
714 // Get handles for the menu items.
715 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
716 MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
717 MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
718 MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
719 MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
720 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
721 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
722 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
723 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
724 MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
725 MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
726 MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
727 MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
728 MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
729 MenuItem ultraListMenuItem = menu.findItem(R.id.ultralist);
730 MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
731 MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
732 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
733 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
734 MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport);
735 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
736 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
737 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
739 // Get a handle for the cookie manager.
740 CookieManager cookieManager = CookieManager.getInstance();
742 // Initialize the current user agent string and the font size.
743 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
746 // 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.
747 if (currentWebView != null) {
748 // Set the add or edit domain text.
749 if (currentWebView.getDomainSettingsApplied()) {
750 addOrEditDomain.setTitle(R.string.edit_domain_settings);
752 addOrEditDomain.setTitle(R.string.add_domain_settings);
755 // Get the current user agent from the WebView.
756 currentUserAgent = currentWebView.getSettings().getUserAgentString();
758 // Get the current font size from the
759 fontSize = currentWebView.getSettings().getTextZoom();
761 // Set the status of the menu item checkboxes.
762 domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
763 saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26.
764 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
765 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
766 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
767 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
768 ultraListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
769 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
770 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
771 swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
772 wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
773 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
774 nightModeMenuItem.setChecked(currentWebView.getNightMode());
776 // Initialize the display names for the blocklists with the number of blocked requests.
777 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
778 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
779 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
780 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
781 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
782 ultraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
783 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
784 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
786 // Only modify third-party cookies if the API >= 21.
787 if (Build.VERSION.SDK_INT >= 21) {
788 // Set the status of the third-party cookies checkbox.
789 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
791 // Enable third-party cookies if first-party cookies are enabled.
792 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
795 // Enable DOM Storage if JavaScript is enabled.
796 domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
799 // Set the status of the menu item checkboxes.
800 firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
801 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
803 // Enable Clear Cookies if there are any.
804 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
806 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
807 String privateDataDirectoryString = getApplicationInfo().dataDir;
809 // Get a count of the number of files in the Local Storage directory.
810 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
811 int localStorageDirectoryNumberOfFiles = 0;
812 if (localStorageDirectory.exists()) {
813 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
816 // Get a count of the number of files in the IndexedDB directory.
817 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
818 int indexedDBDirectoryNumberOfFiles = 0;
819 if (indexedDBDirectory.exists()) {
820 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
823 // Enable Clear DOM Storage if there is any.
824 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
826 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
827 if (Build.VERSION.SDK_INT < 26) {
828 // Get the WebView database.
829 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
831 // Enable the clear form data menu item if there is anything to clear.
832 clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
835 // Enable Clear Data if any of the submenu items are enabled.
836 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
838 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
839 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
841 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
842 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
843 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
844 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
845 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
846 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
847 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
848 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
849 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
850 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
851 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
852 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
853 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
854 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
855 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
856 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
857 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
858 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
859 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
860 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
861 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
862 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
863 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
864 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
865 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
866 } else { // Custom user agent.
867 menu.findItem(R.id.user_agent_custom).setChecked(true);
870 // Instantiate the font size title and the selected font size menu item.
871 String fontSizeTitle;
872 MenuItem selectedFontSizeMenuItem;
874 // Prepare the font size title and current size menu item.
877 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
878 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
882 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
883 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
887 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
888 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
892 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
893 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
897 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
898 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
902 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
903 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
907 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
908 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
912 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
913 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
917 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
918 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
922 // Set the font size title and select the current size menu item.
923 fontSizeMenuItem.setTitle(fontSizeTitle);
924 selectedFontSizeMenuItem.setChecked(true);
926 // Run all the other default commands.
927 super.onPrepareOptionsMenu(menu);
934 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
935 @SuppressLint("SetJavaScriptEnabled")
936 public boolean onOptionsItemSelected(MenuItem menuItem) {
937 // Get the selected menu item ID.
938 int menuItemId = menuItem.getItemId();
940 // Get a handle for the shared preferences.
941 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
943 // Get a handle for the cookie manager.
944 CookieManager cookieManager = CookieManager.getInstance();
946 // Run the commands that correlate to the selected menu item.
947 switch (menuItemId) {
948 case R.id.toggle_javascript:
949 // Toggle the JavaScript status.
950 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
952 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
953 updatePrivacyIcons(true);
955 // Display a `Snackbar`.
956 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
957 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
958 } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled.
959 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
960 } else { // Privacy mode.
961 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
964 // Reload the current WebView.
965 currentWebView.reload();
967 // Consume the event.
970 case R.id.add_or_edit_domain:
971 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
972 // Reapply the domain settings on returning to `MainWebViewActivity`.
973 reapplyDomainSettingsOnRestart = true;
975 // Create an intent to launch the domains activity.
976 Intent domainsIntent = new Intent(this, DomainsActivity.class);
978 // Add the extra information to the intent.
979 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
980 domainsIntent.putExtra("close_on_back", true);
981 domainsIntent.putExtra("current_url", currentWebView.getUrl());
983 // Get the current certificate.
984 SslCertificate sslCertificate = currentWebView.getCertificate();
986 // Check to see if the SSL certificate is populated.
987 if (sslCertificate != null) {
988 // Extract the certificate to strings.
989 String issuedToCName = sslCertificate.getIssuedTo().getCName();
990 String issuedToOName = sslCertificate.getIssuedTo().getOName();
991 String issuedToUName = sslCertificate.getIssuedTo().getUName();
992 String issuedByCName = sslCertificate.getIssuedBy().getCName();
993 String issuedByOName = sslCertificate.getIssuedBy().getOName();
994 String issuedByUName = sslCertificate.getIssuedBy().getUName();
995 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
996 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
998 // Add the certificate to the intent.
999 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1000 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1001 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1002 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1003 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1004 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1005 domainsIntent.putExtra("ssl_start_date", startDateLong);
1006 domainsIntent.putExtra("ssl_end_date", endDateLong);
1009 // Check to see if the current IP addresses have been received.
1010 if (currentWebView.hasCurrentIpAddresses()) {
1011 // Add the current IP addresses to the intent.
1012 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1016 startActivity(domainsIntent);
1017 } else { // Add a new domain.
1018 // Apply the new domain settings on returning to `MainWebViewActivity`.
1019 reapplyDomainSettingsOnRestart = true;
1021 // Get the current domain
1022 Uri currentUri = Uri.parse(currentWebView.getUrl());
1023 String currentDomain = currentUri.getHost();
1025 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1026 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1028 // Create the domain and store the database ID.
1029 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1031 // Create an intent to launch the domains activity.
1032 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1034 // Add the extra information to the intent.
1035 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1036 domainsIntent.putExtra("close_on_back", true);
1037 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1039 // Get the current certificate.
1040 SslCertificate sslCertificate = currentWebView.getCertificate();
1042 // Check to see if the SSL certificate is populated.
1043 if (sslCertificate != null) {
1044 // Extract the certificate to strings.
1045 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1046 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1047 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1048 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1049 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1050 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1051 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1052 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1054 // Add the certificate to the intent.
1055 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1056 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1057 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1058 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1059 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1060 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1061 domainsIntent.putExtra("ssl_start_date", startDateLong);
1062 domainsIntent.putExtra("ssl_end_date", endDateLong);
1065 // Check to see if the current IP addresses have been received.
1066 if (currentWebView.hasCurrentIpAddresses()) {
1067 // Add the current IP addresses to the intent.
1068 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1072 startActivity(domainsIntent);
1075 // Consume the event.
1078 case R.id.toggle_first_party_cookies:
1079 // Switch the first-party cookie status.
1080 cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1082 // Store the first-party cookie status.
1083 currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1085 // Update the menu checkbox.
1086 menuItem.setChecked(cookieManager.acceptCookie());
1088 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1089 updatePrivacyIcons(true);
1091 // Display a snackbar.
1092 if (cookieManager.acceptCookie()) { // First-party cookies are enabled.
1093 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1094 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1095 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1096 } else { // Privacy mode.
1097 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1100 // Reload the current WebView.
1101 currentWebView.reload();
1103 // Consume the event.
1106 case R.id.toggle_third_party_cookies:
1107 if (Build.VERSION.SDK_INT >= 21) {
1108 // Switch the status of thirdPartyCookiesEnabled.
1109 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1111 // Update the menu checkbox.
1112 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1114 // Display a snackbar.
1115 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1116 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1118 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1121 // Reload the current WebView.
1122 currentWebView.reload();
1123 } // Else do nothing because SDK < 21.
1125 // Consume the event.
1128 case R.id.toggle_dom_storage:
1129 // Toggle the status of domStorageEnabled.
1130 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1132 // Update the menu checkbox.
1133 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1135 // Update the privacy icon. `true` refreshes the app bar icons.
1136 updatePrivacyIcons(true);
1138 // Display a snackbar.
1139 if (currentWebView.getSettings().getDomStorageEnabled()) {
1140 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1142 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1145 // Reload the current WebView.
1146 currentWebView.reload();
1148 // Consume the event.
1151 // Form data can be removed once the minimum API >= 26.
1152 case R.id.toggle_save_form_data:
1153 // Switch the status of saveFormDataEnabled.
1154 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1156 // Update the menu checkbox.
1157 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1159 // Display a snackbar.
1160 if (currentWebView.getSettings().getSaveFormData()) {
1161 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1163 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1166 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1167 updatePrivacyIcons(true);
1169 // Reload the current WebView.
1170 currentWebView.reload();
1172 // Consume the event.
1175 case R.id.clear_cookies:
1176 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1177 .setAction(R.string.undo, v -> {
1178 // Do nothing because everything will be handled by `onDismissed()` below.
1180 .addCallback(new Snackbar.Callback() {
1181 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1183 public void onDismissed(Snackbar snackbar, int event) {
1184 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1185 // Delete the cookies, which command varies by SDK.
1186 if (Build.VERSION.SDK_INT < 21) {
1187 cookieManager.removeAllCookie();
1189 cookieManager.removeAllCookies(null);
1196 // Consume the event.
1199 case R.id.clear_dom_storage:
1200 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1201 .setAction(R.string.undo, v -> {
1202 // Do nothing because everything will be handled by `onDismissed()` below.
1204 .addCallback(new Snackbar.Callback() {
1205 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1207 public void onDismissed(Snackbar snackbar, int event) {
1208 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1209 // Delete the DOM Storage.
1210 WebStorage webStorage = WebStorage.getInstance();
1211 webStorage.deleteAllData();
1213 // Initialize a handler to manually delete the DOM storage files and directories.
1214 Handler deleteDomStorageHandler = new Handler();
1216 // Setup a runnable to manually delete the DOM storage files and directories.
1217 Runnable deleteDomStorageRunnable = () -> {
1219 // Get a handle for the runtime.
1220 Runtime runtime = Runtime.getRuntime();
1222 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1223 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1224 String privateDataDirectoryString = getApplicationInfo().dataDir;
1226 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1227 Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1229 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1230 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1231 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1232 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1233 Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1235 // Wait for the processes to finish.
1236 deleteLocalStorageProcess.waitFor();
1237 deleteIndexProcess.waitFor();
1238 deleteQuotaManagerProcess.waitFor();
1239 deleteQuotaManagerJournalProcess.waitFor();
1240 deleteDatabasesProcess.waitFor();
1241 } catch (Exception exception) {
1242 // Do nothing if an error is thrown.
1246 // Manually delete the DOM storage files after 200 milliseconds.
1247 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1253 // Consume the event.
1256 // Form data can be remove once the minimum API >= 26.
1257 case R.id.clear_form_data:
1258 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1259 .setAction(R.string.undo, v -> {
1260 // Do nothing because everything will be handled by `onDismissed()` below.
1262 .addCallback(new Snackbar.Callback() {
1263 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1265 public void onDismissed(Snackbar snackbar, int event) {
1266 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1267 // Delete the form data.
1268 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1269 mainWebViewDatabase.clearFormData();
1275 // Consume the event.
1279 // Toggle the EasyList status.
1280 currentWebView.enableBlocklist(NestedScrollWebView.EASYLIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1282 // Update the menu checkbox.
1283 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1285 // Reload the current WebView.
1286 currentWebView.reload();
1288 // Consume the event.
1291 case R.id.easyprivacy:
1292 // Toggle the EasyPrivacy status.
1293 currentWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1295 // Update the menu checkbox.
1296 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1298 // Reload the current WebView.
1299 currentWebView.reload();
1301 // Consume the event.
1304 case R.id.fanboys_annoyance_list:
1305 // Toggle Fanboy's Annoyance List status.
1306 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1308 // Update the menu checkbox.
1309 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1311 // Update the staus of Fanboy's Social Blocking List.
1312 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1313 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1315 // Reload the current WebView.
1316 currentWebView.reload();
1318 // Consume the event.
1321 case R.id.fanboys_social_blocking_list:
1322 // Toggle Fanboy's Social Blocking List status.
1323 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1325 // Update the menu checkbox.
1326 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1328 // Reload the current WebView.
1329 currentWebView.reload();
1331 // Consume the event.
1334 case R.id.ultralist:
1335 // Toggle the UltraList status.
1336 currentWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1338 // Update the menu checkbox.
1339 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1341 // Reload the current WebView.
1342 currentWebView.reload();
1344 // Consume the event.
1347 case R.id.ultraprivacy:
1348 // Toggle the UltraPrivacy status.
1349 currentWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1351 // Update the menu checkbox.
1352 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1354 // Reload the current WebView.
1355 currentWebView.reload();
1357 // Consume the event.
1360 case R.id.block_all_third_party_requests:
1361 //Toggle the third-party requests blocker status.
1362 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1364 // Update the menu checkbox.
1365 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1367 // Reload the current WebView.
1368 currentWebView.reload();
1370 // Consume the event.
1373 case R.id.user_agent_privacy_browser:
1374 // Update the user agent.
1375 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1377 // Reload the current WebView.
1378 currentWebView.reload();
1380 // Consume the event.
1383 case R.id.user_agent_webview_default:
1384 // Update the user agent.
1385 currentWebView.getSettings().setUserAgentString("");
1387 // Reload the current WebView.
1388 currentWebView.reload();
1390 // Consume the event.
1393 case R.id.user_agent_firefox_on_android:
1394 // Update the user agent.
1395 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1397 // Reload the current WebView.
1398 currentWebView.reload();
1400 // Consume the event.
1403 case R.id.user_agent_chrome_on_android:
1404 // Update the user agent.
1405 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1407 // Reload the current WebView.
1408 currentWebView.reload();
1410 // Consume the event.
1413 case R.id.user_agent_safari_on_ios:
1414 // Update the user agent.
1415 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1417 // Reload the current WebView.
1418 currentWebView.reload();
1420 // Consume the event.
1423 case R.id.user_agent_firefox_on_linux:
1424 // Update the user agent.
1425 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1427 // Reload the current WebView.
1428 currentWebView.reload();
1430 // Consume the event.
1433 case R.id.user_agent_chromium_on_linux:
1434 // Update the user agent.
1435 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1437 // Reload the current WebView.
1438 currentWebView.reload();
1440 // Consume the event.
1443 case R.id.user_agent_firefox_on_windows:
1444 // Update the user agent.
1445 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1447 // Reload the current WebView.
1448 currentWebView.reload();
1450 // Consume the event.
1453 case R.id.user_agent_chrome_on_windows:
1454 // Update the user agent.
1455 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1457 // Reload the current WebView.
1458 currentWebView.reload();
1460 // Consume the event.
1463 case R.id.user_agent_edge_on_windows:
1464 // Update the user agent.
1465 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1467 // Reload the current WebView.
1468 currentWebView.reload();
1470 // Consume the event.
1473 case R.id.user_agent_internet_explorer_on_windows:
1474 // Update the user agent.
1475 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1477 // Reload the current WebView.
1478 currentWebView.reload();
1480 // Consume the event.
1483 case R.id.user_agent_safari_on_macos:
1484 // Update the user agent.
1485 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1487 // Reload the current WebView.
1488 currentWebView.reload();
1490 // Consume the event.
1493 case R.id.user_agent_custom:
1494 // Update the user agent.
1495 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1497 // Reload the current WebView.
1498 currentWebView.reload();
1500 // Consume the event.
1503 case R.id.font_size_twenty_five_percent:
1504 // Set the font size.
1505 currentWebView.getSettings().setTextZoom(25);
1507 // Consume the event.
1510 case R.id.font_size_fifty_percent:
1511 // Set the font size.
1512 currentWebView.getSettings().setTextZoom(50);
1514 // Consume the event.
1517 case R.id.font_size_seventy_five_percent:
1518 // Set the font size.
1519 currentWebView.getSettings().setTextZoom(75);
1521 // Consume the event.
1524 case R.id.font_size_one_hundred_percent:
1525 // Set the font size.
1526 currentWebView.getSettings().setTextZoom(100);
1528 // Consume the event.
1531 case R.id.font_size_one_hundred_twenty_five_percent:
1532 // Set the font size.
1533 currentWebView.getSettings().setTextZoom(125);
1535 // Consume the event.
1538 case R.id.font_size_one_hundred_fifty_percent:
1539 // Set the font size.
1540 currentWebView.getSettings().setTextZoom(150);
1542 // Consume the event.
1545 case R.id.font_size_one_hundred_seventy_five_percent:
1546 // Set the font size.
1547 currentWebView.getSettings().setTextZoom(175);
1549 // Consume the event.
1552 case R.id.font_size_two_hundred_percent:
1553 // Set the font size.
1554 currentWebView.getSettings().setTextZoom(200);
1556 // Consume the event.
1559 case R.id.swipe_to_refresh:
1560 // Toggle the stored status of swipe to refresh.
1561 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1563 // Get a handle for the swipe refresh layout.
1564 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1566 // Update the swipe refresh layout.
1567 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
1568 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
1569 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1570 } else { // Swipe to refresh is disabled.
1571 // Disable the swipe refresh layout.
1572 swipeRefreshLayout.setEnabled(false);
1575 // Consume the event.
1578 case R.id.wide_viewport:
1579 // Toggle the viewport.
1580 currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1582 // Consume the event.
1585 case R.id.display_images:
1586 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1587 // Disable loading of images.
1588 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1590 // Reload the website to remove existing images.
1591 currentWebView.reload();
1592 } else { // Images are not currently loaded automatically.
1593 // Enable loading of images. Missing images will be loaded without the need for a reload.
1594 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1597 // Consume the event.
1600 case R.id.night_mode:
1601 // Toggle night mode.
1602 currentWebView.setNightMode(!currentWebView.getNightMode());
1604 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1605 if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript.
1606 // Enable JavaScript.
1607 currentWebView.getSettings().setJavaScriptEnabled(true);
1608 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
1609 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1610 currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
1611 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
1612 // Apply the JavaScript preference.
1613 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1616 // Update the privacy icons.
1617 updatePrivacyIcons(false);
1619 // Reload the website.
1620 currentWebView.reload();
1622 // Consume the event.
1625 case R.id.find_on_page:
1626 // Get a handle for the views.
1627 Toolbar toolbar = findViewById(R.id.toolbar);
1628 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1629 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1631 // Set the minimum height of the find on page linear layout to match the toolbar.
1632 findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1634 // Hide the toolbar.
1635 toolbar.setVisibility(View.GONE);
1637 // Show the find on page linear layout.
1638 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1640 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1641 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1642 findOnPageEditText.postDelayed(() -> {
1643 // Set the focus on `findOnPageEditText`.
1644 findOnPageEditText.requestFocus();
1646 // Get a handle for the input method manager.
1647 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1649 // Remove the lint warning below that the input method manager might be null.
1650 assert inputMethodManager != null;
1652 // Display the keyboard. `0` sets no input flags.
1653 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1656 // Consume the event.
1660 // Get a print manager instance.
1661 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1663 // Remove the lint error below that print manager might be null.
1664 assert printManager != null;
1666 // Create a print document adapter from the current WebView.
1667 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1669 // Print the document.
1670 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
1672 // Consume the event.
1675 case R.id.save_as_image:
1676 // Instantiate the save webpage image dialog.
1677 DialogFragment saveWebpageImageDialogFragment = new SaveWebpageImageDialog();
1679 // Show the save webpage image dialog.
1680 saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_as_image));
1682 // Consume the event.
1685 case R.id.add_to_homescreen:
1686 // Instantiate the create home screen shortcut dialog.
1687 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1688 currentWebView.getFavoriteOrDefaultIcon());
1690 // Show the create home screen shortcut dialog.
1691 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1693 // Consume the event.
1696 case R.id.view_source:
1697 // Create an intent to launch the view source activity.
1698 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1700 // Add the variables to the intent.
1701 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1702 viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
1705 startActivity(viewSourceIntent);
1707 // Consume the event.
1710 case R.id.share_url:
1711 // Setup the share string.
1712 String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1714 // Create the share intent.
1715 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1716 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1717 shareIntent.setType("text/plain");
1720 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1722 // Consume the event.
1725 case R.id.open_with_app:
1726 // Open the URL with an outside app.
1727 openWithApp(currentWebView.getUrl());
1729 // Consume the event.
1732 case R.id.open_with_browser:
1733 // Open the URL with an outside browser.
1734 openWithBrowser(currentWebView.getUrl());
1736 // Consume the event.
1739 case R.id.proxy_through_orbot:
1740 // Toggle the proxy through Orbot variable.
1741 proxyThroughOrbot = !proxyThroughOrbot;
1743 // Apply the proxy through Orbot settings.
1744 applyProxyThroughOrbot(true);
1746 // Consume the event.
1750 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
1751 // Reload the current WebView.
1752 currentWebView.reload();
1753 } else { // The stop button was pushed.
1754 // Stop the loading of the WebView.
1755 currentWebView.stopLoading();
1758 // Consume the event.
1761 case R.id.ad_consent:
1762 // Instantiate the ad consent dialog.
1763 DialogFragment adConsentDialogFragment = new AdConsentDialog();
1765 // Display the ad consent dialog.
1766 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
1768 // Consume the event.
1772 // Don't consume the event.
1773 return super.onOptionsItemSelected(menuItem);
1777 // removeAllCookies is deprecated, but it is required for API < 21.
1779 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
1780 // Get the menu item ID.
1781 int menuItemId = menuItem.getItemId();
1783 // Get a handle for the shared preferences.
1784 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1786 // Run the commands that correspond to the selected menu item.
1787 switch (menuItemId) {
1788 case R.id.clear_and_exit:
1789 // Clear and exit Privacy Browser.
1794 // Select the homepage based on the proxy through Orbot status.
1795 if (proxyThroughOrbot) {
1796 // Load the Tor homepage.
1797 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
1799 // Load the normal homepage.
1800 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
1805 if (currentWebView.canGoBack()) {
1806 // Reset the current domain name so that navigation works if third-party requests are blocked.
1807 currentWebView.resetCurrentDomainName();
1809 // Set navigating history so that the domain settings are applied when the new URL is loaded.
1810 currentWebView.setNavigatingHistory(true);
1812 // Load the previous website in the history.
1813 currentWebView.goBack();
1818 if (currentWebView.canGoForward()) {
1819 // Reset the current domain name so that navigation works if third-party requests are blocked.
1820 currentWebView.resetCurrentDomainName();
1822 // Set navigating history so that the domain settings are applied when the new URL is loaded.
1823 currentWebView.setNavigatingHistory(true);
1825 // Load the next website in the history.
1826 currentWebView.goForward();
1831 // Instantiate the URL history dialog.
1832 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
1834 // Show the URL history dialog.
1835 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
1839 // Populate the resource requests.
1840 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
1842 // Create an intent to launch the Requests activity.
1843 Intent requestsIntent = new Intent(this, RequestsActivity.class);
1845 // Add the block third-party requests status to the intent.
1846 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1849 startActivity(requestsIntent);
1852 case R.id.downloads:
1853 // Launch the system Download Manager.
1854 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
1856 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
1857 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1859 startActivity(downloadManagerIntent);
1863 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
1864 reapplyDomainSettingsOnRestart = true;
1866 // Launch the domains activity.
1867 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1869 // Add the extra information to the intent.
1870 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1872 // Get the current certificate.
1873 SslCertificate sslCertificate = currentWebView.getCertificate();
1875 // Check to see if the SSL certificate is populated.
1876 if (sslCertificate != null) {
1877 // Extract the certificate to strings.
1878 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1879 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1880 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1881 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1882 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1883 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1884 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1885 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1887 // Add the certificate to the intent.
1888 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1889 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1890 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1891 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1892 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1893 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1894 domainsIntent.putExtra("ssl_start_date", startDateLong);
1895 domainsIntent.putExtra("ssl_end_date", endDateLong);
1898 // Check to see if the current IP addresses have been received.
1899 if (currentWebView.hasCurrentIpAddresses()) {
1900 // Add the current IP addresses to the intent.
1901 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1905 startActivity(domainsIntent);
1909 // Set the flag to reapply app settings on restart when returning from Settings.
1910 reapplyAppSettingsOnRestart = true;
1912 // Set the flag to reapply the domain settings on restart when returning from Settings.
1913 reapplyDomainSettingsOnRestart = true;
1915 // Launch the settings activity.
1916 Intent settingsIntent = new Intent(this, SettingsActivity.class);
1917 startActivity(settingsIntent);
1920 case R.id.import_export:
1921 // Launch the import/export activity.
1922 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
1923 startActivity(importExportIntent);
1927 // Launch the logcat activity.
1928 Intent logcatIntent = new Intent(this, LogcatActivity.class);
1929 startActivity(logcatIntent);
1933 // Launch `GuideActivity`.
1934 Intent guideIntent = new Intent(this, GuideActivity.class);
1935 startActivity(guideIntent);
1939 // Create an intent to launch the about activity.
1940 Intent aboutIntent = new Intent(this, AboutActivity.class);
1942 // Create a string array for the blocklist versions.
1943 String[] blocklistVersions = new String[] {easyList.get(0).get(0)[0], easyPrivacy.get(0).get(0)[0], fanboysAnnoyanceList.get(0).get(0)[0], fanboysSocialList.get(0).get(0)[0],
1944 ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
1946 // Add the blocklist versions to the intent.
1947 aboutIntent.putExtra("blocklist_versions", blocklistVersions);
1950 startActivity(aboutIntent);
1954 // Get a handle for the drawer layout.
1955 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1957 // Close the navigation drawer.
1958 drawerLayout.closeDrawer(GravityCompat.START);
1963 public void onPostCreate(Bundle savedInstanceState) {
1964 // Run the default commands.
1965 super.onPostCreate(savedInstanceState);
1967 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
1968 actionBarDrawerToggle.syncState();
1972 public void onConfigurationChanged(Configuration newConfig) {
1973 // Run the default commands.
1974 super.onConfigurationChanged(newConfig);
1976 // Get the status bar pixel size.
1977 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
1978 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
1980 // Get the resource density.
1981 float screenDensity = getResources().getDisplayMetrics().density;
1983 // Recalculate the drawer header padding.
1984 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
1985 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
1986 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
1988 // Reload the ad for the free flavor if not in full screen mode.
1989 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
1990 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
1991 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
1994 // `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:
1995 // https://code.google.com/p/android/issues/detail?id=20493#c8
1996 // ActivityCompat.invalidateOptionsMenu(this);
2000 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2001 // Store the hit test result.
2002 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2004 // Define the URL strings.
2005 final String imageUrl;
2006 final String linkUrl;
2008 // Get handles for the system managers.
2009 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2010 FragmentManager fragmentManager = getSupportFragmentManager();
2011 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2013 // Remove the lint errors below that the clipboard manager might be null.
2014 assert clipboardManager != null;
2016 // Process the link according to the type.
2017 switch (hitTestResult.getType()) {
2018 // `SRC_ANCHOR_TYPE` is a link.
2019 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2020 // Get the target URL.
2021 linkUrl = hitTestResult.getExtra();
2023 // Set the target URL as the title of the `ContextMenu`.
2024 menu.setHeaderTitle(linkUrl);
2026 // Add an Open in New Tab entry.
2027 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2028 // Load the link URL in a new tab.
2031 // Consume the event.
2035 // Add an Open with App entry.
2036 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2037 openWithApp(linkUrl);
2039 // Consume the event.
2043 // Add an Open with Browser entry.
2044 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2045 openWithBrowser(linkUrl);
2047 // Consume the event.
2051 // Add a Copy URL entry.
2052 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2053 // Save the link URL in a `ClipData`.
2054 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2056 // Set the `ClipData` as the clipboard's primary clip.
2057 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2059 // Consume the event.
2063 // Add a Download URL entry.
2064 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2065 // Check if the download should be processed by an external app.
2066 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2067 openUrlWithExternalApp(linkUrl);
2068 } else { // Download with Android's download manager.
2069 // Check to see if the storage permission has already been granted.
2070 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2071 // Store the variables for future use by `onRequestPermissionsResult()`.
2072 downloadUrl = linkUrl;
2073 downloadContentDisposition = "none";
2074 downloadContentLength = -1;
2076 // Show a dialog if the user has previously denied the permission.
2077 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2078 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2079 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2081 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2082 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2083 } else { // Show the permission request directly.
2084 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2085 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2087 } else { // The storage permission has already been granted.
2088 // Get a handle for the download file alert dialog.
2089 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2091 // Show the download file alert dialog.
2092 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2096 // Consume the event.
2100 // Add a Cancel entry, which by default closes the context menu.
2101 menu.add(R.string.cancel);
2104 case WebView.HitTestResult.EMAIL_TYPE:
2105 // Get the target URL.
2106 linkUrl = hitTestResult.getExtra();
2108 // Set the target URL as the title of the `ContextMenu`.
2109 menu.setHeaderTitle(linkUrl);
2111 // Add a Write Email entry.
2112 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2113 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2114 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2116 // Parse the url and set it as the data for the `Intent`.
2117 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2119 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2120 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2123 startActivity(emailIntent);
2125 // Consume the event.
2129 // Add a Copy Email Address entry.
2130 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2131 // Save the email address in a `ClipData`.
2132 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2134 // Set the `ClipData` as the clipboard's primary clip.
2135 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2137 // Consume the event.
2141 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2142 menu.add(R.string.cancel);
2145 // `IMAGE_TYPE` is an image.
2146 case WebView.HitTestResult.IMAGE_TYPE:
2147 // Get the image URL.
2148 imageUrl = hitTestResult.getExtra();
2150 // Set the image URL as the title of the context menu.
2151 menu.setHeaderTitle(imageUrl);
2153 // Add an Open in New Tab entry.
2154 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2155 // Load the image URL in a new tab.
2156 addNewTab(imageUrl);
2158 // Consume the event.
2162 // Add a View Image entry.
2163 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2164 // Load the image in the current tab.
2167 // Consume the event.
2171 // Add a Download Image entry.
2172 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2173 // Check if the download should be processed by an external app.
2174 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2175 openUrlWithExternalApp(imageUrl);
2176 } else { // Download with Android's download manager.
2177 // Check to see if the storage permission has already been granted.
2178 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2179 // Store the image URL for use by `onRequestPermissionResult()`.
2180 downloadImageUrl = imageUrl;
2182 // Show a dialog if the user has previously denied the permission.
2183 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2184 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2185 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2187 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2188 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2189 } else { // Show the permission request directly.
2190 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2191 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2193 } else { // The storage permission has already been granted.
2194 // Get a handle for the download image alert dialog.
2195 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2197 // Show the download image alert dialog.
2198 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2202 // Consume the event.
2206 // Add a Copy URL entry.
2207 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2208 // Save the image URL in a clip data.
2209 ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2211 // Set the clip data as the clipboard's primary clip.
2212 clipboardManager.setPrimaryClip(imageTypeClipData);
2214 // Consume the event.
2218 // Add an Open with App entry.
2219 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2220 // Open the image URL with an external app.
2221 openWithApp(imageUrl);
2223 // Consume the event.
2227 // Add an Open with Browser entry.
2228 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2229 // Open the image URL with an external browser.
2230 openWithBrowser(imageUrl);
2232 // Consume the event.
2236 // Add a Cancel entry, which by default closes the context menu.
2237 menu.add(R.string.cancel);
2240 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2241 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2242 // Get the image URL.
2243 imageUrl = hitTestResult.getExtra();
2245 // Instantiate a handler.
2246 Handler handler = new Handler();
2248 // Get a message from the handler.
2249 Message message = handler.obtainMessage();
2251 // Request the image details from the last touched node be returned in the message.
2252 currentWebView.requestFocusNodeHref(message);
2254 // Get the link URL from the message data.
2255 linkUrl = message.getData().getString("url");
2257 // Set the link URL as the title of the context menu.
2258 menu.setHeaderTitle(linkUrl);
2260 // Add an Open in New Tab entry.
2261 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2262 // Load the link URL in a new tab.
2265 // Consume the event.
2269 // Add a View Image entry.
2270 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2271 // View the image in the current tab.
2274 // Consume the event.
2278 // Add a Download Image entry.
2279 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2280 // Check if the download should be processed by an external app.
2281 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2282 openUrlWithExternalApp(imageUrl);
2283 } else { // Download with Android's download manager.
2284 // Check to see if the storage permission has already been granted.
2285 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2286 // Store the image URL for use by `onRequestPermissionResult()`.
2287 downloadImageUrl = imageUrl;
2289 // Show a dialog if the user has previously denied the permission.
2290 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2291 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2292 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2294 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2295 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2296 } else { // Show the permission request directly.
2297 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2298 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2300 } else { // The storage permission has already been granted.
2301 // Get a handle for the download image alert dialog.
2302 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2304 // Show the download image alert dialog.
2305 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2309 // Consume the event.
2313 // Add a Copy URL entry.
2314 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2315 // Save the link URL in a clip data.
2316 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2318 // Set the clip data as the clipboard's primary clip.
2319 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2321 // Consume the event.
2325 // Add an Open with App entry.
2326 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2327 // Open the link URL with an external app.
2328 openWithApp(linkUrl);
2330 // Consume the event.
2334 // Add an Open with Browser entry.
2335 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2336 // Open the link URL with an external browser.
2337 openWithBrowser(linkUrl);
2339 // Consume the event.
2343 // Add a cancel entry, which by default closes the context menu.
2344 menu.add(R.string.cancel);
2350 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2351 // Get a handle for the bookmarks list view.
2352 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2354 // Get the views from the dialog fragment.
2355 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2356 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2358 // Extract the strings from the edit texts.
2359 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2360 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2362 // Create a favorite icon byte array output stream.
2363 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2365 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2366 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2368 // Convert the favorite icon byte array stream to a byte array.
2369 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2371 // Display the new bookmark below the current items in the (0 indexed) list.
2372 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2374 // Create the bookmark.
2375 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2377 // Update the bookmarks cursor with the current contents of this folder.
2378 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2380 // Update the list view.
2381 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2383 // Scroll to the new bookmark.
2384 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2388 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2389 // Get a handle for the bookmarks list view.
2390 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2392 // Get handles for the views in the dialog fragment.
2393 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2394 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2395 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2397 // Get new folder name string.
2398 String folderNameString = createFolderNameEditText.getText().toString();
2400 // Create a folder icon bitmap.
2401 Bitmap folderIconBitmap;
2403 // Set the folder icon bitmap according to the dialog.
2404 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2405 // Get the default folder icon drawable.
2406 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2408 // Convert the folder icon drawable to a bitmap drawable.
2409 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2411 // Convert the folder icon bitmap drawable to a bitmap.
2412 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2413 } else { // Use the WebView favorite icon.
2414 // Copy the favorite icon bitmap to the folder icon bitmap.
2415 folderIconBitmap = favoriteIconBitmap;
2418 // Create a folder icon byte array output stream.
2419 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2421 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2422 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2424 // Convert the folder icon byte array stream to a byte array.
2425 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2427 // Move all the bookmarks down one in the display order.
2428 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2429 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2430 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2433 // Create the folder, which will be placed at the top of the `ListView`.
2434 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2436 // Update the bookmarks cursor with the current contents of this folder.
2437 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2439 // Update the `ListView`.
2440 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2442 // Scroll to the new folder.
2443 bookmarksListView.setSelection(0);
2447 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2448 // Get handles for the views from `dialogFragment`.
2449 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2450 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2451 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2453 // Store the bookmark strings.
2454 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2455 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2457 // Update the bookmark.
2458 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2459 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2460 } else { // Update the bookmark using the `WebView` favorite icon.
2461 // Create a favorite icon byte array output stream.
2462 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2464 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2465 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2467 // Convert the favorite icon byte array stream to a byte array.
2468 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2470 // Update the bookmark and the favorite icon.
2471 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2474 // Update the bookmarks cursor with the current contents of this folder.
2475 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2477 // Update the list view.
2478 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2482 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2483 // Get handles for the views from `dialogFragment`.
2484 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2485 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2486 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2487 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2489 // Get the new folder name.
2490 String newFolderNameString = editFolderNameEditText.getText().toString();
2492 // Check if the favorite icon has changed.
2493 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2494 // Update the name in the database.
2495 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2496 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2497 // Create the new folder icon Bitmap.
2498 Bitmap folderIconBitmap;
2500 // Populate the new folder icon bitmap.
2501 if (defaultFolderIconRadioButton.isChecked()) {
2502 // Get the default folder icon drawable.
2503 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2505 // Convert the folder icon drawable to a bitmap drawable.
2506 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2508 // Convert the folder icon bitmap drawable to a bitmap.
2509 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2510 } else { // Use the `WebView` favorite icon.
2511 // Copy the favorite icon bitmap to the folder icon bitmap.
2512 folderIconBitmap = favoriteIconBitmap;
2515 // Create a folder icon byte array output stream.
2516 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2518 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2519 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2521 // Convert the folder icon byte array stream to a byte array.
2522 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2524 // Update the folder icon in the database.
2525 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2526 } else { // The folder icon and the name have changed.
2527 // Get the new folder icon `Bitmap`.
2528 Bitmap folderIconBitmap;
2529 if (defaultFolderIconRadioButton.isChecked()) {
2530 // Get the default folder icon drawable.
2531 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2533 // Convert the folder icon drawable to a bitmap drawable.
2534 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2536 // Convert the folder icon bitmap drawable to a bitmap.
2537 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2538 } else { // Use the `WebView` favorite icon.
2539 // Copy the favorite icon bitmap to the folder icon bitmap.
2540 folderIconBitmap = favoriteIconBitmap;
2543 // Create a folder icon byte array output stream.
2544 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2546 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2547 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2549 // Convert the folder icon byte array stream to a byte array.
2550 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2552 // Update the folder name and icon in the database.
2553 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2556 // Update the bookmarks cursor with the current contents of this folder.
2557 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2559 // Update the `ListView`.
2560 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2564 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2565 switch (downloadType) {
2566 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2567 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2568 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2571 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2572 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2573 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2579 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2580 // Get a handle for the fragment manager.
2581 FragmentManager fragmentManager = getSupportFragmentManager();
2583 switch (requestCode) {
2584 case DOWNLOAD_FILE_REQUEST_CODE:
2585 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2586 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2588 // On API 23, displaying the fragment must be delayed or the app will crash.
2589 if (Build.VERSION.SDK_INT == 23) {
2590 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2592 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2595 // Reset the download variables.
2597 downloadContentDisposition = "";
2598 downloadContentLength = 0;
2601 case DOWNLOAD_IMAGE_REQUEST_CODE:
2602 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2603 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
2605 // On API 23, displaying the fragment must be delayed or the app will crash.
2606 if (Build.VERSION.SDK_INT == 23) {
2607 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2609 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2612 // Reset the image URL variable.
2613 downloadImageUrl = "";
2616 case SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
2617 // Check to see if the storage permission was granted. If the dialog was canceled the grant result will be empty.
2618 if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
2619 // Save the webpage image.
2620 new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
2621 } else { // The storage permission was not granted.
2622 // Display an error snackbar.
2623 Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
2626 // Reset the save website image file path.
2627 saveWebsiteImageFilePath = "";
2633 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
2634 // Download the image if it has an HTTP or HTTPS URI.
2635 if (imageUrl.startsWith("http")) {
2636 // Get a handle for the system `DOWNLOAD_SERVICE`.
2637 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2639 // Parse `imageUrl`.
2640 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
2642 // Get a handle for the cookie manager.
2643 CookieManager cookieManager = CookieManager.getInstance();
2645 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
2646 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2647 if (cookieManager.acceptCookie()) {
2648 // Get the cookies for `imageUrl`.
2649 String cookies = cookieManager.getCookie(imageUrl);
2651 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
2652 downloadRequest.addRequestHeader("Cookie", cookies);
2655 // Get the file name from the dialog fragment.
2656 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
2657 String imageName = downloadImageNameEditText.getText().toString();
2659 // Specify the download location.
2660 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
2661 // Download to the public download directory.
2662 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
2663 } else { // External write permission denied.
2664 // Download to the app's external download directory.
2665 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
2668 // Allow `MediaScanner` to index the download if it is a media file.
2669 downloadRequest.allowScanningByMediaScanner();
2671 // Add the URL as the description for the download.
2672 downloadRequest.setDescription(imageUrl);
2674 // Show the download notification after the download is completed.
2675 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2677 // Remove the lint warning below that `downloadManager` might be `null`.
2678 assert downloadManager != null;
2680 // Initiate the download.
2681 downloadManager.enqueue(downloadRequest);
2682 } else { // The image is not an HTTP or HTTPS URI.
2683 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
2688 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
2689 // Download the file if it has an HTTP or HTTPS URI.
2690 if (downloadUrl.startsWith("http")) {
2691 // Get a handle for the system `DOWNLOAD_SERVICE`.
2692 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2694 // Parse `downloadUrl`.
2695 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
2697 // Get a handle for the cookie manager.
2698 CookieManager cookieManager = CookieManager.getInstance();
2700 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
2701 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2702 if (cookieManager.acceptCookie()) {
2703 // Get the cookies for `downloadUrl`.
2704 String cookies = cookieManager.getCookie(downloadUrl);
2706 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
2707 downloadRequest.addRequestHeader("Cookie", cookies);
2710 // Get the file name from the dialog fragment.
2711 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
2712 String fileName = downloadFileNameEditText.getText().toString();
2714 // Specify the download location.
2715 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
2716 // Download to the public download directory.
2717 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
2718 } else { // External write permission denied.
2719 // Download to the app's external download directory.
2720 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
2723 // Allow `MediaScanner` to index the download if it is a media file.
2724 downloadRequest.allowScanningByMediaScanner();
2726 // Add the URL as the description for the download.
2727 downloadRequest.setDescription(downloadUrl);
2729 // Show the download notification after the download is completed.
2730 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2732 // Remove the lint warning below that `downloadManager` might be `null`.
2733 assert downloadManager != null;
2735 // Initiate the download.
2736 downloadManager.enqueue(downloadRequest);
2737 } else { // The download is not an HTTP or HTTPS URI.
2738 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
2742 // Override `onBackPressed` to handle the navigation drawer and and the WebView.
2744 public void onBackPressed() {
2745 // Get a handle for the drawer layout and the tab layout.
2746 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2747 TabLayout tabLayout = findViewById(R.id.tablayout);
2749 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
2750 // Close the navigation drawer.
2751 drawerLayout.closeDrawer(GravityCompat.START);
2752 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
2753 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
2754 // close the bookmarks drawer.
2755 drawerLayout.closeDrawer(GravityCompat.END);
2756 } else { // A subfolder is displayed.
2757 // Place the former parent folder in `currentFolder`.
2758 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
2760 // Load the new folder.
2761 loadBookmarksFolder();
2763 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
2764 // Reset the current domain name so that navigation works if third-party requests are blocked.
2765 currentWebView.resetCurrentDomainName();
2767 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2768 currentWebView.setNavigatingHistory(true);
2771 currentWebView.goBack();
2772 } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs.
2773 // Close the current tab.
2775 } else { // There isn't anything to do in Privacy Browser.
2776 // Run the default commands.
2777 super.onBackPressed();
2779 // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted.
2784 // Process the results of a file browse.
2786 public void onActivityResult(int requestCode, int resultCode, Intent data) {
2787 // Run the commands that correlate to the specified request code.
2788 switch (requestCode) {
2789 case FILE_UPLOAD_REQUEST_CODE:
2790 // File uploads only work on API >= 21.
2791 if (Build.VERSION.SDK_INT >= 21) {
2792 // Pass the file to the WebView.
2793 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
2797 case BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
2798 // Don't do anything if the user pressed back from the file picker.
2799 if (resultCode == Activity.RESULT_OK) {
2800 // Get a handle for the save dialog fragment.
2801 DialogFragment saveWebpageImageDialogFragment= (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_as_image));
2803 // Only update the file name if the dialog still exists.
2804 if (saveWebpageImageDialogFragment != null) {
2805 // Get a handle for the save webpage image dialog.
2806 Dialog saveWebpageImageDialog = saveWebpageImageDialogFragment.getDialog();
2808 // Get a handle for the file name edit text.
2809 EditText fileNameEditText = saveWebpageImageDialog.findViewById(R.id.file_name_edittext);
2811 // Instantiate the file name helper.
2812 FileNameHelper fileNameHelper = new FileNameHelper();
2814 // Convert the file name URI to a file name path.
2815 String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
2817 // Set the file name path as the text of the file name edit text.
2818 fileNameEditText.setText(fileNamePath);
2825 private void loadUrlFromTextBox() {
2826 // Get a handle for the URL edit text.
2827 EditText urlEditText = findViewById(R.id.url_edittext);
2829 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
2830 String unformattedUrlString = urlEditText.getText().toString().trim();
2832 // Initialize the formatted URL string.
2835 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
2836 if (unformattedUrlString.startsWith("content://")) { // This is a Content URL.
2837 // Load the entire content URL.
2838 url = unformattedUrlString;
2839 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2840 unformattedUrlString.startsWith("file://")) { // This is a standard URL.
2841 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
2842 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2843 unformattedUrlString = "https://" + unformattedUrlString;
2846 // Initialize `unformattedUrl`.
2847 URL unformattedUrl = null;
2849 // 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.
2851 unformattedUrl = new URL(unformattedUrlString);
2852 } catch (MalformedURLException e) {
2853 e.printStackTrace();
2856 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2857 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2858 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2859 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2860 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2861 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2864 Uri.Builder uri = new Uri.Builder();
2865 uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2867 // Decode the URI as a UTF-8 string in.
2869 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2870 } catch (UnsupportedEncodingException exception) {
2871 // Do nothing. The formatted URL string will remain blank.
2873 } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string.
2874 // Create an encoded URL String.
2875 String encodedUrlString;
2877 // Sanitize the search input.
2879 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2880 } catch (UnsupportedEncodingException exception) {
2881 encodedUrlString = "";
2884 // Add the base search URL.
2885 url = searchURL + encodedUrlString;
2888 // 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.
2889 urlEditText.clearFocus();
2895 private void loadUrl(String url) {
2896 // Sanitize the URL.
2897 url = sanitizeUrl(url);
2899 // Apply the domain settings.
2900 applyDomainSettings(currentWebView, url, true, false);
2903 currentWebView.loadUrl(url, customHeaders);
2906 public void findPreviousOnPage(View view) {
2907 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
2908 currentWebView.findNext(false);
2911 public void findNextOnPage(View view) {
2912 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2913 currentWebView.findNext(true);
2916 public void closeFindOnPage(View view) {
2917 // Get a handle for the views.
2918 Toolbar toolbar = findViewById(R.id.toolbar);
2919 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2920 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
2922 // Delete the contents of `find_on_page_edittext`.
2923 findOnPageEditText.setText(null);
2925 // Clear the highlighted phrases if the WebView is not null.
2926 if (currentWebView != null) {
2927 currentWebView.clearMatches();
2930 // Hide the find on page linear layout.
2931 findOnPageLinearLayout.setVisibility(View.GONE);
2933 // Show the toolbar.
2934 toolbar.setVisibility(View.VISIBLE);
2936 // Get a handle for the input method manager.
2937 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
2939 // Remove the lint warning below that the input method manager might be null.
2940 assert inputMethodManager != null;
2942 // Hide the keyboard.
2943 inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
2947 public void onSaveWebpageImage(DialogFragment dialogFragment) {
2948 // Get a handle for the file name edit text.
2949 EditText fileNameEditText = dialogFragment.getDialog().findViewById(R.id.file_name_edittext);
2951 // Get the file path string.
2952 saveWebsiteImageFilePath = fileNameEditText.getText().toString();
2954 // Check to see if the storage permission is needed.
2955 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
2956 // Save the webpage image.
2957 new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
2958 } else { // The storage permission has not been granted.
2959 // Get the external private directory `File`.
2960 File externalPrivateDirectoryFile = getExternalFilesDir(null);
2962 // Remove the incorrect lint error below that the file might be null.
2963 assert externalPrivateDirectoryFile != null;
2965 // Get the external private directory string.
2966 String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
2968 // Check to see if the file path is in the external private directory.
2969 if (saveWebsiteImageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
2970 // Save the webpage image.
2971 new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
2972 } else { // The file path is in a public directory.
2973 // Check if the user has previously denied the storage permission.
2974 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2975 // Instantiate the storage permission alert dialog.
2976 DialogFragment storagePermissionDialogFragment = new StoragePermissionDialog();
2978 // Show the storage permission alert dialog. The permission will be requested when the dialog is closed.
2979 storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
2980 } else { // Show the permission request directly.
2981 // Request the write external storage permission. The webpage image will be saved when it finishes.
2982 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
2989 public void onCloseStoragePermissionDialog() {
2990 // Request the write external storage permission. The webpage image will be saved when it finishes.
2991 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
2994 private void applyAppSettings() {
2995 // Initialize the app if this is the first run. This is done here instead of in `onCreate()` to shorten the time that an unthemed background is displayed on app startup.
2996 if (webViewDefaultUserAgent == null) {
3000 // Get a handle for the shared preferences.
3001 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3003 // Store the values from the shared preferences in variables.
3004 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3005 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3006 sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true);
3007 sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true);
3008 sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true);
3009 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3010 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3011 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3012 scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
3014 // Get handles for the views that need to be modified.
3015 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3016 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
3017 ActionBar actionBar = getSupportActionBar();
3018 Toolbar toolbar = findViewById(R.id.toolbar);
3019 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3020 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
3021 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3023 // Remove the incorrect lint warning below that the action bar might be null.
3024 assert actionBar != null;
3026 // Apply the proxy through Orbot settings.
3027 applyProxyThroughOrbot(false);
3029 // Set Do Not Track status.
3030 if (doNotTrackEnabled) {
3031 customHeaders.put("DNT", "1");
3033 customHeaders.remove("DNT");
3036 // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3037 CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
3038 AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
3039 AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams();
3040 AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams();
3042 // Add the scrolling behavior to the layout parameters.
3044 // Enable scrolling of the app bar.
3045 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
3046 toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3047 findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3048 tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3050 // Disable scrolling of the app bar.
3051 swipeRefreshLayoutParams.setBehavior(null);
3052 toolbarLayoutParams.setScrollFlags(0);
3053 findOnPageLayoutParams.setScrollFlags(0);
3054 tabsLayoutParams.setScrollFlags(0);
3056 // Expand the app bar if it is currently collapsed.
3057 appBarLayout.setExpanded(true);
3060 // Apply the modified layout parameters.
3061 swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams);
3062 toolbar.setLayoutParams(toolbarLayoutParams);
3063 findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams);
3064 tabsLinearLayout.setLayoutParams(tabsLayoutParams);
3066 // Set the app bar scrolling for each WebView.
3067 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3068 // Get the WebView tab fragment.
3069 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3071 // Get the fragment view.
3072 View fragmentView = webViewTabFragment.getView();
3074 // Only modify the WebViews if they exist.
3075 if (fragmentView != null) {
3076 // Get the nested scroll WebView from the tab fragment.
3077 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3079 // Set the app bar scrolling.
3080 nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
3084 // Update the full screen browsing mode settings.
3085 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3086 // Update the visibility of the app bar, which might have changed in the settings.
3088 // Hide the tab linear layout.
3089 tabsLinearLayout.setVisibility(View.GONE);
3091 // Hide the action bar.
3094 // Show the tab linear layout.
3095 tabsLinearLayout.setVisibility(View.VISIBLE);
3097 // Show the action bar.
3101 // Hide the banner ad in the free flavor.
3102 if (BuildConfig.FLAVOR.contentEquals("free")) {
3103 AdHelper.hideAd(findViewById(R.id.adview));
3106 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3107 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3109 /* Hide the system bars.
3110 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3111 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3112 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3113 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3115 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3116 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3117 } else { // Privacy Browser is not in full screen browsing mode.
3118 // 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.
3119 inFullScreenBrowsingMode = false;
3121 // Show the tab linear layout.
3122 tabsLinearLayout.setVisibility(View.VISIBLE);
3124 // Show the action bar.
3127 // Show the banner ad in the free flavor.
3128 if (BuildConfig.FLAVOR.contentEquals("free")) {
3129 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3130 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3133 // Remove the `SYSTEM_UI` flags from the root frame layout.
3134 rootFrameLayout.setSystemUiVisibility(0);
3136 // Add the translucent status flag.
3137 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3141 private void initializeApp() {
3142 // Get a handle for the shared preferences.
3143 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3145 // Get the theme preference.
3146 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3148 // Get a handle for the input method.
3149 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3151 // Remove the lint warning below that the input method manager might be null.
3152 assert inputMethodManager != null;
3154 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
3155 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
3156 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
3157 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
3159 // Get handles for the URL views.
3160 EditText urlEditText = findViewById(R.id.url_edittext);
3162 // Remove the formatting from the URL edit text when the user is editing the text.
3163 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
3164 if (hasFocus) { // The user is editing the URL text box.
3165 // Remove the highlighting.
3166 urlEditText.getText().removeSpan(redColorSpan);
3167 urlEditText.getText().removeSpan(initialGrayColorSpan);
3168 urlEditText.getText().removeSpan(finalGrayColorSpan);
3169 } else { // The user has stopped editing the URL text box.
3170 // Move to the beginning of the string.
3171 urlEditText.setSelection(0);
3173 // Reapply the highlighting.
3178 // Set the go button on the keyboard to load the URL in `urlTextBox`.
3179 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
3180 // If the event is a key-down event on the `enter` button, load the URL.
3181 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
3182 // Load the URL into the mainWebView and consume the event.
3183 loadUrlFromTextBox();
3185 // If the enter key was pressed, consume the event.
3188 // If any other key was pressed, do not consume the event.
3193 // Initialize the Orbot status and the waiting for Orbot trackers.
3194 orbotStatus = "unknown";
3195 waitingForOrbot = false;
3197 // Create an Orbot status `BroadcastReceiver`.
3198 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
3200 public void onReceive(Context context, Intent intent) {
3201 // Store the content of the status message in `orbotStatus`.
3202 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
3204 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
3205 if (orbotStatus.equals("ON") && waitingForOrbot) {
3206 // Reset the waiting for Orbot status.
3207 waitingForOrbot = false;
3209 // Get the intent that started the app.
3210 Intent launchingIntent = getIntent();
3212 // Get the information from the intent.
3213 String launchingIntentAction = launchingIntent.getAction();
3214 Uri launchingIntentUriData = launchingIntent.getData();
3216 // If the intent action is a web search, perform the search.
3217 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
3218 // Create an encoded URL string.
3219 String encodedUrlString;
3221 // Sanitize the search input and convert it to a search.
3223 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
3224 } catch (UnsupportedEncodingException exception) {
3225 encodedUrlString = "";
3228 // Load the completed search URL.
3229 loadUrl(searchURL + encodedUrlString);
3230 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
3231 // Load the URL from the intent.
3232 loadUrl(launchingIntentUriData.toString());
3233 } else { // The is no URL in the intent.
3234 // Select the homepage based on the proxy through Orbot status.
3235 if (proxyThroughOrbot) {
3236 // Load the Tor homepage.
3237 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
3239 // Load the normal homepage.
3240 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
3247 // Register `orbotStatusBroadcastReceiver` on `this` context.
3248 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
3250 // Get handles for views that need to be modified.
3251 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3252 NavigationView navigationView = findViewById(R.id.navigationview);
3253 TabLayout tabLayout = findViewById(R.id.tablayout);
3254 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3255 ViewPager webViewPager = findViewById(R.id.webviewpager);
3256 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
3257 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
3258 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
3259 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
3260 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3262 // Listen for touches on the navigation menu.
3263 navigationView.setNavigationItemSelectedListener(this);
3265 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
3266 Menu navigationMenu = navigationView.getMenu();
3267 MenuItem navigationBackMenuItem = navigationMenu.getItem(2);
3268 MenuItem navigationForwardMenuItem = navigationMenu.getItem(3);
3269 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4);
3270 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
3272 // Update the web view pager every time a tab is modified.
3273 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
3275 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
3280 public void onPageSelected(int position) {
3281 // Close the find on page bar if it is open.
3282 closeFindOnPage(null);
3284 // Set the current WebView.
3285 setCurrentWebView(position);
3287 // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager or by creating a new tab.
3288 if (tabLayout.getSelectedTabPosition() != position) {
3289 // Create a handler to select the tab.
3290 Handler selectTabHandler = new Handler();
3292 // Create a runnable to select the tab.
3293 Runnable selectTabRunnable = () -> {
3294 // Get a handle for the tab.
3295 TabLayout.Tab tab = tabLayout.getTabAt(position);
3297 // Assert that the tab is not null.
3304 // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated.
3305 selectTabHandler.postDelayed(selectTabRunnable, 150);
3310 public void onPageScrollStateChanged(int state) {
3315 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
3316 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
3318 public void onTabSelected(TabLayout.Tab tab) {
3319 // Select the same page in the view pager.
3320 webViewPager.setCurrentItem(tab.getPosition());
3324 public void onTabUnselected(TabLayout.Tab tab) {
3329 public void onTabReselected(TabLayout.Tab tab) {
3330 // Instantiate the View SSL Certificate dialog.
3331 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
3333 // Display the View SSL Certificate dialog.
3334 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
3338 // 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.
3339 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
3341 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
3342 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
3343 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
3344 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
3346 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
3347 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
3348 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
3349 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
3352 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
3353 launchBookmarksActivityFab.setOnClickListener(v -> {
3354 // Get a copy of the favorite icon bitmap.
3355 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
3357 // Create a favorite icon byte array output stream.
3358 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3360 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
3361 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
3363 // Convert the favorite icon byte array stream to a byte array.
3364 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
3366 // Create an intent to launch the bookmarks activity.
3367 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
3369 // Add the extra information to the intent.
3370 bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
3371 bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
3372 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
3373 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
3376 startActivity(bookmarksIntent);
3379 // Set the create new bookmark folder FAB to display an alert dialog.
3380 createBookmarkFolderFab.setOnClickListener(v -> {
3381 // Create a create bookmark folder dialog.
3382 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
3384 // Show the create bookmark folder dialog.
3385 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
3388 // Set the create new bookmark FAB to display an alert dialog.
3389 createBookmarkFab.setOnClickListener(view -> {
3390 // Instantiate the create bookmark dialog.
3391 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
3393 // Display the create bookmark dialog.
3394 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
3397 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
3398 findOnPageEditText.addTextChangedListener(new TextWatcher() {
3400 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3405 public void onTextChanged(CharSequence s, int start, int before, int count) {
3410 public void afterTextChanged(Editable s) {
3411 // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null.
3412 if (currentWebView != null) {
3413 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
3418 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
3419 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
3420 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
3421 // Hide the soft keyboard.
3422 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3424 // Consume the event.
3426 } else { // A different key was pressed.
3427 // Do not consume the event.
3432 // Implement swipe to refresh.
3433 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
3435 // Store the default progress view offsets for use later in `initializeWebView()`.
3436 defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
3437 defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
3439 // Set the swipe to refresh color according to the theme.
3441 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
3442 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
3444 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
3447 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
3448 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
3449 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
3451 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
3452 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
3454 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
3455 currentBookmarksFolder = "";
3457 // Load the home folder, which is `""` in the database.
3458 loadBookmarksFolder();
3460 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
3461 // Convert the id from long to int to match the format of the bookmarks database.
3462 int databaseID = (int) id;
3464 // Get the bookmark cursor for this ID and move it to the first row.
3465 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
3466 bookmarkCursor.moveToFirst();
3468 // Act upon the bookmark according to the type.
3469 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
3470 // Store the new folder name in `currentBookmarksFolder`.
3471 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
3473 // Load the new folder.
3474 loadBookmarksFolder();
3475 } else { // The selected bookmark is not a folder.
3476 // Load the bookmark URL.
3477 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
3479 // Close the bookmarks drawer.
3480 drawerLayout.closeDrawer(GravityCompat.END);
3483 // Close the `Cursor`.
3484 bookmarkCursor.close();
3487 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
3488 // Convert the database ID from `long` to `int`.
3489 int databaseId = (int) id;
3491 // Find out if the selected bookmark is a folder.
3492 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
3495 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
3496 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
3498 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
3499 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3500 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
3502 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
3503 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3504 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
3507 // Consume the event.
3511 // Get the status bar pixel size.
3512 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
3513 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
3515 // Get the resource density.
3516 float screenDensity = getResources().getDisplayMetrics().density;
3518 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
3519 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
3520 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
3521 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
3523 // The drawer listener is used to update the navigation menu.`
3524 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
3526 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
3530 public void onDrawerOpened(@NonNull View drawerView) {
3534 public void onDrawerClosed(@NonNull View drawerView) {
3538 public void onDrawerStateChanged(int newState) {
3539 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
3540 // Get handles for the drawer headers.
3541 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
3542 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
3544 // 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.
3545 if (navigationHeaderTextView != null) {
3546 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
3549 // 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.
3550 if (bookmarksHeaderTextView != null) {
3551 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
3554 // Update the navigation menu items if the WebView is not null.
3555 if (currentWebView != null) {
3556 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
3557 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
3558 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
3559 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
3561 // Hide the keyboard (if displayed).
3562 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3565 // 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.
3566 urlEditText.clearFocus();
3567 currentWebView.clearFocus();
3572 // 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).
3573 customHeaders.put("X-Requested-With", "");
3575 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
3576 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
3578 // Get a handle for the WebView.
3579 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
3581 // Store the default user agent.
3582 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
3584 // Destroy the bare WebView.
3585 bareWebView.destroy();
3588 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3589 @SuppressLint("SetJavaScriptEnabled")
3590 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) {
3591 // Store a copy of the current user agent to track changes for the return boolean.
3592 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3594 // Store the current URL.
3595 nestedScrollWebView.setCurrentUrl(url);
3597 // Parse the URL into a URI.
3598 Uri uri = Uri.parse(url);
3600 // Extract the domain from `uri`.
3601 String newHostName = uri.getHost();
3603 // Strings don't like to be null.
3604 if (newHostName == null) {
3608 // Apply the domain settings if a new domain is being loaded or if the new domain is blank. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3609 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3610 // Set the new host name as the current domain name.
3611 nestedScrollWebView.setCurrentDomainName(newHostName);
3613 // Reset the ignoring of pinned domain information.
3614 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3616 // Clear any pinned SSL certificate or IP addresses.
3617 nestedScrollWebView.clearPinnedSslCertificate();
3618 nestedScrollWebView.clearPinnedIpAddresses();
3620 // Reset the favorite icon if specified.
3622 // Initialize the favorite icon.
3623 nestedScrollWebView.initializeFavoriteIcon();
3625 // Get the current page position.
3626 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3628 // Get a handle for the tab layout.
3629 TabLayout tabLayout = findViewById(R.id.tablayout);
3631 // Get the corresponding tab.
3632 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3634 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3636 // Get the tab custom view.
3637 View tabCustomView = tab.getCustomView();
3639 // Remove the warning below that the tab custom view might be null.
3640 assert tabCustomView != null;
3642 // Get the tab views.
3643 ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3644 TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3646 // Set the default favorite icon as the favorite icon for this tab.
3647 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3649 // Set the loading title text.
3650 tabTitleTextView.setText(R.string.loading);
3654 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3655 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3657 // Get a full cursor from `domainsDatabaseHelper`.
3658 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3660 // Initialize `domainSettingsSet`.
3661 Set<String> domainSettingsSet = new HashSet<>();
3663 // Get the domain name column index.
3664 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3666 // Populate `domainSettingsSet`.
3667 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3668 // Move `domainsCursor` to the current row.
3669 domainNameCursor.moveToPosition(i);
3671 // Store the domain name in `domainSettingsSet`.
3672 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3675 // Close `domainNameCursor.
3676 domainNameCursor.close();
3678 // Initialize the domain name in database variable.
3679 String domainNameInDatabase = null;
3681 // Check the hostname against the domain settings set.
3682 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3683 // Record the domain name in the database.
3684 domainNameInDatabase = newHostName;
3686 // Set the domain settings applied tracker to true.
3687 nestedScrollWebView.setDomainSettingsApplied(true);
3688 } else { // The hostname is not contained in the domain settings set.
3689 // Set the domain settings applied tracker to false.
3690 nestedScrollWebView.setDomainSettingsApplied(false);
3693 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3694 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3695 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3696 // Set the domain settings applied tracker to true.
3697 nestedScrollWebView.setDomainSettingsApplied(true);
3699 // Store the applied domain names as it appears in the database.
3700 domainNameInDatabase = "*." + newHostName;
3703 // Strip out the lowest subdomain of of the host name.
3704 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3708 // Get a handle for the shared preferences.
3709 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3711 // Store the general preference information.
3712 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3713 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3714 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3715 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3716 boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true);
3717 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3719 // Get a handle for the cookie manager.
3720 CookieManager cookieManager = CookieManager.getInstance();
3722 // Get handles for the views.
3723 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3724 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3726 // Initialize the user agent array adapter and string array.
3727 ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3728 String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3730 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3731 // Get a cursor for the current host and move it to the first position.
3732 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3733 currentDomainSettingsCursor.moveToFirst();
3735 // Get the settings from the cursor.
3736 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3737 nestedScrollWebView.setDomainSettingsJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3738 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3739 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3740 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3741 // Form data can be removed once the minimum API >= 26.
3742 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3743 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST,
3744 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3745 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY,
3746 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3747 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3748 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3749 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3750 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3751 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)) == 1);
3752 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY,
3753 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3754 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3755 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3756 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3757 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3758 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3759 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3760 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT));
3761 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3762 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3763 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3764 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3765 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3766 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3767 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3768 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3769 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3770 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3772 // Create the pinned SSL date variables.
3773 Date pinnedSslStartDate;
3774 Date pinnedSslEndDate;
3776 // 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.
3777 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3778 pinnedSslStartDate = null;
3780 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3783 // 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.
3784 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3785 pinnedSslEndDate = null;
3787 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3790 // If there is a pinned SSL certificate, store it in the WebView.
3791 if (pinnedSslCertificate) {
3792 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3793 pinnedSslStartDate, pinnedSslEndDate);
3796 // If there is a pinned IP address, store it in the WebView.
3797 if (pinnedIpAddresses) {
3798 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3801 // Set night mode according to the night mode int.
3802 switch (nightModeInt) {
3803 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3804 // Set night mode according to the current default.
3805 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3808 case DomainsDatabaseHelper.ENABLED:
3809 // Enable night mode.
3810 nestedScrollWebView.setNightMode(true);
3813 case DomainsDatabaseHelper.DISABLED:
3814 // Disable night mode.
3815 nestedScrollWebView.setNightMode(false);
3819 // Enable JavaScript if night mode is enabled.
3820 if (nestedScrollWebView.getNightMode()) {
3821 // Enable JavaScript.
3822 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3824 // Set JavaScript according to the domain settings.
3825 nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled());
3828 // Close the current host domain settings cursor.
3829 currentDomainSettingsCursor.close();
3831 // Apply the domain settings.
3832 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3834 // Set third-party cookies status if API >= 21.
3835 if (Build.VERSION.SDK_INT >= 21) {
3836 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3839 // Apply the form data setting if the API < 26.
3840 if (Build.VERSION.SDK_INT < 26) {
3841 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3844 // Apply the font size.
3845 if (fontSize == 0) { // Apply the default font size.
3846 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3847 } else { // Apply the specified font size.
3848 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3851 // Set the user agent.
3852 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3853 // Get the array position of the default user agent name.
3854 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3856 // Set the user agent according to the system default.
3857 switch (defaultUserAgentArrayPosition) {
3858 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3859 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3860 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3863 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3864 // Set the user agent to `""`, which uses the default value.
3865 nestedScrollWebView.getSettings().setUserAgentString("");
3868 case SETTINGS_CUSTOM_USER_AGENT:
3869 // Set the default custom user agent.
3870 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3874 // Get the user agent string from the user agent data array
3875 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3877 } else { // Set the user agent according to the stored name.
3878 // Get the array position of the user agent name.
3879 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3881 switch (userAgentArrayPosition) {
3882 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3883 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3886 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3887 // Set the user agent to `""`, which uses the default value.
3888 nestedScrollWebView.getSettings().setUserAgentString("");
3892 // Get the user agent string from the user agent data array.
3893 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3897 // Set swipe to refresh.
3898 switch (swipeToRefreshInt) {
3899 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3900 // Store the swipe to refresh status in the nested scroll WebView.
3901 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3903 // 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.
3904 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3907 case DomainsDatabaseHelper.ENABLED:
3908 // Store the swipe to refresh status in the nested scroll WebView.
3909 nestedScrollWebView.setSwipeToRefresh(true);
3911 // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3912 swipeRefreshLayout.setEnabled(true);
3915 case DomainsDatabaseHelper.DISABLED:
3916 // Store the swipe to refresh status in the nested scroll WebView.
3917 nestedScrollWebView.setSwipeToRefresh(false);
3919 // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3920 swipeRefreshLayout.setEnabled(false);
3923 // Set the viewport.
3924 switch (wideViewportInt) {
3925 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3926 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
3929 case DomainsDatabaseHelper.ENABLED:
3930 nestedScrollWebView.getSettings().setUseWideViewPort(true);
3933 case DomainsDatabaseHelper.DISABLED:
3934 nestedScrollWebView.getSettings().setUseWideViewPort(false);
3938 // Set the loading of webpage images.
3939 switch (displayWebpageImagesInt) {
3940 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3941 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3944 case DomainsDatabaseHelper.ENABLED:
3945 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3948 case DomainsDatabaseHelper.DISABLED:
3949 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3953 // 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.
3955 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3957 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3959 } else { // The new URL does not have custom domain settings. Load the defaults.
3960 // Store the values from the shared preferences.
3961 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3962 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
3963 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3964 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3965 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3966 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true));
3967 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3968 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3969 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3970 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, sharedPreferences.getBoolean("ultralist", true));
3971 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3972 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3973 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3975 // Enable JavaScript if night mode is enabled.
3976 if (nestedScrollWebView.getNightMode()) {
3977 // Enable JavaScript.
3978 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3980 // Set JavaScript according to the domain settings.
3981 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3984 // Apply the default settings.
3985 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3986 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3988 // Apply the form data setting if the API < 26.
3989 if (Build.VERSION.SDK_INT < 26) {
3990 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3993 // Store the swipe to refresh status in the nested scroll WebView.
3994 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3996 // Apply swipe to refresh according to the default.
3997 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3999 // Reset the pinned variables.
4000 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
4002 // Set third-party cookies status if API >= 21.
4003 if (Build.VERSION.SDK_INT >= 21) {
4004 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
4007 // Get the array position of the user agent name.
4008 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4010 // Set the user agent.
4011 switch (userAgentArrayPosition) {
4012 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
4013 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4014 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4017 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4018 // Set the user agent to `""`, which uses the default value.
4019 nestedScrollWebView.getSettings().setUserAgentString("");
4022 case SETTINGS_CUSTOM_USER_AGENT:
4023 // Set the default custom user agent.
4024 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4028 // Get the user agent string from the user agent data array
4029 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4032 // Set the viewport.
4033 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4035 // Set the loading of webpage images.
4036 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4038 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
4039 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4042 // Close the domains database helper.
4043 domainsDatabaseHelper.close();
4045 // Update the privacy icons.
4046 updatePrivacyIcons(true);
4049 // Reload the website if returning from the Domains activity.
4050 if (reloadWebsite) {
4051 nestedScrollWebView.reload();
4054 // Return the user agent changed status.
4055 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
4058 private void applyProxyThroughOrbot(boolean reloadWebsite) {
4059 // Get a handle for the shared preferences.
4060 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4062 // Get the search and theme preferences.
4063 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
4064 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
4065 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
4066 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4067 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4069 // Get a handle for the app bar layout.
4070 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4072 // Set the homepage, search, and proxy options.
4073 if (proxyThroughOrbot) { // Set the Tor options.
4074 // Set the search URL.
4075 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
4076 searchURL = torSearchCustomUrlString;
4077 } else { // Use the string from the pre-built list.
4078 searchURL = torSearchString;
4081 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
4082 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4084 // Set the app bar background to indicate proxying through Orbot is enabled.
4086 appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4088 appBarLayout.setBackgroundResource(R.color.blue_50);
4091 // Check to see if Orbot is ready.
4092 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
4093 // Set `waitingForOrbot`.
4094 waitingForOrbot = true;
4096 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4097 currentWebView.getSettings().setUseWideViewPort(false);
4099 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
4100 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
4101 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
4102 // Reload the website.
4103 currentWebView.reload();
4105 } else { // Set the non-Tor options.
4106 // Set the search URL.
4107 if (searchString.equals("Custom URL")) { // Get the custom URL string.
4108 searchURL = searchCustomUrlString;
4109 } else { // Use the string from the pre-built list.
4110 searchURL = searchString;
4113 // Reset the proxy to default. The host is `""` and the port is `"0"`.
4114 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4116 // Set the default app bar layout background.
4118 appBarLayout.setBackgroundResource(R.color.gray_900);
4120 appBarLayout.setBackgroundResource(R.color.gray_100);
4123 // Reset `waitingForOrbot.
4124 waitingForOrbot = false;
4126 // Reload the WebViews if requested.
4127 if (reloadWebsite) {
4128 // Reload the WebViews.
4129 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4130 // Get the WebView tab fragment.
4131 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4133 // Get the fragment view.
4134 View fragmentView = webViewTabFragment.getView();
4136 // Only reload the WebViews if they exist.
4137 if (fragmentView != null) {
4138 // Get the nested scroll WebView from the tab fragment.
4139 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4141 // Reload the WebView.
4142 nestedScrollWebView.reload();
4149 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4150 // Only update the privacy icons if the options menu and the current WebView have already been populated.
4151 if ((optionsMenu != null) && (currentWebView != null)) {
4152 // Get a handle for the shared preferences.
4153 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4155 // Get the theme and screenshot preferences.
4156 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4158 // Get handles for the menu items.
4159 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
4160 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
4161 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
4162 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
4164 // Update the privacy icon.
4165 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
4166 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4167 } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled.
4168 privacyMenuItem.setIcon(R.drawable.warning);
4169 } else { // All the dangerous features are disabled.
4170 privacyMenuItem.setIcon(R.drawable.privacy_mode);
4173 // Update the first-party cookies icon.
4174 if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled.
4175 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4176 } else { // First-party cookies are disabled.
4178 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4180 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4184 // Update the DOM storage icon.
4185 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
4186 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4187 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
4189 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4191 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4193 } else { // JavaScript is disabled, so DOM storage is ghosted.
4195 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4197 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4201 // Update the refresh icon.
4203 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4205 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4208 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4209 if (runInvalidateOptionsMenu) {
4210 invalidateOptionsMenu();
4215 private void openUrlWithExternalApp(String url) {
4216 // Create a download intent. Not specifying the action type will display the maximum number of options.
4217 Intent downloadIntent = new Intent();
4219 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4220 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4222 // Flag the intent to open in a new task.
4223 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4225 // Show the chooser.
4226 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4229 private void highlightUrlText() {
4230 // Get a handle for the URL edit text.
4231 EditText urlEditText = findViewById(R.id.url_edittext);
4233 // Only highlight the URL text if the box is not currently selected.
4234 if (!urlEditText.hasFocus()) {
4235 // Get the URL string.
4236 String urlString = urlEditText.getText().toString();
4238 // Highlight the URL according to the protocol.
4239 if (urlString.startsWith("file://")) { // This is a file URL.
4240 // De-emphasize only the protocol.
4241 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4242 } else if (urlString.startsWith("content://")) {
4243 // De-emphasize only the protocol.
4244 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4245 } else { // This is a web URL.
4246 // Get the index of the `/` immediately after the domain name.
4247 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4249 // Create a base URL string.
4252 // Get the base URL.
4253 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4254 // Get the base URL.
4255 baseUrl = urlString.substring(0, endOfDomainName);
4256 } else { // There are no characters after the base URL.
4257 // Set the base URL to be the entire URL string.
4258 baseUrl = urlString;
4261 // Get the index of the last `.` in the domain.
4262 int lastDotIndex = baseUrl.lastIndexOf(".");
4264 // Get the index of the penultimate `.` in the domain.
4265 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4267 // Markup the beginning of the URL.
4268 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4269 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4271 // De-emphasize subdomains.
4272 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4273 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4275 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4276 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4277 // De-emphasize the protocol and the additional subdomains.
4278 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4279 } else { // There is only one subdomain in the domain name.
4280 // De-emphasize only the protocol.
4281 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4285 // De-emphasize the text after the domain name.
4286 if (endOfDomainName > 0) {
4287 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4293 private void loadBookmarksFolder() {
4294 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4295 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4297 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4298 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4300 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4301 // Inflate the individual item layout. `false` does not attach it to the root.
4302 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4306 public void bindView(View view, Context context, Cursor cursor) {
4307 // Get handles for the views.
4308 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4309 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4311 // Get the favorite icon byte array from the cursor.
4312 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4314 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4315 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4317 // Display the bitmap in `bookmarkFavoriteIcon`.
4318 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4320 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4321 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4322 bookmarkNameTextView.setText(bookmarkNameString);
4324 // Make the font bold for folders.
4325 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4326 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4327 } else { // Reset the font to default for normal bookmarks.
4328 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4333 // Get a handle for the bookmarks list view.
4334 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4336 // Populate the list view with the adapter.
4337 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4339 // Get a handle for the bookmarks title text view.
4340 TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4342 // Set the bookmarks drawer title.
4343 if (currentBookmarksFolder.isEmpty()) {
4344 bookmarksTitleTextView.setText(R.string.bookmarks);
4346 bookmarksTitleTextView.setText(currentBookmarksFolder);
4350 private void openWithApp(String url) {
4351 // Create the open with intent with `ACTION_VIEW`.
4352 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4354 // Set the URI but not the MIME type. This should open all available apps.
4355 openWithAppIntent.setData(Uri.parse(url));
4357 // Flag the intent to open in a new task.
4358 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4361 // Show the chooser.
4362 startActivity(openWithAppIntent);
4363 } catch (ActivityNotFoundException exception) {
4364 // Show a snackbar with the error.
4365 Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
4369 private void openWithBrowser(String url) {
4370 // Create the open with intent with `ACTION_VIEW`.
4371 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4373 // Set the URI and the MIME type. `"text/html"` should load browser options.
4374 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4376 // Flag the intent to open in a new task.
4377 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4380 // Show the chooser.
4381 startActivity(openWithBrowserIntent);
4382 } catch (ActivityNotFoundException exception) {
4383 // Show a snackbar with the error.
4384 Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
4388 private String sanitizeUrl(String url) {
4389 // Sanitize Google Analytics.
4390 if (sanitizeGoogleAnalytics) {
4392 if (url.contains("?utm_")) {
4393 url = url.substring(0, url.indexOf("?utm_"));
4397 if (url.contains("&utm_")) {
4398 url = url.substring(0, url.indexOf("&utm_"));
4402 // Sanitize Facebook Click IDs.
4403 if (sanitizeFacebookClickIds) {
4404 // Remove `?fbclid=`.
4405 if (url.contains("?fbclid=")) {
4406 url = url.substring(0, url.indexOf("?fbclid="));
4409 // Remove `&fbclid=`.
4410 if (url.contains("&fbclid=")) {
4411 url = url.substring(0, url.indexOf("&fbclid="));
4415 // Sanitize Twitter AMP redirects.
4416 if (sanitizeTwitterAmpRedirects) {
4418 if (url.contains("?amp=1")) {
4419 url = url.substring(0, url.indexOf("?amp=1"));
4423 // Return the sanitized URL.
4427 public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4428 // Store the blocklists.
4429 easyList = combinedBlocklists.get(0);
4430 easyPrivacy = combinedBlocklists.get(1);
4431 fanboysAnnoyanceList = combinedBlocklists.get(2);
4432 fanboysSocialList = combinedBlocklists.get(3);
4433 ultraList = combinedBlocklists.get(4);
4434 ultraPrivacy = combinedBlocklists.get(5);
4436 // Add the first tab.
4440 public void addTab(View view) {
4441 // Add a new tab with a blank URL.
4445 private void addNewTab(String url) {
4446 // Sanitize the URL.
4447 url = sanitizeUrl(url);
4449 // Get a handle for the tab layout and the view pager.
4450 TabLayout tabLayout = findViewById(R.id.tablayout);
4451 ViewPager webViewPager = findViewById(R.id.webviewpager);
4453 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4454 int newTabNumber = tabLayout.getTabCount();
4457 tabLayout.addTab(tabLayout.newTab());
4460 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4462 // Remove the lint warning below that the current tab might be null.
4463 assert newTab != null;
4465 // Set a custom view on the new tab.
4466 newTab.setCustomView(R.layout.tab_custom_view);
4468 // Add the new WebView page.
4469 webViewPagerAdapter.addPage(newTabNumber, webViewPager, url);
4472 public void closeTab(View view) {
4473 // Get a handle for the tab layout.
4474 TabLayout tabLayout = findViewById(R.id.tablayout);
4476 // Run the command according to the number of tabs.
4477 if (tabLayout.getTabCount() > 1) { // There is more than one tab open.
4478 // Close the current tab.
4480 } else { // There is only one tab open.
4485 private void closeCurrentTab() {
4486 // Get handles for the views.
4487 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4488 TabLayout tabLayout = findViewById(R.id.tablayout);
4489 ViewPager webViewPager = findViewById(R.id.webviewpager);
4491 // Get the current tab number.
4492 int currentTabNumber = tabLayout.getSelectedTabPosition();
4494 // Delete the current tab.
4495 tabLayout.removeTabAt(currentTabNumber);
4497 // 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.
4498 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4499 setCurrentWebView(currentTabNumber);
4502 // Expand the app bar if it is currently collapsed.
4503 appBarLayout.setExpanded(true);
4506 private void clearAndExit() {
4507 // Get a handle for the shared preferences.
4508 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4510 // Close the bookmarks cursor and database.
4511 bookmarksCursor.close();
4512 bookmarksDatabaseHelper.close();
4514 // Get the status of the clear everything preference.
4515 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
4517 // Get a handle for the runtime.
4518 Runtime runtime = Runtime.getRuntime();
4520 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4521 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4522 String privateDataDirectoryString = getApplicationInfo().dataDir;
4525 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
4526 // The command to remove cookies changed slightly in API 21.
4527 if (Build.VERSION.SDK_INT >= 21) {
4528 CookieManager.getInstance().removeAllCookies(null);
4530 CookieManager.getInstance().removeAllCookie();
4533 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4535 // Two commands must be used because `Runtime.exec()` does not like `*`.
4536 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4537 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4539 // Wait until the processes have finished.
4540 deleteCookiesProcess.waitFor();
4541 deleteCookiesJournalProcess.waitFor();
4542 } catch (Exception exception) {
4543 // Do nothing if an error is thrown.
4547 // Clear DOM storage.
4548 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
4549 // Ask `WebStorage` to clear the DOM storage.
4550 WebStorage webStorage = WebStorage.getInstance();
4551 webStorage.deleteAllData();
4553 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4555 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4556 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4558 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4559 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4560 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4561 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4562 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4564 // Wait until the processes have finished.
4565 deleteLocalStorageProcess.waitFor();
4566 deleteIndexProcess.waitFor();
4567 deleteQuotaManagerProcess.waitFor();
4568 deleteQuotaManagerJournalProcess.waitFor();
4569 deleteDatabaseProcess.waitFor();
4570 } catch (Exception exception) {
4571 // Do nothing if an error is thrown.
4575 // Clear form data if the API < 26.
4576 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
4577 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4578 webViewDatabase.clearFormData();
4580 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4582 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4583 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4584 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4586 // Wait until the processes have finished.
4587 deleteWebDataProcess.waitFor();
4588 deleteWebDataJournalProcess.waitFor();
4589 } catch (Exception exception) {
4590 // Do nothing if an error is thrown.
4595 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
4596 // Clear the cache from each WebView.
4597 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4598 // Get the WebView tab fragment.
4599 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4601 // Get the fragment view.
4602 View fragmentView = webViewTabFragment.getView();
4604 // Only clear the cache if the WebView exists.
4605 if (fragmentView != null) {
4606 // Get the nested scroll WebView from the tab fragment.
4607 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4609 // Clear the cache for this WebView.
4610 nestedScrollWebView.clearCache(true);
4614 // Manually delete the cache directories.
4616 // Delete the main cache directory.
4617 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4619 // Delete the secondary `Service Worker` cache directory.
4620 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4621 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
4623 // Wait until the processes have finished.
4624 deleteCacheProcess.waitFor();
4625 deleteServiceWorkerProcess.waitFor();
4626 } catch (Exception exception) {
4627 // Do nothing if an error is thrown.
4631 // Wipe out each WebView.
4632 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4633 // Get the WebView tab fragment.
4634 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4636 // Get the fragment view.
4637 View fragmentView = webViewTabFragment.getView();
4639 // Only wipe out the WebView if it exists.
4640 if (fragmentView != null) {
4641 // Get the nested scroll WebView from the tab fragment.
4642 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4644 // Clear SSL certificate preferences for this WebView.
4645 nestedScrollWebView.clearSslPreferences();
4647 // Clear the back/forward history for this WebView.
4648 nestedScrollWebView.clearHistory();
4650 // Destroy the internal state of `mainWebView`.
4651 nestedScrollWebView.destroy();
4655 // Clear the custom headers.
4656 customHeaders.clear();
4658 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4659 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4660 if (clearEverything) {
4662 // Delete the folder.
4663 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
4665 // Wait until the process has finished.
4666 deleteAppWebviewProcess.waitFor();
4667 } catch (Exception exception) {
4668 // Do nothing if an error is thrown.
4672 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4673 if (Build.VERSION.SDK_INT >= 21) {
4674 finishAndRemoveTask();
4679 // Remove the terminated program from RAM. The status code is `0`.
4683 private void setCurrentWebView(int pageNumber) {
4684 // Get a handle for the shared preferences.
4685 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4687 // Get the theme preference.
4688 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4690 // Get handles for the URL views.
4691 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4692 EditText urlEditText = findViewById(R.id.url_edittext);
4693 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4695 // Stop the swipe to refresh indicator if it is running
4696 swipeRefreshLayout.setRefreshing(false);
4698 // Get the WebView tab fragment.
4699 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4701 // Get the fragment view.
4702 View fragmentView = webViewTabFragment.getView();
4704 // Set the current WebView if the fragment view is not null.
4705 if (fragmentView != null) { // The fragment has been populated.
4706 // Store the current WebView.
4707 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4709 // Update the status of swipe to refresh.
4710 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
4711 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
4712 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4713 } else { // Swipe to refresh is disabled.
4714 // Disable the swipe refresh layout.
4715 swipeRefreshLayout.setEnabled(false);
4718 // Get a handle for the cookie manager.
4719 CookieManager cookieManager = CookieManager.getInstance();
4721 // Set the first-party cookie status.
4722 cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
4724 // Update the privacy icons. `true` redraws the icons in the app bar.
4725 updatePrivacyIcons(true);
4727 // Get a handle for the input method manager.
4728 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4730 // Remove the lint warning below that the input method manager might be null.
4731 assert inputMethodManager != null;
4733 // Get the current URL.
4734 String url = currentWebView.getUrl();
4736 // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4737 if (!loadingNewIntent) { // A new intent is not being loaded.
4738 if ((url == null) || url.equals("about:blank")) { // The WebView is blank.
4739 // Display the hint in the URL edit text.
4740 urlEditText.setText("");
4742 // Request focus for the URL text box.
4743 urlEditText.requestFocus();
4745 // Display the keyboard.
4746 inputMethodManager.showSoftInput(urlEditText, 0);
4747 } else { // The WebView has a loaded URL.
4748 // Clear the focus from the URL text box.
4749 urlEditText.clearFocus();
4751 // Hide the soft keyboard.
4752 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4754 // Display the current URL in the URL text box.
4755 urlEditText.setText(url);
4757 // Highlight the URL text.
4760 } else { // A new intent is being loaded.
4761 // Reset the loading new intent tracker.
4762 loadingNewIntent = false;
4765 // Set the background to indicate the domain settings status.
4766 if (currentWebView.getDomainSettingsApplied()) {
4767 // 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.
4769 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4771 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4774 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4776 } else { // The fragment has not been populated. Try again in 100 milliseconds.
4777 // Create a handler to set the current WebView.
4778 Handler setCurrentWebViewHandler = new Handler();
4780 // Create a runnable to set the current WebView.
4781 Runnable setCurrentWebWebRunnable = () -> {
4782 // Set the current WebView.
4783 setCurrentWebView(pageNumber);
4786 // Try setting the current WebView again after 100 milliseconds.
4787 setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
4792 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) {
4793 // Get handles for the activity views.
4794 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4795 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4796 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4797 ActionBar actionBar = getSupportActionBar();
4798 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
4799 EditText urlEditText = findViewById(R.id.url_edittext);
4800 TabLayout tabLayout = findViewById(R.id.tablayout);
4801 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4803 // Remove the incorrect lint warning below that the action bar might be null.
4804 assert actionBar != null;
4806 // Get a handle for the activity
4807 Activity activity = this;
4809 // Get a handle for the input method manager.
4810 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4812 // Instantiate the blocklist helper.
4813 BlocklistHelper blocklistHelper = new BlocklistHelper();
4815 // Remove the lint warning below that the input method manager might be null.
4816 assert inputMethodManager != null;
4818 // Get a handle for the shared preferences.
4819 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4821 // Initialize the favorite icon.
4822 nestedScrollWebView.initializeFavoriteIcon();
4824 // Set the app bar scrolling.
4825 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4827 // Allow pinch to zoom.
4828 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4830 // Hide zoom controls.
4831 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4833 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4834 if (Build.VERSION.SDK_INT >= 21) {
4835 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4838 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4839 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4841 // Explicitly disable geolocation.
4842 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4844 // Create a double-tap gesture detector to toggle full-screen mode.
4845 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4846 // Override `onDoubleTap()`. All other events are handled using the default settings.
4848 public boolean onDoubleTap(MotionEvent event) {
4849 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4850 // Toggle the full screen browsing mode tracker.
4851 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4853 // Toggle the full screen browsing mode.
4854 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4855 // Store the swipe refresh layout top padding.
4856 swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop();
4858 // Hide the app bar if specified.
4860 // Close the find on page bar if it is visible.
4861 closeFindOnPage(null);
4863 // Hide the tab linear layout.
4864 tabsLinearLayout.setVisibility(View.GONE);
4866 // Hide the action bar.
4869 // Check to see if app bar scrolling is disabled.
4870 if (!scrollAppBar) {
4871 // Remove the padding from the top of the swipe refresh layout.
4872 swipeRefreshLayout.setPadding(0, 0, 0, 0);
4876 // Hide the banner ad in the free flavor.
4877 if (BuildConfig.FLAVOR.contentEquals("free")) {
4878 AdHelper.hideAd(findViewById(R.id.adview));
4881 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4882 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4884 /* Hide the system bars.
4885 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4886 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4887 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4888 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4890 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4891 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4892 } else { // Switch to normal viewing mode.
4893 // Show the tab linear layout.
4894 tabsLinearLayout.setVisibility(View.VISIBLE);
4896 // Show the action bar.
4899 // Check to see if app bar scrolling is disabled.
4900 if (!scrollAppBar) {
4901 // Add the padding from the top of the swipe refresh layout.
4902 swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0);
4905 // Show the banner ad in the free flavor.
4906 if (BuildConfig.FLAVOR.contentEquals("free")) {
4908 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4911 // Remove the `SYSTEM_UI` flags from the root frame layout.
4912 rootFrameLayout.setSystemUiVisibility(0);
4914 // Add the translucent status flag.
4915 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4918 // Consume the double-tap.
4920 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4926 // Pass all touch events on the WebView through the double-tap gesture detector.
4927 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4928 // Call `performClick()` on the view, which is required for accessibility.
4929 view.performClick();
4931 // Send the event to the gesture detector.
4932 return doubleTapGestureDetector.onTouchEvent(event);
4935 // Register the WebView for a context menu. This is used to see link targets and download images.
4936 registerForContextMenu(nestedScrollWebView);
4938 // Allow the downloading of files.
4939 nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4940 // Check if the download should be processed by an external app.
4941 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
4942 // Create a download intent. Not specifying the action type will display the maximum number of options.
4943 Intent downloadIntent = new Intent();
4945 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4946 downloadIntent.setDataAndType(Uri.parse(downloadUrl), "text/html");
4948 // Flag the intent to open in a new task.
4949 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4951 // Show the chooser.
4952 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4953 } else { // Download with Android's download manager.
4954 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4955 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4956 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4958 // Store the variables for future use by `onRequestPermissionsResult()`.
4959 this.downloadUrl = downloadUrl;
4960 downloadContentDisposition = contentDisposition;
4961 downloadContentLength = contentLength;
4963 // Show a dialog if the user has previously denied the permission.
4964 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4965 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4966 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4968 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4969 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4970 } else { // Show the permission request directly.
4971 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4972 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4974 } else { // The storage permission has already been granted.
4975 // Get a handle for the download file alert dialog.
4976 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, contentDisposition, contentLength);
4978 // Show the download file alert dialog.
4979 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4984 // Update the find on page count.
4985 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4986 // Get a handle for `findOnPageCountTextView`.
4987 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4990 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4991 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4992 // Set `findOnPageCountTextView` to `0/0`.
4993 findOnPageCountTextView.setText(R.string.zero_of_zero);
4994 } else if (isDoneCounting) { // There are matches.
4995 // `activeMatchOrdinal` is zero-based.
4996 int activeMatch = activeMatchOrdinal + 1;
4998 // Build the match string.
4999 String matchString = activeMatch + "/" + numberOfMatches;
5001 // Set `findOnPageCountTextView`.
5002 findOnPageCountTextView.setText(matchString);
5007 // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. Also reinforce full screen browsing mode.
5008 // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`.
5009 nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
5010 if (nestedScrollWebView.getSwipeToRefresh()) {
5011 // Only enable swipe to refresh if the WebView is scrolled to the top.
5012 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5015 // Reinforce the system UI visibility flags if in full screen browsing mode.
5016 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5017 if (inFullScreenBrowsingMode) {
5018 /* Hide the system bars.
5019 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5020 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5021 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5022 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5024 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5025 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5029 // Set the web chrome client.
5030 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5031 // Update the progress bar when a page is loading.
5033 public void onProgressChanged(WebView view, int progress) {
5034 // Inject the night mode CSS if night mode is enabled.
5035 if (nestedScrollWebView.getNightMode()) { // Night mode is enabled.
5036 // `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
5037 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
5038 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
5039 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
5040 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
5041 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
5042 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
5043 // Initialize a handler to display `mainWebView`.
5044 Handler displayWebViewHandler = new Handler();
5046 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
5047 Runnable displayWebViewRunnable = () -> {
5048 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
5049 if (progressBar.getVisibility() == View.GONE) {
5050 nestedScrollWebView.setVisibility(View.VISIBLE);
5054 // Display the WebView after 500 milliseconds.
5055 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
5057 } else { // Night mode is disabled.
5058 // Display the nested scroll WebView if night mode is disabled.
5059 // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
5060 // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
5061 nestedScrollWebView.setVisibility(View.VISIBLE);
5064 // Update the progress bar.
5065 progressBar.setProgress(progress);
5067 // Set the visibility of the progress bar.
5068 if (progress < 100) {
5069 // Show the progress bar.
5070 progressBar.setVisibility(View.VISIBLE);
5072 // Hide the progress bar.
5073 progressBar.setVisibility(View.GONE);
5075 //Stop the swipe to refresh indicator if it is running
5076 swipeRefreshLayout.setRefreshing(false);
5080 // Set the favorite icon when it changes.
5082 public void onReceivedIcon(WebView view, Bitmap icon) {
5083 // Only update the favorite icon if the website has finished loading.
5084 if (progressBar.getVisibility() == View.GONE) {
5085 // Store the new favorite icon.
5086 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5088 // Get the current page position.
5089 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5091 // Get the current tab.
5092 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5094 // Check to see if the tab has been populated.
5096 // Get the custom view from the tab.
5097 View tabView = tab.getCustomView();
5099 // Check to see if the custom tab view has been populated.
5100 if (tabView != null) {
5101 // Get the favorite icon image view from the tab.
5102 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5104 // Display the favorite icon in the tab.
5105 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5111 // Save a copy of the title when it changes.
5113 public void onReceivedTitle(WebView view, String title) {
5114 // Get the current page position.
5115 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5117 // Get the current tab.
5118 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5120 // Only populate the title text view if the tab has been fully created.
5122 // Get the custom view from the tab.
5123 View tabView = tab.getCustomView();
5125 // Remove the incorrect warning below that the current tab view might be null.
5126 assert tabView != null;
5128 // Get the title text view from the tab.
5129 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5131 // Set the title according to the URL.
5132 if (title.equals("about:blank")) {
5133 // Set the title to indicate a new tab.
5134 tabTitleTextView.setText(R.string.new_tab);
5136 // Set the title as the tab text.
5137 tabTitleTextView.setText(title);
5142 // Enter full screen video.
5144 public void onShowCustomView(View video, CustomViewCallback callback) {
5145 // Get a handle for the full screen video frame layout.
5146 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5148 // Set the full screen video flag.
5149 displayingFullScreenVideo = true;
5151 // Pause the ad if this is the free flavor.
5152 if (BuildConfig.FLAVOR.contentEquals("free")) {
5153 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
5154 AdHelper.pauseAd(findViewById(R.id.adview));
5157 // Hide the keyboard.
5158 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5160 // Hide the main content relative layout.
5161 mainContentRelativeLayout.setVisibility(View.GONE);
5163 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
5164 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
5166 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
5167 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5169 /* Hide the system bars.
5170 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5171 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5172 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5173 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5175 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5176 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5178 // Disable the sliding drawers.
5179 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5181 // Add the video view to the full screen video frame layout.
5182 fullScreenVideoFrameLayout.addView(video);
5184 // Show the full screen video frame layout.
5185 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5188 // Exit full screen video.
5190 public void onHideCustomView() {
5191 // Get a handle for the full screen video frame layout.
5192 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5194 // Unset the full screen video flag.
5195 displayingFullScreenVideo = false;
5197 // Remove all the views from the full screen video frame layout.
5198 fullScreenVideoFrameLayout.removeAllViews();
5200 // Hide the full screen video frame layout.
5201 fullScreenVideoFrameLayout.setVisibility(View.GONE);
5203 // Enable the sliding drawers.
5204 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
5206 // Show the main content relative layout.
5207 mainContentRelativeLayout.setVisibility(View.VISIBLE);
5209 // Apply the appropriate full screen mode flags.
5210 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
5211 // Hide the app bar if specified.
5213 // Hide the tab linear layout.
5214 tabsLinearLayout.setVisibility(View.GONE);
5216 // Hide the action bar.
5220 // Hide the banner ad in the free flavor.
5221 if (BuildConfig.FLAVOR.contentEquals("free")) {
5222 AdHelper.hideAd(findViewById(R.id.adview));
5225 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
5226 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5228 /* Hide the system bars.
5229 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5230 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5231 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5232 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5234 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5235 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5236 } else { // Switch to normal viewing mode.
5237 // Remove the `SYSTEM_UI` flags from the root frame layout.
5238 rootFrameLayout.setSystemUiVisibility(0);
5240 // Add the translucent status flag.
5241 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5244 // Reload the ad for the free flavor if not in full screen mode.
5245 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
5247 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
5253 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5254 // Show the file chooser if the device is running API >= 21.
5255 if (Build.VERSION.SDK_INT >= 21) {
5256 // Store the file path callback.
5257 fileChooserCallback = filePathCallback;
5259 // Create an intent to open a chooser based ont the file chooser parameters.
5260 Intent fileChooserIntent = fileChooserParams.createIntent();
5262 // Open the file chooser.
5263 startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
5269 nestedScrollWebView.setWebViewClient(new WebViewClient() {
5270 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
5271 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5273 public boolean shouldOverrideUrlLoading(WebView view, String url) {
5274 // Sanitize the url.
5275 url = sanitizeUrl(url);
5277 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
5278 // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed.
5279 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5281 // Check if the user agent has changed.
5282 if (userAgentChanged) {
5283 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
5284 nestedScrollWebView.loadUrl(url, customHeaders);
5286 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5289 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
5292 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
5293 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5294 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5296 // Parse the url and set it as the data for the intent.
5297 emailIntent.setData(Uri.parse(url));
5299 // Open the email program in a new task instead of as part of Privacy Browser.
5300 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5303 startActivity(emailIntent);
5305 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5307 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
5308 // Open the dialer and load the phone number, but wait for the user to place the call.
5309 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5311 // Add the phone number to the intent.
5312 dialIntent.setData(Uri.parse(url));
5314 // Open the dialer in a new task instead of as part of Privacy Browser.
5315 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5318 startActivity(dialIntent);
5320 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5322 } else { // Load a system chooser to select an app that can handle the URL.
5323 // Open an app that can handle the URL.
5324 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5326 // Add the URL to the intent.
5327 genericIntent.setData(Uri.parse(url));
5329 // List all apps that can handle the URL instead of just opening the first one.
5330 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5332 // Open the app in a new task instead of as part of Privacy Browser.
5333 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5335 // Start the app or display a snackbar if no app is available to handle the URL.
5337 startActivity(genericIntent);
5338 } catch (ActivityNotFoundException exception) {
5339 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
5342 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5347 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5349 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
5350 // Check to see if the resource request is for the main URL.
5351 if (url.equals(nestedScrollWebView.getCurrentUrl())) {
5352 // `return null` loads the resource request, which should never be blocked if it is the main URL.
5356 // Wait until the blocklists have been populated. When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately.
5357 while (ultraPrivacy == null) {
5358 // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5359 synchronized (this) {
5361 // Check to see if the blocklists have been populated after 100 ms.
5363 } catch (InterruptedException exception) {
5369 // Sanitize the URL.
5370 url = sanitizeUrl(url);
5372 // Get a handle for the navigation view.
5373 NavigationView navigationView = findViewById(R.id.navigationview);
5375 // Get a handle for the navigation menu.
5376 Menu navigationMenu = navigationView.getMenu();
5378 // Get a handle for the navigation requests menu item. The menu is 0 based.
5379 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
5381 // Create an empty web resource response to be used if the resource request is blocked.
5382 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5384 // Reset the whitelist results tracker.
5385 String[] whitelistResultStringArray = null;
5387 // Initialize the third party request tracker.
5388 boolean isThirdPartyRequest = false;
5390 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5391 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5393 // Store a copy of the current domain for use in later requests.
5394 String currentDomain = currentBaseDomain;
5396 // Nobody is happy when comparing null strings.
5397 if ((currentBaseDomain != null) && (url != null)) {
5398 // Convert the request URL to a URI.
5399 Uri requestUri = Uri.parse(url);
5401 // Get the request host name.
5402 String requestBaseDomain = requestUri.getHost();
5404 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5405 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5406 // Determine the current base domain.
5407 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5408 // Remove the first subdomain.
5409 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5412 // Determine the request base domain.
5413 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5414 // Remove the first subdomain.
5415 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5418 // Update the third party request tracker.
5419 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5423 // Get the current WebView page position.
5424 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5426 // Determine if the WebView is currently displayed.
5427 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5429 // Block third-party requests if enabled.
5430 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
5431 // Add the result to the resource requests.
5432 nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5434 // Increment the blocked requests counters.
5435 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5436 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5438 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5439 if (webViewDisplayed) {
5440 // Updating the UI must be run from the UI thread.
5441 activity.runOnUiThread(() -> {
5442 // Update the menu item titles.
5443 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5445 // Update the options menu if it has been populated.
5446 if (optionsMenu != null) {
5447 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5448 optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5449 getString(R.string.block_all_third_party_requests));
5454 // Return an empty web resource response.
5455 return emptyWebResourceResponse;
5458 // Check UltraList if it is enabled.
5459 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)) {
5460 // Check the URL against UltraList.
5461 String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5463 // Process the UltraList results.
5464 if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraLists's blacklist.
5465 // Add the result to the resource requests.
5466 nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5468 // Increment the blocked requests counters.
5469 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5470 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5472 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5473 if (webViewDisplayed) {
5474 // Updating the UI must be run from the UI thread.
5475 activity.runOnUiThread(() -> {
5476 // Update the menu item titles.
5477 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5479 // Update the options menu if it has been populated.
5480 if (optionsMenu != null) {
5481 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5482 optionsMenu.findItem(R.id.ultralist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5487 // The resource request was blocked. Return an empty web resource response.
5488 return emptyWebResourceResponse;
5489 } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraList's whitelist.
5490 // Add a whitelist entry to the resource requests array.
5491 nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5493 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5498 // Check UltraPrivacy if it is enabled.
5499 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)) {
5500 // Check the URL against UltraPrivacy.
5501 String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5503 // Process the UltraPrivacy results.
5504 if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
5505 // Add the result to the resource requests.
5506 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5507 ultraPrivacyResults[5]});
5509 // Increment the blocked requests counters.
5510 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5511 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5513 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5514 if (webViewDisplayed) {
5515 // Updating the UI must be run from the UI thread.
5516 activity.runOnUiThread(() -> {
5517 // Update the menu item titles.
5518 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5520 // Update the options menu if it has been populated.
5521 if (optionsMenu != null) {
5522 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5523 optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5528 // The resource request was blocked. Return an empty web resource response.
5529 return emptyWebResourceResponse;
5530 } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
5531 // Add a whitelist entry to the resource requests array.
5532 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5533 ultraPrivacyResults[5]});
5535 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5540 // Check EasyList if it is enabled.
5541 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)) {
5542 // Check the URL against EasyList.
5543 String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5545 // Process the EasyList results.
5546 if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
5547 // Add the result to the resource requests.
5548 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5550 // Increment the blocked requests counters.
5551 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5552 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5554 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5555 if (webViewDisplayed) {
5556 // Updating the UI must be run from the UI thread.
5557 activity.runOnUiThread(() -> {
5558 // Update the menu item titles.
5559 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5561 // Update the options menu if it has been populated.
5562 if (optionsMenu != null) {
5563 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5564 optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5569 // The resource request was blocked. Return an empty web resource response.
5570 return emptyWebResourceResponse;
5571 } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
5572 // Update the whitelist result string array tracker.
5573 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5577 // Check EasyPrivacy if it is enabled.
5578 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)) {
5579 // Check the URL against EasyPrivacy.
5580 String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5582 // Process the EasyPrivacy results.
5583 if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
5584 // Add the result to the resource requests.
5585 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5586 easyPrivacyResults[5]});
5588 // Increment the blocked requests counters.
5589 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5590 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5592 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5593 if (webViewDisplayed) {
5594 // Updating the UI must be run from the UI thread.
5595 activity.runOnUiThread(() -> {
5596 // Update the menu item titles.
5597 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5599 // Update the options menu if it has been populated.
5600 if (optionsMenu != null) {
5601 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5602 optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
5607 // The resource request was blocked. Return an empty web resource response.
5608 return emptyWebResourceResponse;
5609 } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
5610 // Update the whitelist result string array tracker.
5611 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5615 // Check Fanboy’s Annoyance List if it is enabled.
5616 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
5617 // Check the URL against Fanboy's Annoyance List.
5618 String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5620 // Process the Fanboy's Annoyance List results.
5621 if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
5622 // Add the result to the resource requests.
5623 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5624 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5626 // Increment the blocked requests counters.
5627 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5628 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5630 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5631 if (webViewDisplayed) {
5632 // Updating the UI must be run from the UI thread.
5633 activity.runOnUiThread(() -> {
5634 // Update the menu item titles.
5635 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5637 // Update the options menu if it has been populated.
5638 if (optionsMenu != null) {
5639 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5640 optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5641 getString(R.string.fanboys_annoyance_list));
5646 // The resource request was blocked. Return an empty web resource response.
5647 return emptyWebResourceResponse;
5648 } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
5649 // Update the whitelist result string array tracker.
5650 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5651 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5653 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5654 // Check the URL against Fanboy's Annoyance List.
5655 String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5657 // Process the Fanboy's Social Blocking List results.
5658 if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5659 // Add the result to the resource requests.
5660 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5661 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5663 // Increment the blocked requests counters.
5664 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5665 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5667 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5668 if (webViewDisplayed) {
5669 // Updating the UI must be run from the UI thread.
5670 activity.runOnUiThread(() -> {
5671 // Update the menu item titles.
5672 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5674 // Update the options menu if it has been populated.
5675 if (optionsMenu != null) {
5676 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5677 optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5678 getString(R.string.fanboys_social_blocking_list));
5683 // The resource request was blocked. Return an empty web resource response.
5684 return emptyWebResourceResponse;
5685 } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5686 // Update the whitelist result string array tracker.
5687 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5688 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5692 // Add the request to the log because it hasn't been processed by any of the previous checks.
5693 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5694 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5695 } else { // The request didn't match any blocklist entry. Log it as a default request.
5696 nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
5699 // The resource request has not been blocked. `return null` loads the requested resource.
5703 // Handle HTTP authentication requests.
5705 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5706 // Store the handler.
5707 nestedScrollWebView.setHttpAuthHandler(handler);
5709 // Instantiate an HTTP authentication dialog.
5710 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5712 // Show the HTTP authentication dialog.
5713 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5717 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5718 // Get the preferences.
5719 boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
5720 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5722 // Get a handler for the app bar layout.
5723 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
5725 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.
5727 // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5728 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5730 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5731 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
5733 // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated.
5734 int appBarHeight = appBarLayout.getHeight();
5736 // The swipe refresh layout must be manually moved below the app bar layout.
5737 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5739 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5740 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5743 // Reset the list of resource requests.
5744 nestedScrollWebView.clearResourceRequests();
5746 // Reset the requests counters.
5747 nestedScrollWebView.resetRequestsCounters();
5749 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5750 if (nestedScrollWebView.getNightMode()) {
5751 nestedScrollWebView.setVisibility(View.INVISIBLE);
5753 nestedScrollWebView.setVisibility(View.VISIBLE);
5756 // Hide the keyboard.
5757 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5759 // Check to see if Privacy Browser is waiting on Orbot.
5760 if (!waitingForOrbot) { // Process the URL.
5761 // Get the current page position.
5762 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5764 // Update the URL text bar if the page is currently selected.
5765 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5766 // Clear the focus from the URL edit text.
5767 urlEditText.clearFocus();
5769 // Display the formatted URL text.
5770 urlEditText.setText(url);
5772 // Apply text highlighting to `urlTextBox`.
5776 // Reset the list of host IP addresses.
5777 nestedScrollWebView.clearCurrentIpAddresses();
5779 // Get a URI for the current URL.
5780 Uri currentUri = Uri.parse(url);
5782 // Get the IP addresses for the host.
5783 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5785 // Apply any custom domain settings if the URL was loaded by navigating history.
5786 if (nestedScrollWebView.getNavigatingHistory()) {
5787 // Reset navigating history.
5788 nestedScrollWebView.setNavigatingHistory(false);
5790 // Apply the domain settings.
5791 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5793 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5794 if (userAgentChanged) {
5799 // Replace Refresh with Stop if the options menu has been created. (The first WebView typically begins loading before the menu items are instantiated.)
5800 if (optionsMenu != null) {
5801 // Get a handle for the refresh menu item.
5802 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5805 refreshMenuItem.setTitle(R.string.stop);
5807 // Get the app bar and theme preferences.
5808 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5810 // If the icon is displayed in the AppBar, set it according to the theme.
5811 if (displayAdditionalAppBarIcons) {
5813 refreshMenuItem.setIcon(R.drawable.close_dark);
5815 refreshMenuItem.setIcon(R.drawable.close_light);
5823 public void onPageFinished(WebView view, String url) {
5824 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5825 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
5826 CookieManager.getInstance().flush();
5829 // Update the Refresh menu item if the options menu has been created.
5830 if (optionsMenu != null) {
5831 // Get a handle for the refresh menu item.
5832 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5834 // Reset the Refresh title.
5835 refreshMenuItem.setTitle(R.string.refresh);
5837 // Get the app bar and theme preferences.
5838 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5839 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5841 // If the icon is displayed in the AppBar, reset it according to the theme.
5842 if (displayAdditionalAppBarIcons) {
5844 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5846 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5851 // Clear the cache and history if Incognito Mode is enabled.
5852 if (incognitoModeEnabled) {
5853 // Clear the cache. `true` includes disk files.
5854 nestedScrollWebView.clearCache(true);
5856 // Clear the back/forward history.
5857 nestedScrollWebView.clearHistory();
5859 // Manually delete cache folders.
5861 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5862 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5863 String privateDataDirectoryString = getApplicationInfo().dataDir;
5865 // Delete the main cache directory.
5866 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5868 // Delete the secondary `Service Worker` cache directory.
5869 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5870 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5871 } catch (IOException e) {
5872 // Do nothing if an error is thrown.
5876 // Update the URL text box and apply domain settings if not waiting on Orbot.
5877 if (!waitingForOrbot) {
5878 // Get the current page position.
5879 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5881 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5882 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5883 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5884 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5887 // Get the current URL from the nested scroll WebView. This is more accurate than using the URL passed into the method, which is sometimes not the final one.
5888 String currentUrl = nestedScrollWebView.getUrl();
5890 // Get the current tab.
5891 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
5893 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5894 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5895 // Probably some sort of race condition when Privacy Browser is being resumed.
5896 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5897 // Check to see if the URL is `about:blank`.
5898 if (currentUrl.equals("about:blank")) { // The WebView is blank.
5899 // Display the hint in the URL edit text.
5900 urlEditText.setText("");
5902 // Request focus for the URL text box.
5903 urlEditText.requestFocus();
5905 // Display the keyboard.
5906 inputMethodManager.showSoftInput(urlEditText, 0);
5908 // Apply the domain settings. This clears any settings from the previous domain.
5909 applyDomainSettings(nestedScrollWebView, "", true, false);
5911 // Only populate the title text view if the tab has been fully created.
5913 // Get the custom view from the tab.
5914 View tabView = tab.getCustomView();
5916 // Remove the incorrect warning below that the current tab view might be null.
5917 assert tabView != null;
5919 // Get the title text view from the tab.
5920 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5922 // Set the title as the tab text.
5923 tabTitleTextView.setText(R.string.new_tab);
5925 } else { // The WebView has loaded a webpage.
5926 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5927 urlEditText.setText(currentUrl);
5929 // Apply text highlighting to the URL.
5932 // Only populate the title text view if the tab has been fully created.
5934 // Get the custom view from the tab.
5935 View tabView = tab.getCustomView();
5937 // Remove the incorrect warning below that the current tab view might be null.
5938 assert tabView != null;
5940 // Get the title text view from the tab.
5941 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5943 // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5944 tabTitleTextView.setText(nestedScrollWebView.getTitle());
5951 // Handle SSL Certificate errors.
5953 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5954 // Get the current website SSL certificate.
5955 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5957 // Extract the individual pieces of information from the current website SSL certificate.
5958 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5959 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5960 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5961 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5962 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5963 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5964 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5965 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5967 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5968 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5969 // Get the pinned SSL certificate.
5970 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5972 // Extract the arrays from the array list.
5973 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5974 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5976 // Check if the current SSL certificate matches the pinned certificate.
5977 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5978 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5979 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5980 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5982 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5985 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5986 // Store the SSL error handler.
5987 nestedScrollWebView.setSslErrorHandler(handler);
5989 // Instantiate an SSL certificate error alert dialog.
5990 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
5992 // Show the SSL certificate error dialog.
5993 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5998 // Check to see if this is the first page.
5999 if (pageNumber == 0) {
6000 // Set this nested scroll WebView as the current WebView.
6001 currentWebView = nestedScrollWebView;
6003 // Apply the app settings from the shared preferences.
6006 // Load the website if not waiting for Orbot to connect.
6007 if (!waitingForOrbot) {
6008 // Get the intent that started the app.
6009 Intent launchingIntent = getIntent();
6011 // Get the information from the intent.
6012 String launchingIntentAction = launchingIntent.getAction();
6013 Uri launchingIntentUriData = launchingIntent.getData();
6015 // If the intent action is a web search, perform the search.
6016 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
6017 // Create an encoded URL string.
6018 String encodedUrlString;
6020 // Sanitize the search input and convert it to a search.
6022 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6023 } catch (UnsupportedEncodingException exception) {
6024 encodedUrlString = "";
6027 // Load the completed search URL.
6028 loadUrl(searchURL + encodedUrlString);
6029 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
6030 // Load the URL from the intent.
6031 loadUrl(launchingIntentUriData.toString());
6032 } else { // The is no URL in the intent.
6033 // Select the homepage based on the proxy through Orbot status.
6034 if (proxyThroughOrbot) {
6035 // Load the Tor homepage.
6036 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
6038 // Load the normal homepage.
6039 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
6043 } else { // This is not the first tab.
6044 // Apply the domain settings.
6045 applyDomainSettings(nestedScrollWebView, url, false, false);
6048 nestedScrollWebView.loadUrl(url, customHeaders);
6050 // Set the focus and display the keyboard if the URL is blank.
6051 if (url.equals("")) {
6052 // Request focus for the URL text box.
6053 urlEditText.requestFocus();
6055 // Create a display keyboard handler.
6056 Handler displayKeyboardHandler = new Handler();
6058 // Create a display keyboard runnable.
6059 Runnable displayKeyboardRunnable = () -> {
6060 // Display the keyboard.
6061 inputMethodManager.showSoftInput(urlEditText, 0);
6064 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6065 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);