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 // Parse the URL into a URI.
3595 Uri uri = Uri.parse(url);
3597 // Extract the domain from `uri`.
3598 String newHostName = uri.getHost();
3600 // Strings don't like to be null.
3601 if (newHostName == null) {
3605 // 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.
3606 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3607 // Set the new host name as the current domain name.
3608 nestedScrollWebView.setCurrentDomainName(newHostName);
3610 // Reset the ignoring of pinned domain information.
3611 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3613 // Clear any pinned SSL certificate or IP addresses.
3614 nestedScrollWebView.clearPinnedSslCertificate();
3615 nestedScrollWebView.clearPinnedIpAddresses();
3617 // Reset the favorite icon if specified.
3619 // Initialize the favorite icon.
3620 nestedScrollWebView.initializeFavoriteIcon();
3622 // Get the current page position.
3623 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3625 // Get a handle for the tab layout.
3626 TabLayout tabLayout = findViewById(R.id.tablayout);
3628 // Get the corresponding tab.
3629 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3631 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3633 // Get the tab custom view.
3634 View tabCustomView = tab.getCustomView();
3636 // Remove the warning below that the tab custom view might be null.
3637 assert tabCustomView != null;
3639 // Get the tab views.
3640 ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3641 TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3643 // Set the default favorite icon as the favorite icon for this tab.
3644 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3646 // Set the loading title text.
3647 tabTitleTextView.setText(R.string.loading);
3651 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3652 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3654 // Get a full cursor from `domainsDatabaseHelper`.
3655 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3657 // Initialize `domainSettingsSet`.
3658 Set<String> domainSettingsSet = new HashSet<>();
3660 // Get the domain name column index.
3661 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3663 // Populate `domainSettingsSet`.
3664 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3665 // Move `domainsCursor` to the current row.
3666 domainNameCursor.moveToPosition(i);
3668 // Store the domain name in `domainSettingsSet`.
3669 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3672 // Close `domainNameCursor.
3673 domainNameCursor.close();
3675 // Initialize the domain name in database variable.
3676 String domainNameInDatabase = null;
3678 // Check the hostname against the domain settings set.
3679 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3680 // Record the domain name in the database.
3681 domainNameInDatabase = newHostName;
3683 // Set the domain settings applied tracker to true.
3684 nestedScrollWebView.setDomainSettingsApplied(true);
3685 } else { // The hostname is not contained in the domain settings set.
3686 // Set the domain settings applied tracker to false.
3687 nestedScrollWebView.setDomainSettingsApplied(false);
3690 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3691 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3692 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3693 // Set the domain settings applied tracker to true.
3694 nestedScrollWebView.setDomainSettingsApplied(true);
3696 // Store the applied domain names as it appears in the database.
3697 domainNameInDatabase = "*." + newHostName;
3700 // Strip out the lowest subdomain of of the host name.
3701 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3705 // Get a handle for the shared preferences.
3706 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3708 // Store the general preference information.
3709 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3710 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3711 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3712 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3713 boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true);
3714 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3716 // Get a handle for the cookie manager.
3717 CookieManager cookieManager = CookieManager.getInstance();
3719 // Get handles for the views.
3720 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3721 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3723 // Initialize the user agent array adapter and string array.
3724 ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3725 String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3727 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3728 // Get a cursor for the current host and move it to the first position.
3729 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3730 currentDomainSettingsCursor.moveToFirst();
3732 // Get the settings from the cursor.
3733 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3734 nestedScrollWebView.setDomainSettingsJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3735 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3736 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3737 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3738 // Form data can be removed once the minimum API >= 26.
3739 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3740 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST,
3741 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3742 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY,
3743 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3744 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3745 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3746 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3747 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3748 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)) == 1);
3749 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY,
3750 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3751 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3752 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3753 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3754 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3755 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3756 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3757 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT));
3758 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3759 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3760 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3761 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3762 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3763 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3764 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3765 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3766 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3767 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3769 // Create the pinned SSL date variables.
3770 Date pinnedSslStartDate;
3771 Date pinnedSslEndDate;
3773 // 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.
3774 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3775 pinnedSslStartDate = null;
3777 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3780 // 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.
3781 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3782 pinnedSslEndDate = null;
3784 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3787 // If there is a pinned SSL certificate, store it in the WebView.
3788 if (pinnedSslCertificate) {
3789 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3790 pinnedSslStartDate, pinnedSslEndDate);
3793 // If there is a pinned IP address, store it in the WebView.
3794 if (pinnedIpAddresses) {
3795 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3798 // Set night mode according to the night mode int.
3799 switch (nightModeInt) {
3800 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3801 // Set night mode according to the current default.
3802 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3805 case DomainsDatabaseHelper.ENABLED:
3806 // Enable night mode.
3807 nestedScrollWebView.setNightMode(true);
3810 case DomainsDatabaseHelper.DISABLED:
3811 // Disable night mode.
3812 nestedScrollWebView.setNightMode(false);
3816 // Enable JavaScript if night mode is enabled.
3817 if (nestedScrollWebView.getNightMode()) {
3818 // Enable JavaScript.
3819 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3821 // Set JavaScript according to the domain settings.
3822 nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled());
3825 // Close the current host domain settings cursor.
3826 currentDomainSettingsCursor.close();
3828 // Apply the domain settings.
3829 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3831 // Set third-party cookies status if API >= 21.
3832 if (Build.VERSION.SDK_INT >= 21) {
3833 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3836 // Apply the form data setting if the API < 26.
3837 if (Build.VERSION.SDK_INT < 26) {
3838 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3841 // Apply the font size.
3842 if (fontSize == 0) { // Apply the default font size.
3843 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3844 } else { // Apply the specified font size.
3845 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3848 // Set the user agent.
3849 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3850 // Get the array position of the default user agent name.
3851 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3853 // Set the user agent according to the system default.
3854 switch (defaultUserAgentArrayPosition) {
3855 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3856 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3857 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3860 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3861 // Set the user agent to `""`, which uses the default value.
3862 nestedScrollWebView.getSettings().setUserAgentString("");
3865 case SETTINGS_CUSTOM_USER_AGENT:
3866 // Set the default custom user agent.
3867 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3871 // Get the user agent string from the user agent data array
3872 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3874 } else { // Set the user agent according to the stored name.
3875 // Get the array position of the user agent name.
3876 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3878 switch (userAgentArrayPosition) {
3879 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3880 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3883 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3884 // Set the user agent to `""`, which uses the default value.
3885 nestedScrollWebView.getSettings().setUserAgentString("");
3889 // Get the user agent string from the user agent data array.
3890 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3894 // Set swipe to refresh.
3895 switch (swipeToRefreshInt) {
3896 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3897 // Store the swipe to refresh status in the nested scroll WebView.
3898 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3900 // 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.
3901 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3904 case DomainsDatabaseHelper.ENABLED:
3905 // Store the swipe to refresh status in the nested scroll WebView.
3906 nestedScrollWebView.setSwipeToRefresh(true);
3908 // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3909 swipeRefreshLayout.setEnabled(true);
3912 case DomainsDatabaseHelper.DISABLED:
3913 // Store the swipe to refresh status in the nested scroll WebView.
3914 nestedScrollWebView.setSwipeToRefresh(false);
3916 // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3917 swipeRefreshLayout.setEnabled(false);
3920 // Set the viewport.
3921 switch (wideViewportInt) {
3922 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3923 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
3926 case DomainsDatabaseHelper.ENABLED:
3927 nestedScrollWebView.getSettings().setUseWideViewPort(true);
3930 case DomainsDatabaseHelper.DISABLED:
3931 nestedScrollWebView.getSettings().setUseWideViewPort(false);
3935 // Set the loading of webpage images.
3936 switch (displayWebpageImagesInt) {
3937 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3938 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3941 case DomainsDatabaseHelper.ENABLED:
3942 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3945 case DomainsDatabaseHelper.DISABLED:
3946 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3950 // 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.
3952 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3954 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3956 } else { // The new URL does not have custom domain settings. Load the defaults.
3957 // Store the values from the shared preferences.
3958 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3959 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
3960 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3961 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3962 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3963 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true));
3964 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3965 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3966 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3967 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, sharedPreferences.getBoolean("ultralist", true));
3968 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3969 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3970 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3972 // Enable JavaScript if night mode is enabled.
3973 if (nestedScrollWebView.getNightMode()) {
3974 // Enable JavaScript.
3975 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3977 // Set JavaScript according to the domain settings.
3978 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3981 // Apply the default settings.
3982 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3983 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3985 // Apply the form data setting if the API < 26.
3986 if (Build.VERSION.SDK_INT < 26) {
3987 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3990 // Store the swipe to refresh status in the nested scroll WebView.
3991 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3993 // Apply swipe to refresh according to the default.
3994 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3996 // Reset the pinned variables.
3997 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
3999 // Set third-party cookies status if API >= 21.
4000 if (Build.VERSION.SDK_INT >= 21) {
4001 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
4004 // Get the array position of the user agent name.
4005 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4007 // Set the user agent.
4008 switch (userAgentArrayPosition) {
4009 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
4010 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4011 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4014 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4015 // Set the user agent to `""`, which uses the default value.
4016 nestedScrollWebView.getSettings().setUserAgentString("");
4019 case SETTINGS_CUSTOM_USER_AGENT:
4020 // Set the default custom user agent.
4021 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4025 // Get the user agent string from the user agent data array
4026 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4029 // Set the viewport.
4030 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4032 // Set the loading of webpage images.
4033 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4035 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
4036 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4039 // Close the domains database helper.
4040 domainsDatabaseHelper.close();
4042 // Update the privacy icons.
4043 updatePrivacyIcons(true);
4046 // Reload the website if returning from the Domains activity.
4047 if (reloadWebsite) {
4048 nestedScrollWebView.reload();
4051 // Return the user agent changed status.
4052 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
4055 private void applyProxyThroughOrbot(boolean reloadWebsite) {
4056 // Get a handle for the shared preferences.
4057 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4059 // Get the search and theme preferences.
4060 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
4061 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
4062 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
4063 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4064 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4066 // Get a handle for the app bar layout.
4067 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4069 // Set the homepage, search, and proxy options.
4070 if (proxyThroughOrbot) { // Set the Tor options.
4071 // Set the search URL.
4072 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
4073 searchURL = torSearchCustomUrlString;
4074 } else { // Use the string from the pre-built list.
4075 searchURL = torSearchString;
4078 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
4079 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4081 // Set the app bar background to indicate proxying through Orbot is enabled.
4083 appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4085 appBarLayout.setBackgroundResource(R.color.blue_50);
4088 // Check to see if Orbot is ready.
4089 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
4090 // Set `waitingForOrbot`.
4091 waitingForOrbot = true;
4093 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4094 currentWebView.getSettings().setUseWideViewPort(false);
4096 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
4097 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
4098 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
4099 // Reload the website.
4100 currentWebView.reload();
4102 } else { // Set the non-Tor options.
4103 // Set the search URL.
4104 if (searchString.equals("Custom URL")) { // Get the custom URL string.
4105 searchURL = searchCustomUrlString;
4106 } else { // Use the string from the pre-built list.
4107 searchURL = searchString;
4110 // Reset the proxy to default. The host is `""` and the port is `"0"`.
4111 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4113 // Set the default app bar layout background.
4115 appBarLayout.setBackgroundResource(R.color.gray_900);
4117 appBarLayout.setBackgroundResource(R.color.gray_100);
4120 // Reset `waitingForOrbot.
4121 waitingForOrbot = false;
4123 // Reload the WebViews if requested.
4124 if (reloadWebsite) {
4125 // Reload the WebViews.
4126 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4127 // Get the WebView tab fragment.
4128 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4130 // Get the fragment view.
4131 View fragmentView = webViewTabFragment.getView();
4133 // Only reload the WebViews if they exist.
4134 if (fragmentView != null) {
4135 // Get the nested scroll WebView from the tab fragment.
4136 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4138 // Reload the WebView.
4139 nestedScrollWebView.reload();
4146 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4147 // Only update the privacy icons if the options menu and the current WebView have already been populated.
4148 if ((optionsMenu != null) && (currentWebView != null)) {
4149 // Get a handle for the shared preferences.
4150 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4152 // Get the theme and screenshot preferences.
4153 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4155 // Get handles for the menu items.
4156 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
4157 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
4158 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
4159 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
4161 // Update the privacy icon.
4162 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
4163 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4164 } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled.
4165 privacyMenuItem.setIcon(R.drawable.warning);
4166 } else { // All the dangerous features are disabled.
4167 privacyMenuItem.setIcon(R.drawable.privacy_mode);
4170 // Update the first-party cookies icon.
4171 if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled.
4172 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4173 } else { // First-party cookies are disabled.
4175 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4177 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4181 // Update the DOM storage icon.
4182 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
4183 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4184 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
4186 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4188 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4190 } else { // JavaScript is disabled, so DOM storage is ghosted.
4192 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4194 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4198 // Update the refresh icon.
4200 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4202 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4205 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4206 if (runInvalidateOptionsMenu) {
4207 invalidateOptionsMenu();
4212 private void openUrlWithExternalApp(String url) {
4213 // Create a download intent. Not specifying the action type will display the maximum number of options.
4214 Intent downloadIntent = new Intent();
4216 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4217 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4219 // Flag the intent to open in a new task.
4220 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4222 // Show the chooser.
4223 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4226 private void highlightUrlText() {
4227 // Get a handle for the URL edit text.
4228 EditText urlEditText = findViewById(R.id.url_edittext);
4230 // Only highlight the URL text if the box is not currently selected.
4231 if (!urlEditText.hasFocus()) {
4232 // Get the URL string.
4233 String urlString = urlEditText.getText().toString();
4235 // Highlight the URL according to the protocol.
4236 if (urlString.startsWith("file://")) { // This is a file URL.
4237 // De-emphasize only the protocol.
4238 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4239 } else if (urlString.startsWith("content://")) {
4240 // De-emphasize only the protocol.
4241 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4242 } else { // This is a web URL.
4243 // Get the index of the `/` immediately after the domain name.
4244 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4246 // Create a base URL string.
4249 // Get the base URL.
4250 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4251 // Get the base URL.
4252 baseUrl = urlString.substring(0, endOfDomainName);
4253 } else { // There are no characters after the base URL.
4254 // Set the base URL to be the entire URL string.
4255 baseUrl = urlString;
4258 // Get the index of the last `.` in the domain.
4259 int lastDotIndex = baseUrl.lastIndexOf(".");
4261 // Get the index of the penultimate `.` in the domain.
4262 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4264 // Markup the beginning of the URL.
4265 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4266 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4268 // De-emphasize subdomains.
4269 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4270 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4272 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4273 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4274 // De-emphasize the protocol and the additional subdomains.
4275 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4276 } else { // There is only one subdomain in the domain name.
4277 // De-emphasize only the protocol.
4278 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4282 // De-emphasize the text after the domain name.
4283 if (endOfDomainName > 0) {
4284 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4290 private void loadBookmarksFolder() {
4291 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4292 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4294 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4295 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4297 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4298 // Inflate the individual item layout. `false` does not attach it to the root.
4299 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4303 public void bindView(View view, Context context, Cursor cursor) {
4304 // Get handles for the views.
4305 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4306 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4308 // Get the favorite icon byte array from the cursor.
4309 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4311 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4312 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4314 // Display the bitmap in `bookmarkFavoriteIcon`.
4315 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4317 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4318 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4319 bookmarkNameTextView.setText(bookmarkNameString);
4321 // Make the font bold for folders.
4322 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4323 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4324 } else { // Reset the font to default for normal bookmarks.
4325 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4330 // Get a handle for the bookmarks list view.
4331 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4333 // Populate the list view with the adapter.
4334 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4336 // Get a handle for the bookmarks title text view.
4337 TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4339 // Set the bookmarks drawer title.
4340 if (currentBookmarksFolder.isEmpty()) {
4341 bookmarksTitleTextView.setText(R.string.bookmarks);
4343 bookmarksTitleTextView.setText(currentBookmarksFolder);
4347 private void openWithApp(String url) {
4348 // Create the open with intent with `ACTION_VIEW`.
4349 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4351 // Set the URI but not the MIME type. This should open all available apps.
4352 openWithAppIntent.setData(Uri.parse(url));
4354 // Flag the intent to open in a new task.
4355 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4358 // Show the chooser.
4359 startActivity(openWithAppIntent);
4360 } catch (ActivityNotFoundException exception) {
4361 // Show a snackbar with the error.
4362 Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
4366 private void openWithBrowser(String url) {
4367 // Create the open with intent with `ACTION_VIEW`.
4368 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4370 // Set the URI and the MIME type. `"text/html"` should load browser options.
4371 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4373 // Flag the intent to open in a new task.
4374 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4377 // Show the chooser.
4378 startActivity(openWithBrowserIntent);
4379 } catch (ActivityNotFoundException exception) {
4380 // Show a snackbar with the error.
4381 Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
4385 private String sanitizeUrl(String url) {
4386 // Sanitize Google Analytics.
4387 if (sanitizeGoogleAnalytics) {
4389 if (url.contains("?utm_")) {
4390 url = url.substring(0, url.indexOf("?utm_"));
4394 if (url.contains("&utm_")) {
4395 url = url.substring(0, url.indexOf("&utm_"));
4399 // Sanitize Facebook Click IDs.
4400 if (sanitizeFacebookClickIds) {
4401 // Remove `?fbclid=`.
4402 if (url.contains("?fbclid=")) {
4403 url = url.substring(0, url.indexOf("?fbclid="));
4406 // Remove `&fbclid=`.
4407 if (url.contains("&fbclid=")) {
4408 url = url.substring(0, url.indexOf("&fbclid="));
4412 // Sanitize Twitter AMP redirects.
4413 if (sanitizeTwitterAmpRedirects) {
4415 if (url.contains("?amp=1")) {
4416 url = url.substring(0, url.indexOf("?amp=1"));
4420 // Return the sanitized URL.
4424 public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4425 // Store the blocklists.
4426 easyList = combinedBlocklists.get(0);
4427 easyPrivacy = combinedBlocklists.get(1);
4428 fanboysAnnoyanceList = combinedBlocklists.get(2);
4429 fanboysSocialList = combinedBlocklists.get(3);
4430 ultraList = combinedBlocklists.get(4);
4431 ultraPrivacy = combinedBlocklists.get(5);
4433 // Add the first tab.
4437 public void addTab(View view) {
4438 // Add a new tab with a blank URL.
4442 private void addNewTab(String url) {
4443 // Sanitize the URL.
4444 url = sanitizeUrl(url);
4446 // Get a handle for the tab layout and the view pager.
4447 TabLayout tabLayout = findViewById(R.id.tablayout);
4448 ViewPager webViewPager = findViewById(R.id.webviewpager);
4450 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4451 int newTabNumber = tabLayout.getTabCount();
4454 tabLayout.addTab(tabLayout.newTab());
4457 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4459 // Remove the lint warning below that the current tab might be null.
4460 assert newTab != null;
4462 // Set a custom view on the new tab.
4463 newTab.setCustomView(R.layout.tab_custom_view);
4465 // Add the new WebView page.
4466 webViewPagerAdapter.addPage(newTabNumber, webViewPager, url);
4469 public void closeTab(View view) {
4470 // Get a handle for the tab layout.
4471 TabLayout tabLayout = findViewById(R.id.tablayout);
4473 // Run the command according to the number of tabs.
4474 if (tabLayout.getTabCount() > 1) { // There is more than one tab open.
4475 // Close the current tab.
4477 } else { // There is only one tab open.
4482 private void closeCurrentTab() {
4483 // Get handles for the views.
4484 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4485 TabLayout tabLayout = findViewById(R.id.tablayout);
4486 ViewPager webViewPager = findViewById(R.id.webviewpager);
4488 // Get the current tab number.
4489 int currentTabNumber = tabLayout.getSelectedTabPosition();
4491 // Delete the current tab.
4492 tabLayout.removeTabAt(currentTabNumber);
4494 // 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.
4495 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4496 setCurrentWebView(currentTabNumber);
4499 // Expand the app bar if it is currently collapsed.
4500 appBarLayout.setExpanded(true);
4503 private void clearAndExit() {
4504 // Get a handle for the shared preferences.
4505 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4507 // Close the bookmarks cursor and database.
4508 bookmarksCursor.close();
4509 bookmarksDatabaseHelper.close();
4511 // Get the status of the clear everything preference.
4512 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
4514 // Get a handle for the runtime.
4515 Runtime runtime = Runtime.getRuntime();
4517 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4518 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4519 String privateDataDirectoryString = getApplicationInfo().dataDir;
4522 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
4523 // The command to remove cookies changed slightly in API 21.
4524 if (Build.VERSION.SDK_INT >= 21) {
4525 CookieManager.getInstance().removeAllCookies(null);
4527 CookieManager.getInstance().removeAllCookie();
4530 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4532 // Two commands must be used because `Runtime.exec()` does not like `*`.
4533 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4534 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4536 // Wait until the processes have finished.
4537 deleteCookiesProcess.waitFor();
4538 deleteCookiesJournalProcess.waitFor();
4539 } catch (Exception exception) {
4540 // Do nothing if an error is thrown.
4544 // Clear DOM storage.
4545 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
4546 // Ask `WebStorage` to clear the DOM storage.
4547 WebStorage webStorage = WebStorage.getInstance();
4548 webStorage.deleteAllData();
4550 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4552 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4553 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4555 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4556 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4557 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4558 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4559 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4561 // Wait until the processes have finished.
4562 deleteLocalStorageProcess.waitFor();
4563 deleteIndexProcess.waitFor();
4564 deleteQuotaManagerProcess.waitFor();
4565 deleteQuotaManagerJournalProcess.waitFor();
4566 deleteDatabaseProcess.waitFor();
4567 } catch (Exception exception) {
4568 // Do nothing if an error is thrown.
4572 // Clear form data if the API < 26.
4573 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
4574 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4575 webViewDatabase.clearFormData();
4577 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4579 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4580 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4581 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4583 // Wait until the processes have finished.
4584 deleteWebDataProcess.waitFor();
4585 deleteWebDataJournalProcess.waitFor();
4586 } catch (Exception exception) {
4587 // Do nothing if an error is thrown.
4592 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
4593 // Clear the cache from each WebView.
4594 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4595 // Get the WebView tab fragment.
4596 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4598 // Get the fragment view.
4599 View fragmentView = webViewTabFragment.getView();
4601 // Only clear the cache if the WebView exists.
4602 if (fragmentView != null) {
4603 // Get the nested scroll WebView from the tab fragment.
4604 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4606 // Clear the cache for this WebView.
4607 nestedScrollWebView.clearCache(true);
4611 // Manually delete the cache directories.
4613 // Delete the main cache directory.
4614 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4616 // Delete the secondary `Service Worker` cache directory.
4617 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4618 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
4620 // Wait until the processes have finished.
4621 deleteCacheProcess.waitFor();
4622 deleteServiceWorkerProcess.waitFor();
4623 } catch (Exception exception) {
4624 // Do nothing if an error is thrown.
4628 // Wipe out each WebView.
4629 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4630 // Get the WebView tab fragment.
4631 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4633 // Get the fragment view.
4634 View fragmentView = webViewTabFragment.getView();
4636 // Only wipe out the WebView if it exists.
4637 if (fragmentView != null) {
4638 // Get the nested scroll WebView from the tab fragment.
4639 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4641 // Clear SSL certificate preferences for this WebView.
4642 nestedScrollWebView.clearSslPreferences();
4644 // Clear the back/forward history for this WebView.
4645 nestedScrollWebView.clearHistory();
4647 // Destroy the internal state of `mainWebView`.
4648 nestedScrollWebView.destroy();
4652 // Clear the custom headers.
4653 customHeaders.clear();
4655 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4656 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4657 if (clearEverything) {
4659 // Delete the folder.
4660 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
4662 // Wait until the process has finished.
4663 deleteAppWebviewProcess.waitFor();
4664 } catch (Exception exception) {
4665 // Do nothing if an error is thrown.
4669 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4670 if (Build.VERSION.SDK_INT >= 21) {
4671 finishAndRemoveTask();
4676 // Remove the terminated program from RAM. The status code is `0`.
4680 private void setCurrentWebView(int pageNumber) {
4681 // Get a handle for the shared preferences.
4682 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4684 // Get the theme preference.
4685 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4687 // Get handles for the URL views.
4688 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4689 EditText urlEditText = findViewById(R.id.url_edittext);
4690 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4692 // Stop the swipe to refresh indicator if it is running
4693 swipeRefreshLayout.setRefreshing(false);
4695 // Get the WebView tab fragment.
4696 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4698 // Get the fragment view.
4699 View fragmentView = webViewTabFragment.getView();
4701 // Set the current WebView if the fragment view is not null.
4702 if (fragmentView != null) { // The fragment has been populated.
4703 // Store the current WebView.
4704 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4706 // Update the status of swipe to refresh.
4707 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
4708 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
4709 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4710 } else { // Swipe to refresh is disabled.
4711 // Disable the swipe refresh layout.
4712 swipeRefreshLayout.setEnabled(false);
4715 // Get a handle for the cookie manager.
4716 CookieManager cookieManager = CookieManager.getInstance();
4718 // Set the first-party cookie status.
4719 cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
4721 // Update the privacy icons. `true` redraws the icons in the app bar.
4722 updatePrivacyIcons(true);
4724 // Get a handle for the input method manager.
4725 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4727 // Remove the lint warning below that the input method manager might be null.
4728 assert inputMethodManager != null;
4730 // Get the current URL.
4731 String url = currentWebView.getUrl();
4733 // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4734 if (!loadingNewIntent) { // A new intent is not being loaded.
4735 if ((url == null) || url.equals("about:blank")) { // The WebView is blank.
4736 // Display the hint in the URL edit text.
4737 urlEditText.setText("");
4739 // Request focus for the URL text box.
4740 urlEditText.requestFocus();
4742 // Display the keyboard.
4743 inputMethodManager.showSoftInput(urlEditText, 0);
4744 } else { // The WebView has a loaded URL.
4745 // Clear the focus from the URL text box.
4746 urlEditText.clearFocus();
4748 // Hide the soft keyboard.
4749 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4751 // Display the current URL in the URL text box.
4752 urlEditText.setText(url);
4754 // Highlight the URL text.
4757 } else { // A new intent is being loaded.
4758 // Reset the loading new intent tracker.
4759 loadingNewIntent = false;
4762 // Set the background to indicate the domain settings status.
4763 if (currentWebView.getDomainSettingsApplied()) {
4764 // 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.
4766 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4768 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4771 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4773 } else { // The fragment has not been populated. Try again in 100 milliseconds.
4774 // Create a handler to set the current WebView.
4775 Handler setCurrentWebViewHandler = new Handler();
4777 // Create a runnable to set the current WebView.
4778 Runnable setCurrentWebWebRunnable = () -> {
4779 // Set the current WebView.
4780 setCurrentWebView(pageNumber);
4783 // Try setting the current WebView again after 100 milliseconds.
4784 setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
4789 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) {
4790 // Get handles for the activity views.
4791 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4792 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4793 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4794 ActionBar actionBar = getSupportActionBar();
4795 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
4796 EditText urlEditText = findViewById(R.id.url_edittext);
4797 TabLayout tabLayout = findViewById(R.id.tablayout);
4798 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4800 // Remove the incorrect lint warning below that the action bar might be null.
4801 assert actionBar != null;
4803 // Get a handle for the activity
4804 Activity activity = this;
4806 // Get a handle for the input method manager.
4807 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4809 // Instantiate the blocklist helper.
4810 BlocklistHelper blocklistHelper = new BlocklistHelper();
4812 // Remove the lint warning below that the input method manager might be null.
4813 assert inputMethodManager != null;
4815 // Get a handle for the shared preferences.
4816 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4818 // Initialize the favorite icon.
4819 nestedScrollWebView.initializeFavoriteIcon();
4821 // Set the app bar scrolling.
4822 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4824 // Allow pinch to zoom.
4825 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4827 // Hide zoom controls.
4828 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4830 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4831 if (Build.VERSION.SDK_INT >= 21) {
4832 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4835 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4836 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4838 // Explicitly disable geolocation.
4839 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4841 // Create a double-tap gesture detector to toggle full-screen mode.
4842 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4843 // Override `onDoubleTap()`. All other events are handled using the default settings.
4845 public boolean onDoubleTap(MotionEvent event) {
4846 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4847 // Toggle the full screen browsing mode tracker.
4848 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4850 // Toggle the full screen browsing mode.
4851 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4852 // Store the swipe refresh layout top padding.
4853 swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop();
4855 // Hide the app bar if specified.
4857 // Close the find on page bar if it is visible.
4858 closeFindOnPage(null);
4860 // Hide the tab linear layout.
4861 tabsLinearLayout.setVisibility(View.GONE);
4863 // Hide the action bar.
4866 // Check to see if app bar scrolling is disabled.
4867 if (!scrollAppBar) {
4868 // Remove the padding from the top of the swipe refresh layout.
4869 swipeRefreshLayout.setPadding(0, 0, 0, 0);
4873 // Hide the banner ad in the free flavor.
4874 if (BuildConfig.FLAVOR.contentEquals("free")) {
4875 AdHelper.hideAd(findViewById(R.id.adview));
4878 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4879 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4881 /* Hide the system bars.
4882 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4883 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4884 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4885 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4887 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4888 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4889 } else { // Switch to normal viewing mode.
4890 // Show the tab linear layout.
4891 tabsLinearLayout.setVisibility(View.VISIBLE);
4893 // Show the action bar.
4896 // Check to see if app bar scrolling is disabled.
4897 if (!scrollAppBar) {
4898 // Add the padding from the top of the swipe refresh layout.
4899 swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0);
4902 // Show the banner ad in the free flavor.
4903 if (BuildConfig.FLAVOR.contentEquals("free")) {
4905 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4908 // Remove the `SYSTEM_UI` flags from the root frame layout.
4909 rootFrameLayout.setSystemUiVisibility(0);
4911 // Add the translucent status flag.
4912 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4915 // Consume the double-tap.
4917 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4923 // Pass all touch events on the WebView through the double-tap gesture detector.
4924 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4925 // Call `performClick()` on the view, which is required for accessibility.
4926 view.performClick();
4928 // Send the event to the gesture detector.
4929 return doubleTapGestureDetector.onTouchEvent(event);
4932 // Register the WebView for a context menu. This is used to see link targets and download images.
4933 registerForContextMenu(nestedScrollWebView);
4935 // Allow the downloading of files.
4936 nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4937 // Check if the download should be processed by an external app.
4938 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
4939 // Create a download intent. Not specifying the action type will display the maximum number of options.
4940 Intent downloadIntent = new Intent();
4942 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4943 downloadIntent.setDataAndType(Uri.parse(downloadUrl), "text/html");
4945 // Flag the intent to open in a new task.
4946 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4948 // Show the chooser.
4949 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4950 } else { // Download with Android's download manager.
4951 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4952 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4953 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4955 // Store the variables for future use by `onRequestPermissionsResult()`.
4956 this.downloadUrl = downloadUrl;
4957 downloadContentDisposition = contentDisposition;
4958 downloadContentLength = contentLength;
4960 // Show a dialog if the user has previously denied the permission.
4961 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4962 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4963 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4965 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4966 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4967 } else { // Show the permission request directly.
4968 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4969 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4971 } else { // The storage permission has already been granted.
4972 // Get a handle for the download file alert dialog.
4973 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, contentDisposition, contentLength);
4975 // Show the download file alert dialog.
4976 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4981 // Update the find on page count.
4982 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4983 // Get a handle for `findOnPageCountTextView`.
4984 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4987 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4988 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4989 // Set `findOnPageCountTextView` to `0/0`.
4990 findOnPageCountTextView.setText(R.string.zero_of_zero);
4991 } else if (isDoneCounting) { // There are matches.
4992 // `activeMatchOrdinal` is zero-based.
4993 int activeMatch = activeMatchOrdinal + 1;
4995 // Build the match string.
4996 String matchString = activeMatch + "/" + numberOfMatches;
4998 // Set `findOnPageCountTextView`.
4999 findOnPageCountTextView.setText(matchString);
5004 // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. Also reinforce full screen browsing mode.
5005 // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`.
5006 nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
5007 if (nestedScrollWebView.getSwipeToRefresh()) {
5008 // Only enable swipe to refresh if the WebView is scrolled to the top.
5009 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5012 // Reinforce the system UI visibility flags if in full screen browsing mode.
5013 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5014 if (inFullScreenBrowsingMode) {
5015 /* Hide the system bars.
5016 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5017 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5018 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5019 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5021 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5022 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5026 // Set the web chrome client.
5027 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5028 // Update the progress bar when a page is loading.
5030 public void onProgressChanged(WebView view, int progress) {
5031 // Inject the night mode CSS if night mode is enabled.
5032 if (nestedScrollWebView.getNightMode()) { // Night mode is enabled.
5033 // `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
5034 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
5035 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
5036 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
5037 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
5038 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
5039 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
5040 // Initialize a handler to display `mainWebView`.
5041 Handler displayWebViewHandler = new Handler();
5043 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
5044 Runnable displayWebViewRunnable = () -> {
5045 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
5046 if (progressBar.getVisibility() == View.GONE) {
5047 nestedScrollWebView.setVisibility(View.VISIBLE);
5051 // Display the WebView after 500 milliseconds.
5052 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
5054 } else { // Night mode is disabled.
5055 // Display the nested scroll WebView if night mode is disabled.
5056 // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
5057 // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
5058 nestedScrollWebView.setVisibility(View.VISIBLE);
5061 // Update the progress bar.
5062 progressBar.setProgress(progress);
5064 // Set the visibility of the progress bar.
5065 if (progress < 100) {
5066 // Show the progress bar.
5067 progressBar.setVisibility(View.VISIBLE);
5069 // Hide the progress bar.
5070 progressBar.setVisibility(View.GONE);
5072 //Stop the swipe to refresh indicator if it is running
5073 swipeRefreshLayout.setRefreshing(false);
5077 // Set the favorite icon when it changes.
5079 public void onReceivedIcon(WebView view, Bitmap icon) {
5080 // Only update the favorite icon if the website has finished loading.
5081 if (progressBar.getVisibility() == View.GONE) {
5082 // Store the new favorite icon.
5083 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5085 // Get the current page position.
5086 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5088 // Get the current tab.
5089 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5091 // Check to see if the tab has been populated.
5093 // Get the custom view from the tab.
5094 View tabView = tab.getCustomView();
5096 // Check to see if the custom tab view has been populated.
5097 if (tabView != null) {
5098 // Get the favorite icon image view from the tab.
5099 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5101 // Display the favorite icon in the tab.
5102 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5108 // Save a copy of the title when it changes.
5110 public void onReceivedTitle(WebView view, String title) {
5111 // Get the current page position.
5112 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5114 // Get the current tab.
5115 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5117 // Only populate the title text view if the tab has been fully created.
5119 // Get the custom view from the tab.
5120 View tabView = tab.getCustomView();
5122 // Remove the incorrect warning below that the current tab view might be null.
5123 assert tabView != null;
5125 // Get the title text view from the tab.
5126 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5128 // Set the title according to the URL.
5129 if (title.equals("about:blank")) {
5130 // Set the title to indicate a new tab.
5131 tabTitleTextView.setText(R.string.new_tab);
5133 // Set the title as the tab text.
5134 tabTitleTextView.setText(title);
5139 // Enter full screen video.
5141 public void onShowCustomView(View video, CustomViewCallback callback) {
5142 // Get a handle for the full screen video frame layout.
5143 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5145 // Set the full screen video flag.
5146 displayingFullScreenVideo = true;
5148 // Pause the ad if this is the free flavor.
5149 if (BuildConfig.FLAVOR.contentEquals("free")) {
5150 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
5151 AdHelper.pauseAd(findViewById(R.id.adview));
5154 // Hide the keyboard.
5155 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5157 // Hide the main content relative layout.
5158 mainContentRelativeLayout.setVisibility(View.GONE);
5160 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
5161 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
5163 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
5164 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5166 /* Hide the system bars.
5167 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5168 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5169 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5170 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5172 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5173 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5175 // Disable the sliding drawers.
5176 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5178 // Add the video view to the full screen video frame layout.
5179 fullScreenVideoFrameLayout.addView(video);
5181 // Show the full screen video frame layout.
5182 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5185 // Exit full screen video.
5187 public void onHideCustomView() {
5188 // Get a handle for the full screen video frame layout.
5189 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5191 // Unset the full screen video flag.
5192 displayingFullScreenVideo = false;
5194 // Remove all the views from the full screen video frame layout.
5195 fullScreenVideoFrameLayout.removeAllViews();
5197 // Hide the full screen video frame layout.
5198 fullScreenVideoFrameLayout.setVisibility(View.GONE);
5200 // Enable the sliding drawers.
5201 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
5203 // Show the main content relative layout.
5204 mainContentRelativeLayout.setVisibility(View.VISIBLE);
5206 // Apply the appropriate full screen mode flags.
5207 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
5208 // Hide the app bar if specified.
5210 // Hide the tab linear layout.
5211 tabsLinearLayout.setVisibility(View.GONE);
5213 // Hide the action bar.
5217 // Hide the banner ad in the free flavor.
5218 if (BuildConfig.FLAVOR.contentEquals("free")) {
5219 AdHelper.hideAd(findViewById(R.id.adview));
5222 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
5223 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5225 /* Hide the system bars.
5226 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5227 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5228 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5229 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5231 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5232 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5233 } else { // Switch to normal viewing mode.
5234 // Remove the `SYSTEM_UI` flags from the root frame layout.
5235 rootFrameLayout.setSystemUiVisibility(0);
5237 // Add the translucent status flag.
5238 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5241 // Reload the ad for the free flavor if not in full screen mode.
5242 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
5244 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
5250 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5251 // Show the file chooser if the device is running API >= 21.
5252 if (Build.VERSION.SDK_INT >= 21) {
5253 // Store the file path callback.
5254 fileChooserCallback = filePathCallback;
5256 // Create an intent to open a chooser based ont the file chooser parameters.
5257 Intent fileChooserIntent = fileChooserParams.createIntent();
5259 // Open the file chooser.
5260 startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
5266 nestedScrollWebView.setWebViewClient(new WebViewClient() {
5267 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
5268 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5270 public boolean shouldOverrideUrlLoading(WebView view, String url) {
5271 // Sanitize the url.
5272 url = sanitizeUrl(url);
5274 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
5275 // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed.
5276 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5278 // Check if the user agent has changed.
5279 if (userAgentChanged) {
5280 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
5281 nestedScrollWebView.loadUrl(url, customHeaders);
5283 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5286 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
5289 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
5290 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5291 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5293 // Parse the url and set it as the data for the intent.
5294 emailIntent.setData(Uri.parse(url));
5296 // Open the email program in a new task instead of as part of Privacy Browser.
5297 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5300 startActivity(emailIntent);
5302 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5304 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
5305 // Open the dialer and load the phone number, but wait for the user to place the call.
5306 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5308 // Add the phone number to the intent.
5309 dialIntent.setData(Uri.parse(url));
5311 // Open the dialer in a new task instead of as part of Privacy Browser.
5312 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5315 startActivity(dialIntent);
5317 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5319 } else { // Load a system chooser to select an app that can handle the URL.
5320 // Open an app that can handle the URL.
5321 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5323 // Add the URL to the intent.
5324 genericIntent.setData(Uri.parse(url));
5326 // List all apps that can handle the URL instead of just opening the first one.
5327 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5329 // Open the app in a new task instead of as part of Privacy Browser.
5330 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5332 // Start the app or display a snackbar if no app is available to handle the URL.
5334 startActivity(genericIntent);
5335 } catch (ActivityNotFoundException exception) {
5336 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
5339 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5344 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5346 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
5347 // 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.
5348 while (ultraPrivacy == null) {
5349 // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5350 synchronized (this) {
5352 // Check to see if the blocklists have been populated after 100 ms.
5354 } catch (InterruptedException exception) {
5360 // Sanitize the URL.
5361 url = sanitizeUrl(url);
5363 // Get a handle for the navigation view.
5364 NavigationView navigationView = findViewById(R.id.navigationview);
5366 // Get a handle for the navigation menu.
5367 Menu navigationMenu = navigationView.getMenu();
5369 // Get a handle for the navigation requests menu item. The menu is 0 based.
5370 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
5372 // Create an empty web resource response to be used if the resource request is blocked.
5373 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5375 // Reset the whitelist results tracker.
5376 String[] whitelistResultStringArray = null;
5378 // Initialize the third party request tracker.
5379 boolean isThirdPartyRequest = false;
5381 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5382 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5384 // Store a copy of the current domain for use in later requests.
5385 String currentDomain = currentBaseDomain;
5387 // Nobody is happy when comparing null strings.
5388 if ((currentBaseDomain != null) && (url != null)) {
5389 // Convert the request URL to a URI.
5390 Uri requestUri = Uri.parse(url);
5392 // Get the request host name.
5393 String requestBaseDomain = requestUri.getHost();
5395 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5396 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5397 // Determine the current base domain.
5398 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5399 // Remove the first subdomain.
5400 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5403 // Determine the request base domain.
5404 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5405 // Remove the first subdomain.
5406 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5409 // Update the third party request tracker.
5410 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5414 // Get the current WebView page position.
5415 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5417 // Determine if the WebView is currently displayed.
5418 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5420 // Block third-party requests if enabled.
5421 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
5422 // Add the result to the resource requests.
5423 nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5425 // Increment the blocked requests counters.
5426 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5427 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5429 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5430 if (webViewDisplayed) {
5431 // Updating the UI must be run from the UI thread.
5432 activity.runOnUiThread(() -> {
5433 // Update the menu item titles.
5434 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5436 // Update the options menu if it has been populated.
5437 if (optionsMenu != null) {
5438 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5439 optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5440 getString(R.string.block_all_third_party_requests));
5445 // Return an empty web resource response.
5446 return emptyWebResourceResponse;
5449 // Check UltraList if it is enabled.
5450 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)) {
5451 // Check the URL against UltraList.
5452 String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5454 // Process the UltraList results.
5455 if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraLists's blacklist.
5456 // Add the result to the resource requests.
5457 nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5459 // Increment the blocked requests counters.
5460 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5461 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5463 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5464 if (webViewDisplayed) {
5465 // Updating the UI must be run from the UI thread.
5466 activity.runOnUiThread(() -> {
5467 // Update the menu item titles.
5468 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5470 // Update the options menu if it has been populated.
5471 if (optionsMenu != null) {
5472 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5473 optionsMenu.findItem(R.id.ultralist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5478 // The resource request was blocked. Return an empty web resource response.
5479 return emptyWebResourceResponse;
5480 } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraList's whitelist.
5481 // Add a whitelist entry to the resource requests array.
5482 nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5484 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5489 // Check UltraPrivacy if it is enabled.
5490 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)) {
5491 // Check the URL against UltraPrivacy.
5492 String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5494 // Process the UltraPrivacy results.
5495 if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
5496 // Add the result to the resource requests.
5497 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5498 ultraPrivacyResults[5]});
5500 // Increment the blocked requests counters.
5501 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5502 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5504 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5505 if (webViewDisplayed) {
5506 // Updating the UI must be run from the UI thread.
5507 activity.runOnUiThread(() -> {
5508 // Update the menu item titles.
5509 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5511 // Update the options menu if it has been populated.
5512 if (optionsMenu != null) {
5513 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5514 optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5519 // The resource request was blocked. Return an empty web resource response.
5520 return emptyWebResourceResponse;
5521 } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
5522 // Add a whitelist entry to the resource requests array.
5523 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5524 ultraPrivacyResults[5]});
5526 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5531 // Check EasyList if it is enabled.
5532 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)) {
5533 // Check the URL against EasyList.
5534 String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5536 // Process the EasyList results.
5537 if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
5538 // Add the result to the resource requests.
5539 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5541 // Increment the blocked requests counters.
5542 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5543 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5545 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5546 if (webViewDisplayed) {
5547 // Updating the UI must be run from the UI thread.
5548 activity.runOnUiThread(() -> {
5549 // Update the menu item titles.
5550 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5552 // Update the options menu if it has been populated.
5553 if (optionsMenu != null) {
5554 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5555 optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5560 // The resource request was blocked. Return an empty web resource response.
5561 return emptyWebResourceResponse;
5562 } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
5563 // Update the whitelist result string array tracker.
5564 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5568 // Check EasyPrivacy if it is enabled.
5569 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)) {
5570 // Check the URL against EasyPrivacy.
5571 String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5573 // Process the EasyPrivacy results.
5574 if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
5575 // Add the result to the resource requests.
5576 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5577 easyPrivacyResults[5]});
5579 // Increment the blocked requests counters.
5580 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5581 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5583 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5584 if (webViewDisplayed) {
5585 // Updating the UI must be run from the UI thread.
5586 activity.runOnUiThread(() -> {
5587 // Update the menu item titles.
5588 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5590 // Update the options menu if it has been populated.
5591 if (optionsMenu != null) {
5592 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5593 optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
5598 // The resource request was blocked. Return an empty web resource response.
5599 return emptyWebResourceResponse;
5600 } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
5601 // Update the whitelist result string array tracker.
5602 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5606 // Check Fanboy’s Annoyance List if it is enabled.
5607 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
5608 // Check the URL against Fanboy's Annoyance List.
5609 String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5611 // Process the Fanboy's Annoyance List results.
5612 if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
5613 // Add the result to the resource requests.
5614 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5615 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5617 // Increment the blocked requests counters.
5618 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5619 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5621 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5622 if (webViewDisplayed) {
5623 // Updating the UI must be run from the UI thread.
5624 activity.runOnUiThread(() -> {
5625 // Update the menu item titles.
5626 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5628 // Update the options menu if it has been populated.
5629 if (optionsMenu != null) {
5630 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5631 optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5632 getString(R.string.fanboys_annoyance_list));
5637 // The resource request was blocked. Return an empty web resource response.
5638 return emptyWebResourceResponse;
5639 } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
5640 // Update the whitelist result string array tracker.
5641 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5642 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5644 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5645 // Check the URL against Fanboy's Annoyance List.
5646 String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5648 // Process the Fanboy's Social Blocking List results.
5649 if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5650 // Add the result to the resource requests.
5651 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5652 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5654 // Increment the blocked requests counters.
5655 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5656 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5658 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5659 if (webViewDisplayed) {
5660 // Updating the UI must be run from the UI thread.
5661 activity.runOnUiThread(() -> {
5662 // Update the menu item titles.
5663 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5665 // Update the options menu if it has been populated.
5666 if (optionsMenu != null) {
5667 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5668 optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5669 getString(R.string.fanboys_social_blocking_list));
5674 // The resource request was blocked. Return an empty web resource response.
5675 return emptyWebResourceResponse;
5676 } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5677 // Update the whitelist result string array tracker.
5678 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5679 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5683 // Add the request to the log because it hasn't been processed by any of the previous checks.
5684 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5685 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5686 } else { // The request didn't match any blocklist entry. Log it as a default request.
5687 nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
5690 // The resource request has not been blocked. `return null` loads the requested resource.
5694 // Handle HTTP authentication requests.
5696 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5697 // Store the handler.
5698 nestedScrollWebView.setHttpAuthHandler(handler);
5700 // Instantiate an HTTP authentication dialog.
5701 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5703 // Show the HTTP authentication dialog.
5704 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5708 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5709 // Get the preferences.
5710 boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
5711 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5713 // Get a handler for the app bar layout.
5714 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
5716 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.
5718 // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5719 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5721 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5722 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
5724 // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated.
5725 int appBarHeight = appBarLayout.getHeight();
5727 // The swipe refresh layout must be manually moved below the app bar layout.
5728 swipeRefreshLayout.setPadding(0, appBarHeight, 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 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5734 // Reset the list of resource requests.
5735 nestedScrollWebView.clearResourceRequests();
5737 // Reset the requests counters.
5738 nestedScrollWebView.resetRequestsCounters();
5740 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5741 if (nestedScrollWebView.getNightMode()) {
5742 nestedScrollWebView.setVisibility(View.INVISIBLE);
5744 nestedScrollWebView.setVisibility(View.VISIBLE);
5747 // Hide the keyboard.
5748 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5750 // Check to see if Privacy Browser is waiting on Orbot.
5751 if (!waitingForOrbot) { // Process the URL.
5752 // Get the current page position.
5753 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5755 // Update the URL text bar if the page is currently selected.
5756 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5757 // Clear the focus from the URL edit text.
5758 urlEditText.clearFocus();
5760 // Display the formatted URL text.
5761 urlEditText.setText(url);
5763 // Apply text highlighting to `urlTextBox`.
5767 // Reset the list of host IP addresses.
5768 nestedScrollWebView.clearCurrentIpAddresses();
5770 // Get a URI for the current URL.
5771 Uri currentUri = Uri.parse(url);
5773 // Get the IP addresses for the host.
5774 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5776 // Apply any custom domain settings if the URL was loaded by navigating history.
5777 if (nestedScrollWebView.getNavigatingHistory()) {
5778 // Reset navigating history.
5779 nestedScrollWebView.setNavigatingHistory(false);
5781 // Apply the domain settings.
5782 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5784 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5785 if (userAgentChanged) {
5790 // Replace Refresh with Stop if the options menu has been created. (The first WebView typically begins loading before the menu items are instantiated.)
5791 if (optionsMenu != null) {
5792 // Get a handle for the refresh menu item.
5793 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5796 refreshMenuItem.setTitle(R.string.stop);
5798 // Get the app bar and theme preferences.
5799 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5801 // If the icon is displayed in the AppBar, set it according to the theme.
5802 if (displayAdditionalAppBarIcons) {
5804 refreshMenuItem.setIcon(R.drawable.close_dark);
5806 refreshMenuItem.setIcon(R.drawable.close_light);
5814 public void onPageFinished(WebView view, String url) {
5815 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5816 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
5817 CookieManager.getInstance().flush();
5820 // Update the Refresh menu item if the options menu has been created.
5821 if (optionsMenu != null) {
5822 // Get a handle for the refresh menu item.
5823 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5825 // Reset the Refresh title.
5826 refreshMenuItem.setTitle(R.string.refresh);
5828 // Get the app bar and theme preferences.
5829 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5830 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5832 // If the icon is displayed in the AppBar, reset it according to the theme.
5833 if (displayAdditionalAppBarIcons) {
5835 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5837 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5842 // Clear the cache and history if Incognito Mode is enabled.
5843 if (incognitoModeEnabled) {
5844 // Clear the cache. `true` includes disk files.
5845 nestedScrollWebView.clearCache(true);
5847 // Clear the back/forward history.
5848 nestedScrollWebView.clearHistory();
5850 // Manually delete cache folders.
5852 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5853 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5854 String privateDataDirectoryString = getApplicationInfo().dataDir;
5856 // Delete the main cache directory.
5857 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5859 // Delete the secondary `Service Worker` cache directory.
5860 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5861 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5862 } catch (IOException e) {
5863 // Do nothing if an error is thrown.
5867 // Update the URL text box and apply domain settings if not waiting on Orbot.
5868 if (!waitingForOrbot) {
5869 // Get the current page position.
5870 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5872 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5873 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5874 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5875 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5878 // 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.
5879 String currentUrl = nestedScrollWebView.getUrl();
5881 // Get the current tab.
5882 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
5884 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5885 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5886 // Probably some sort of race condition when Privacy Browser is being resumed.
5887 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5888 // Check to see if the URL is `about:blank`.
5889 if (currentUrl.equals("about:blank")) { // The WebView is blank.
5890 // Display the hint in the URL edit text.
5891 urlEditText.setText("");
5893 // Request focus for the URL text box.
5894 urlEditText.requestFocus();
5896 // Display the keyboard.
5897 inputMethodManager.showSoftInput(urlEditText, 0);
5899 // Apply the domain settings. This clears any settings from the previous domain.
5900 applyDomainSettings(nestedScrollWebView, "", true, false);
5902 // Only populate the title text view if the tab has been fully created.
5904 // Get the custom view from the tab.
5905 View tabView = tab.getCustomView();
5907 // Remove the incorrect warning below that the current tab view might be null.
5908 assert tabView != null;
5910 // Get the title text view from the tab.
5911 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5913 // Set the title as the tab text.
5914 tabTitleTextView.setText(R.string.new_tab);
5916 } else { // The WebView has loaded a webpage.
5917 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5918 urlEditText.setText(currentUrl);
5920 // Apply text highlighting to the URL.
5923 // Only populate the title text view if the tab has been fully created.
5925 // Get the custom view from the tab.
5926 View tabView = tab.getCustomView();
5928 // Remove the incorrect warning below that the current tab view might be null.
5929 assert tabView != null;
5931 // Get the title text view from the tab.
5932 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5934 // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5935 tabTitleTextView.setText(nestedScrollWebView.getTitle());
5942 // Handle SSL Certificate errors.
5944 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5945 // Get the current website SSL certificate.
5946 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5948 // Extract the individual pieces of information from the current website SSL certificate.
5949 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5950 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5951 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5952 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5953 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5954 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5955 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5956 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5958 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5959 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5960 // Get the pinned SSL certificate.
5961 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5963 // Extract the arrays from the array list.
5964 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5965 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5967 // Check if the current SSL certificate matches the pinned certificate.
5968 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5969 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5970 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5971 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5973 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5976 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5977 // Store the SSL error handler.
5978 nestedScrollWebView.setSslErrorHandler(handler);
5980 // Instantiate an SSL certificate error alert dialog.
5981 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
5983 // Show the SSL certificate error dialog.
5984 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5989 // Check to see if this is the first page.
5990 if (pageNumber == 0) {
5991 // Set this nested scroll WebView as the current WebView.
5992 currentWebView = nestedScrollWebView;
5994 // Apply the app settings from the shared preferences.
5997 // Load the website if not waiting for Orbot to connect.
5998 if (!waitingForOrbot) {
5999 // Get the intent that started the app.
6000 Intent launchingIntent = getIntent();
6002 // Get the information from the intent.
6003 String launchingIntentAction = launchingIntent.getAction();
6004 Uri launchingIntentUriData = launchingIntent.getData();
6006 // If the intent action is a web search, perform the search.
6007 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
6008 // Create an encoded URL string.
6009 String encodedUrlString;
6011 // Sanitize the search input and convert it to a search.
6013 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6014 } catch (UnsupportedEncodingException exception) {
6015 encodedUrlString = "";
6018 // Load the completed search URL.
6019 loadUrl(searchURL + encodedUrlString);
6020 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
6021 // Load the URL from the intent.
6022 loadUrl(launchingIntentUriData.toString());
6023 } else { // The is no URL in the intent.
6024 // Select the homepage based on the proxy through Orbot status.
6025 if (proxyThroughOrbot) {
6026 // Load the Tor homepage.
6027 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
6029 // Load the normal homepage.
6030 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
6034 } else { // This is not the first tab.
6035 // Apply the domain settings.
6036 applyDomainSettings(nestedScrollWebView, url, false, false);
6039 nestedScrollWebView.loadUrl(url, customHeaders);
6041 // Set the focus and display the keyboard if the URL is blank.
6042 if (url.equals("")) {
6043 // Request focus for the URL text box.
6044 urlEditText.requestFocus();
6046 // Create a display keyboard handler.
6047 Handler displayKeyboardHandler = new Handler();
6049 // Create a display keyboard runnable.
6050 Runnable displayKeyboardRunnable = () -> {
6051 // Display the keyboard.
6052 inputMethodManager.showSoftInput(urlEditText, 0);
6055 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6056 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);