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;
436 addNewTab(url, 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.
2029 addNewTab(linkUrl, false);
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_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2155 // Load the image in a new tab.
2156 addNewTab(imageUrl, false);
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.
2263 addNewTab(linkUrl, false);
2265 // Consume the event.
2269 // Add an Open Image in New Tab entry.
2270 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2271 // Load the image in a new tab.
2272 addNewTab(imageUrl, false);
2274 // Consume the event.
2278 // Add a View Image entry.
2279 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2280 // View the image in the current tab.
2283 // Consume the event.
2287 // Add a Download Image entry.
2288 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2289 // Check if the download should be processed by an external app.
2290 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2291 openUrlWithExternalApp(imageUrl);
2292 } else { // Download with Android's download manager.
2293 // Check to see if the storage permission has already been granted.
2294 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2295 // Store the image URL for use by `onRequestPermissionResult()`.
2296 downloadImageUrl = imageUrl;
2298 // Show a dialog if the user has previously denied the permission.
2299 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2300 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2301 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2303 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2304 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2305 } else { // Show the permission request directly.
2306 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2307 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2309 } else { // The storage permission has already been granted.
2310 // Get a handle for the download image alert dialog.
2311 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2313 // Show the download image alert dialog.
2314 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2318 // Consume the event.
2322 // Add a Copy URL entry.
2323 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2324 // Save the link URL in a clip data.
2325 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2327 // Set the clip data as the clipboard's primary clip.
2328 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2330 // Consume the event.
2334 // Add an Open with App entry.
2335 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2336 // Open the link URL with an external app.
2337 openWithApp(linkUrl);
2339 // Consume the event.
2343 // Add an Open with Browser entry.
2344 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2345 // Open the link URL with an external browser.
2346 openWithBrowser(linkUrl);
2348 // Consume the event.
2352 // Add a cancel entry, which by default closes the context menu.
2353 menu.add(R.string.cancel);
2359 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2360 // Get a handle for the bookmarks list view.
2361 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2363 // Get the views from the dialog fragment.
2364 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2365 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2367 // Extract the strings from the edit texts.
2368 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2369 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2371 // Create a favorite icon byte array output stream.
2372 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2374 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2375 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2377 // Convert the favorite icon byte array stream to a byte array.
2378 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2380 // Display the new bookmark below the current items in the (0 indexed) list.
2381 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2383 // Create the bookmark.
2384 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2386 // Update the bookmarks cursor with the current contents of this folder.
2387 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2389 // Update the list view.
2390 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2392 // Scroll to the new bookmark.
2393 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2397 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2398 // Get a handle for the bookmarks list view.
2399 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2401 // Get handles for the views in the dialog fragment.
2402 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2403 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2404 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2406 // Get new folder name string.
2407 String folderNameString = createFolderNameEditText.getText().toString();
2409 // Create a folder icon bitmap.
2410 Bitmap folderIconBitmap;
2412 // Set the folder icon bitmap according to the dialog.
2413 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2414 // Get the default folder icon drawable.
2415 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2417 // Convert the folder icon drawable to a bitmap drawable.
2418 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2420 // Convert the folder icon bitmap drawable to a bitmap.
2421 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2422 } else { // Use the WebView favorite icon.
2423 // Copy the favorite icon bitmap to the folder icon bitmap.
2424 folderIconBitmap = favoriteIconBitmap;
2427 // Create a folder icon byte array output stream.
2428 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2430 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2431 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2433 // Convert the folder icon byte array stream to a byte array.
2434 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2436 // Move all the bookmarks down one in the display order.
2437 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2438 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2439 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2442 // Create the folder, which will be placed at the top of the `ListView`.
2443 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2445 // Update the bookmarks cursor with the current contents of this folder.
2446 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2448 // Update the `ListView`.
2449 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2451 // Scroll to the new folder.
2452 bookmarksListView.setSelection(0);
2456 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2457 // Get handles for the views from `dialogFragment`.
2458 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2459 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2460 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2462 // Store the bookmark strings.
2463 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2464 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2466 // Update the bookmark.
2467 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2468 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2469 } else { // Update the bookmark using the `WebView` favorite icon.
2470 // Create a favorite icon byte array output stream.
2471 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2473 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2474 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2476 // Convert the favorite icon byte array stream to a byte array.
2477 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2479 // Update the bookmark and the favorite icon.
2480 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2483 // Update the bookmarks cursor with the current contents of this folder.
2484 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2486 // Update the list view.
2487 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2491 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2492 // Get handles for the views from `dialogFragment`.
2493 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2494 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2495 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2496 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2498 // Get the new folder name.
2499 String newFolderNameString = editFolderNameEditText.getText().toString();
2501 // Check if the favorite icon has changed.
2502 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2503 // Update the name in the database.
2504 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2505 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2506 // Create the new folder icon Bitmap.
2507 Bitmap folderIconBitmap;
2509 // Populate the new folder icon bitmap.
2510 if (defaultFolderIconRadioButton.isChecked()) {
2511 // Get the default folder icon drawable.
2512 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2514 // Convert the folder icon drawable to a bitmap drawable.
2515 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2517 // Convert the folder icon bitmap drawable to a bitmap.
2518 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2519 } else { // Use the `WebView` favorite icon.
2520 // Copy the favorite icon bitmap to the folder icon bitmap.
2521 folderIconBitmap = favoriteIconBitmap;
2524 // Create a folder icon byte array output stream.
2525 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2527 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2528 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2530 // Convert the folder icon byte array stream to a byte array.
2531 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2533 // Update the folder icon in the database.
2534 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2535 } else { // The folder icon and the name have changed.
2536 // Get the new folder icon `Bitmap`.
2537 Bitmap folderIconBitmap;
2538 if (defaultFolderIconRadioButton.isChecked()) {
2539 // Get the default folder icon drawable.
2540 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2542 // Convert the folder icon drawable to a bitmap drawable.
2543 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2545 // Convert the folder icon bitmap drawable to a bitmap.
2546 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2547 } else { // Use the `WebView` favorite icon.
2548 // Copy the favorite icon bitmap to the folder icon bitmap.
2549 folderIconBitmap = favoriteIconBitmap;
2552 // Create a folder icon byte array output stream.
2553 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2555 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2556 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2558 // Convert the folder icon byte array stream to a byte array.
2559 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2561 // Update the folder name and icon in the database.
2562 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2565 // Update the bookmarks cursor with the current contents of this folder.
2566 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2568 // Update the `ListView`.
2569 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2573 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2574 switch (downloadType) {
2575 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2576 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2577 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2580 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2581 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2582 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2588 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2589 // Get a handle for the fragment manager.
2590 FragmentManager fragmentManager = getSupportFragmentManager();
2592 switch (requestCode) {
2593 case DOWNLOAD_FILE_REQUEST_CODE:
2594 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2595 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2597 // On API 23, displaying the fragment must be delayed or the app will crash.
2598 if (Build.VERSION.SDK_INT == 23) {
2599 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2601 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2604 // Reset the download variables.
2606 downloadContentDisposition = "";
2607 downloadContentLength = 0;
2610 case DOWNLOAD_IMAGE_REQUEST_CODE:
2611 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
2612 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
2614 // On API 23, displaying the fragment must be delayed or the app will crash.
2615 if (Build.VERSION.SDK_INT == 23) {
2616 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2618 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2621 // Reset the image URL variable.
2622 downloadImageUrl = "";
2625 case SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
2626 // Check to see if the storage permission was granted. If the dialog was canceled the grant result will be empty.
2627 if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
2628 // Save the webpage image.
2629 new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
2630 } else { // The storage permission was not granted.
2631 // Display an error snackbar.
2632 Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
2635 // Reset the save website image file path.
2636 saveWebsiteImageFilePath = "";
2642 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
2643 // Download the image if it has an HTTP or HTTPS URI.
2644 if (imageUrl.startsWith("http")) {
2645 // Get a handle for the system `DOWNLOAD_SERVICE`.
2646 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2648 // Parse `imageUrl`.
2649 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
2651 // Get a handle for the cookie manager.
2652 CookieManager cookieManager = CookieManager.getInstance();
2654 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
2655 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2656 if (cookieManager.acceptCookie()) {
2657 // Get the cookies for `imageUrl`.
2658 String cookies = cookieManager.getCookie(imageUrl);
2660 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
2661 downloadRequest.addRequestHeader("Cookie", cookies);
2664 // Get the file name from the dialog fragment.
2665 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
2666 String imageName = downloadImageNameEditText.getText().toString();
2668 // Specify the download location.
2669 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
2670 // Download to the public download directory.
2671 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
2672 } else { // External write permission denied.
2673 // Download to the app's external download directory.
2674 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
2677 // Allow `MediaScanner` to index the download if it is a media file.
2678 downloadRequest.allowScanningByMediaScanner();
2680 // Add the URL as the description for the download.
2681 downloadRequest.setDescription(imageUrl);
2683 // Show the download notification after the download is completed.
2684 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2686 // Remove the lint warning below that `downloadManager` might be `null`.
2687 assert downloadManager != null;
2689 // Initiate the download.
2690 downloadManager.enqueue(downloadRequest);
2691 } else { // The image is not an HTTP or HTTPS URI.
2692 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
2697 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
2698 // Download the file if it has an HTTP or HTTPS URI.
2699 if (downloadUrl.startsWith("http")) {
2700 // Get a handle for the system `DOWNLOAD_SERVICE`.
2701 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2703 // Parse `downloadUrl`.
2704 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
2706 // Get a handle for the cookie manager.
2707 CookieManager cookieManager = CookieManager.getInstance();
2709 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
2710 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2711 if (cookieManager.acceptCookie()) {
2712 // Get the cookies for `downloadUrl`.
2713 String cookies = cookieManager.getCookie(downloadUrl);
2715 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
2716 downloadRequest.addRequestHeader("Cookie", cookies);
2719 // Get the file name from the dialog fragment.
2720 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
2721 String fileName = downloadFileNameEditText.getText().toString();
2723 // Specify the download location.
2724 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
2725 // Download to the public download directory.
2726 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
2727 } else { // External write permission denied.
2728 // Download to the app's external download directory.
2729 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
2732 // Allow `MediaScanner` to index the download if it is a media file.
2733 downloadRequest.allowScanningByMediaScanner();
2735 // Add the URL as the description for the download.
2736 downloadRequest.setDescription(downloadUrl);
2738 // Show the download notification after the download is completed.
2739 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2741 // Remove the lint warning below that `downloadManager` might be `null`.
2742 assert downloadManager != null;
2744 // Initiate the download.
2745 downloadManager.enqueue(downloadRequest);
2746 } else { // The download is not an HTTP or HTTPS URI.
2747 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
2751 // Override `onBackPressed` to handle the navigation drawer and and the WebView.
2753 public void onBackPressed() {
2754 // Get a handle for the drawer layout and the tab layout.
2755 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2756 TabLayout tabLayout = findViewById(R.id.tablayout);
2758 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
2759 // Close the navigation drawer.
2760 drawerLayout.closeDrawer(GravityCompat.START);
2761 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
2762 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
2763 // close the bookmarks drawer.
2764 drawerLayout.closeDrawer(GravityCompat.END);
2765 } else { // A subfolder is displayed.
2766 // Place the former parent folder in `currentFolder`.
2767 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
2769 // Load the new folder.
2770 loadBookmarksFolder();
2772 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
2773 // Reset the current domain name so that navigation works if third-party requests are blocked.
2774 currentWebView.resetCurrentDomainName();
2776 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2777 currentWebView.setNavigatingHistory(true);
2780 currentWebView.goBack();
2781 } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs.
2782 // Close the current tab.
2784 } else { // There isn't anything to do in Privacy Browser.
2785 // Run the default commands.
2786 super.onBackPressed();
2788 // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted.
2793 // Process the results of a file browse.
2795 public void onActivityResult(int requestCode, int resultCode, Intent data) {
2796 // Run the commands that correlate to the specified request code.
2797 switch (requestCode) {
2798 case FILE_UPLOAD_REQUEST_CODE:
2799 // File uploads only work on API >= 21.
2800 if (Build.VERSION.SDK_INT >= 21) {
2801 // Pass the file to the WebView.
2802 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
2806 case BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
2807 // Don't do anything if the user pressed back from the file picker.
2808 if (resultCode == Activity.RESULT_OK) {
2809 // Get a handle for the save dialog fragment.
2810 DialogFragment saveWebpageImageDialogFragment= (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_as_image));
2812 // Only update the file name if the dialog still exists.
2813 if (saveWebpageImageDialogFragment != null) {
2814 // Get a handle for the save webpage image dialog.
2815 Dialog saveWebpageImageDialog = saveWebpageImageDialogFragment.getDialog();
2817 // Get a handle for the file name edit text.
2818 EditText fileNameEditText = saveWebpageImageDialog.findViewById(R.id.file_name_edittext);
2820 // Instantiate the file name helper.
2821 FileNameHelper fileNameHelper = new FileNameHelper();
2823 // Convert the file name URI to a file name path.
2824 String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
2826 // Set the file name path as the text of the file name edit text.
2827 fileNameEditText.setText(fileNamePath);
2834 private void loadUrlFromTextBox() {
2835 // Get a handle for the URL edit text.
2836 EditText urlEditText = findViewById(R.id.url_edittext);
2838 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
2839 String unformattedUrlString = urlEditText.getText().toString().trim();
2841 // Initialize the formatted URL string.
2844 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
2845 if (unformattedUrlString.startsWith("content://")) { // This is a Content URL.
2846 // Load the entire content URL.
2847 url = unformattedUrlString;
2848 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2849 unformattedUrlString.startsWith("file://")) { // This is a standard URL.
2850 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
2851 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2852 unformattedUrlString = "https://" + unformattedUrlString;
2855 // Initialize `unformattedUrl`.
2856 URL unformattedUrl = null;
2858 // 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.
2860 unformattedUrl = new URL(unformattedUrlString);
2861 } catch (MalformedURLException e) {
2862 e.printStackTrace();
2865 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2866 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2867 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2868 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2869 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2870 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2873 Uri.Builder uri = new Uri.Builder();
2874 uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2876 // Decode the URI as a UTF-8 string in.
2878 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2879 } catch (UnsupportedEncodingException exception) {
2880 // Do nothing. The formatted URL string will remain blank.
2882 } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string.
2883 // Create an encoded URL String.
2884 String encodedUrlString;
2886 // Sanitize the search input.
2888 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2889 } catch (UnsupportedEncodingException exception) {
2890 encodedUrlString = "";
2893 // Add the base search URL.
2894 url = searchURL + encodedUrlString;
2897 // 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.
2898 urlEditText.clearFocus();
2904 private void loadUrl(String url) {
2905 // Sanitize the URL.
2906 url = sanitizeUrl(url);
2908 // Apply the domain settings.
2909 applyDomainSettings(currentWebView, url, true, false);
2912 currentWebView.loadUrl(url, customHeaders);
2915 public void findPreviousOnPage(View view) {
2916 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
2917 currentWebView.findNext(false);
2920 public void findNextOnPage(View view) {
2921 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2922 currentWebView.findNext(true);
2925 public void closeFindOnPage(View view) {
2926 // Get a handle for the views.
2927 Toolbar toolbar = findViewById(R.id.toolbar);
2928 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2929 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
2931 // Delete the contents of `find_on_page_edittext`.
2932 findOnPageEditText.setText(null);
2934 // Clear the highlighted phrases if the WebView is not null.
2935 if (currentWebView != null) {
2936 currentWebView.clearMatches();
2939 // Hide the find on page linear layout.
2940 findOnPageLinearLayout.setVisibility(View.GONE);
2942 // Show the toolbar.
2943 toolbar.setVisibility(View.VISIBLE);
2945 // Get a handle for the input method manager.
2946 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
2948 // Remove the lint warning below that the input method manager might be null.
2949 assert inputMethodManager != null;
2951 // Hide the keyboard.
2952 inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
2956 public void onSaveWebpageImage(DialogFragment dialogFragment) {
2957 // Get a handle for the file name edit text.
2958 EditText fileNameEditText = dialogFragment.getDialog().findViewById(R.id.file_name_edittext);
2960 // Get the file path string.
2961 saveWebsiteImageFilePath = fileNameEditText.getText().toString();
2963 // Check to see if the storage permission is needed.
2964 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
2965 // Save the webpage image.
2966 new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
2967 } else { // The storage permission has not been granted.
2968 // Get the external private directory `File`.
2969 File externalPrivateDirectoryFile = getExternalFilesDir(null);
2971 // Remove the incorrect lint error below that the file might be null.
2972 assert externalPrivateDirectoryFile != null;
2974 // Get the external private directory string.
2975 String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
2977 // Check to see if the file path is in the external private directory.
2978 if (saveWebsiteImageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
2979 // Save the webpage image.
2980 new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
2981 } else { // The file path is in a public directory.
2982 // Check if the user has previously denied the storage permission.
2983 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2984 // Instantiate the storage permission alert dialog.
2985 DialogFragment storagePermissionDialogFragment = new StoragePermissionDialog();
2987 // Show the storage permission alert dialog. The permission will be requested when the dialog is closed.
2988 storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
2989 } else { // Show the permission request directly.
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);
2998 public void onCloseStoragePermissionDialog() {
2999 // Request the write external storage permission. The webpage image will be saved when it finishes.
3000 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
3003 private void applyAppSettings() {
3004 // 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.
3005 if (webViewDefaultUserAgent == null) {
3009 // Get a handle for the shared preferences.
3010 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3012 // Store the values from the shared preferences in variables.
3013 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3014 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3015 sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true);
3016 sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true);
3017 sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true);
3018 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3019 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3020 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3021 scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
3023 // Get handles for the views that need to be modified.
3024 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3025 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
3026 ActionBar actionBar = getSupportActionBar();
3027 Toolbar toolbar = findViewById(R.id.toolbar);
3028 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3029 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
3030 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3032 // Remove the incorrect lint warning below that the action bar might be null.
3033 assert actionBar != null;
3035 // Apply the proxy through Orbot settings.
3036 applyProxyThroughOrbot(false);
3038 // Set Do Not Track status.
3039 if (doNotTrackEnabled) {
3040 customHeaders.put("DNT", "1");
3042 customHeaders.remove("DNT");
3045 // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3046 CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
3047 AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
3048 AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams();
3049 AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams();
3051 // Add the scrolling behavior to the layout parameters.
3053 // Enable scrolling of the app bar.
3054 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
3055 toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3056 findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3057 tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3059 // Disable scrolling of the app bar.
3060 swipeRefreshLayoutParams.setBehavior(null);
3061 toolbarLayoutParams.setScrollFlags(0);
3062 findOnPageLayoutParams.setScrollFlags(0);
3063 tabsLayoutParams.setScrollFlags(0);
3065 // Expand the app bar if it is currently collapsed.
3066 appBarLayout.setExpanded(true);
3069 // Apply the modified layout parameters.
3070 swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams);
3071 toolbar.setLayoutParams(toolbarLayoutParams);
3072 findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams);
3073 tabsLinearLayout.setLayoutParams(tabsLayoutParams);
3075 // Set the app bar scrolling for each WebView.
3076 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3077 // Get the WebView tab fragment.
3078 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3080 // Get the fragment view.
3081 View fragmentView = webViewTabFragment.getView();
3083 // Only modify the WebViews if they exist.
3084 if (fragmentView != null) {
3085 // Get the nested scroll WebView from the tab fragment.
3086 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3088 // Set the app bar scrolling.
3089 nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
3093 // Update the full screen browsing mode settings.
3094 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3095 // Update the visibility of the app bar, which might have changed in the settings.
3097 // Hide the tab linear layout.
3098 tabsLinearLayout.setVisibility(View.GONE);
3100 // Hide the action bar.
3103 // Show the tab linear layout.
3104 tabsLinearLayout.setVisibility(View.VISIBLE);
3106 // Show the action bar.
3110 // Hide the banner ad in the free flavor.
3111 if (BuildConfig.FLAVOR.contentEquals("free")) {
3112 AdHelper.hideAd(findViewById(R.id.adview));
3115 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3116 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3118 /* Hide the system bars.
3119 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3120 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3121 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3122 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3124 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3125 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3126 } else { // Privacy Browser is not in full screen browsing mode.
3127 // 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.
3128 inFullScreenBrowsingMode = false;
3130 // Show the tab linear layout.
3131 tabsLinearLayout.setVisibility(View.VISIBLE);
3133 // Show the action bar.
3136 // Show the banner ad in the free flavor.
3137 if (BuildConfig.FLAVOR.contentEquals("free")) {
3138 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3139 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3142 // Remove the `SYSTEM_UI` flags from the root frame layout.
3143 rootFrameLayout.setSystemUiVisibility(0);
3145 // Add the translucent status flag.
3146 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3150 private void initializeApp() {
3151 // Get a handle for the shared preferences.
3152 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3154 // Get the theme preference.
3155 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3157 // Get a handle for the input method.
3158 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3160 // Remove the lint warning below that the input method manager might be null.
3161 assert inputMethodManager != null;
3163 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
3164 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
3165 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
3166 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
3168 // Get handles for the URL views.
3169 EditText urlEditText = findViewById(R.id.url_edittext);
3171 // Remove the formatting from the URL edit text when the user is editing the text.
3172 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
3173 if (hasFocus) { // The user is editing the URL text box.
3174 // Remove the highlighting.
3175 urlEditText.getText().removeSpan(redColorSpan);
3176 urlEditText.getText().removeSpan(initialGrayColorSpan);
3177 urlEditText.getText().removeSpan(finalGrayColorSpan);
3178 } else { // The user has stopped editing the URL text box.
3179 // Move to the beginning of the string.
3180 urlEditText.setSelection(0);
3182 // Reapply the highlighting.
3187 // Set the go button on the keyboard to load the URL in `urlTextBox`.
3188 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
3189 // If the event is a key-down event on the `enter` button, load the URL.
3190 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
3191 // Load the URL into the mainWebView and consume the event.
3192 loadUrlFromTextBox();
3194 // If the enter key was pressed, consume the event.
3197 // If any other key was pressed, do not consume the event.
3202 // Initialize the Orbot status and the waiting for Orbot trackers.
3203 orbotStatus = "unknown";
3204 waitingForOrbot = false;
3206 // Create an Orbot status `BroadcastReceiver`.
3207 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
3209 public void onReceive(Context context, Intent intent) {
3210 // Store the content of the status message in `orbotStatus`.
3211 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
3213 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
3214 if (orbotStatus.equals("ON") && waitingForOrbot) {
3215 // Reset the waiting for Orbot status.
3216 waitingForOrbot = false;
3218 // Get the intent that started the app.
3219 Intent launchingIntent = getIntent();
3221 // Get the information from the intent.
3222 String launchingIntentAction = launchingIntent.getAction();
3223 Uri launchingIntentUriData = launchingIntent.getData();
3225 // If the intent action is a web search, perform the search.
3226 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
3227 // Create an encoded URL string.
3228 String encodedUrlString;
3230 // Sanitize the search input and convert it to a search.
3232 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
3233 } catch (UnsupportedEncodingException exception) {
3234 encodedUrlString = "";
3237 // Load the completed search URL.
3238 loadUrl(searchURL + encodedUrlString);
3239 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
3240 // Load the URL from the intent.
3241 loadUrl(launchingIntentUriData.toString());
3242 } else { // The is no URL in the intent.
3243 // Select the homepage based on the proxy through Orbot status.
3244 if (proxyThroughOrbot) {
3245 // Load the Tor homepage.
3246 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
3248 // Load the normal homepage.
3249 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
3256 // Register `orbotStatusBroadcastReceiver` on `this` context.
3257 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
3259 // Get handles for views that need to be modified.
3260 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3261 NavigationView navigationView = findViewById(R.id.navigationview);
3262 TabLayout tabLayout = findViewById(R.id.tablayout);
3263 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3264 ViewPager webViewPager = findViewById(R.id.webviewpager);
3265 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
3266 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
3267 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
3268 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
3269 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3271 // Listen for touches on the navigation menu.
3272 navigationView.setNavigationItemSelectedListener(this);
3274 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
3275 Menu navigationMenu = navigationView.getMenu();
3276 MenuItem navigationBackMenuItem = navigationMenu.getItem(2);
3277 MenuItem navigationForwardMenuItem = navigationMenu.getItem(3);
3278 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4);
3279 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
3281 // Update the web view pager every time a tab is modified.
3282 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
3284 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
3289 public void onPageSelected(int position) {
3290 // Close the find on page bar if it is open.
3291 closeFindOnPage(null);
3293 // Set the current WebView.
3294 setCurrentWebView(position);
3296 // 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.
3297 if (tabLayout.getSelectedTabPosition() != position) {
3298 // Create a handler to select the tab.
3299 Handler selectTabHandler = new Handler();
3301 // Create a runnable to select the tab.
3302 Runnable selectTabRunnable = () -> {
3303 // Get a handle for the tab.
3304 TabLayout.Tab tab = tabLayout.getTabAt(position);
3306 // Assert that the tab is not null.
3313 // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated.
3314 selectTabHandler.postDelayed(selectTabRunnable, 150);
3319 public void onPageScrollStateChanged(int state) {
3324 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
3325 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
3327 public void onTabSelected(TabLayout.Tab tab) {
3328 // Select the same page in the view pager.
3329 webViewPager.setCurrentItem(tab.getPosition());
3333 public void onTabUnselected(TabLayout.Tab tab) {
3338 public void onTabReselected(TabLayout.Tab tab) {
3339 // Instantiate the View SSL Certificate dialog.
3340 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
3342 // Display the View SSL Certificate dialog.
3343 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
3347 // 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.
3348 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
3350 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
3351 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
3352 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
3353 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
3355 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
3356 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
3357 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
3358 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
3361 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
3362 launchBookmarksActivityFab.setOnClickListener(v -> {
3363 // Get a copy of the favorite icon bitmap.
3364 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
3366 // Create a favorite icon byte array output stream.
3367 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3369 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
3370 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
3372 // Convert the favorite icon byte array stream to a byte array.
3373 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
3375 // Create an intent to launch the bookmarks activity.
3376 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
3378 // Add the extra information to the intent.
3379 bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
3380 bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
3381 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
3382 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
3385 startActivity(bookmarksIntent);
3388 // Set the create new bookmark folder FAB to display an alert dialog.
3389 createBookmarkFolderFab.setOnClickListener(v -> {
3390 // Create a create bookmark folder dialog.
3391 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
3393 // Show the create bookmark folder dialog.
3394 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
3397 // Set the create new bookmark FAB to display an alert dialog.
3398 createBookmarkFab.setOnClickListener(view -> {
3399 // Instantiate the create bookmark dialog.
3400 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
3402 // Display the create bookmark dialog.
3403 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
3406 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
3407 findOnPageEditText.addTextChangedListener(new TextWatcher() {
3409 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3414 public void onTextChanged(CharSequence s, int start, int before, int count) {
3419 public void afterTextChanged(Editable s) {
3420 // 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.
3421 if (currentWebView != null) {
3422 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
3427 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
3428 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
3429 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
3430 // Hide the soft keyboard.
3431 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3433 // Consume the event.
3435 } else { // A different key was pressed.
3436 // Do not consume the event.
3441 // Implement swipe to refresh.
3442 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
3444 // Store the default progress view offsets for use later in `initializeWebView()`.
3445 defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
3446 defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
3448 // Set the swipe to refresh color according to the theme.
3450 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
3451 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
3453 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
3456 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
3457 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
3458 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
3460 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
3461 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
3463 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
3464 currentBookmarksFolder = "";
3466 // Load the home folder, which is `""` in the database.
3467 loadBookmarksFolder();
3469 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
3470 // Convert the id from long to int to match the format of the bookmarks database.
3471 int databaseID = (int) id;
3473 // Get the bookmark cursor for this ID and move it to the first row.
3474 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
3475 bookmarkCursor.moveToFirst();
3477 // Act upon the bookmark according to the type.
3478 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
3479 // Store the new folder name in `currentBookmarksFolder`.
3480 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
3482 // Load the new folder.
3483 loadBookmarksFolder();
3484 } else { // The selected bookmark is not a folder.
3485 // Load the bookmark URL.
3486 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
3488 // Close the bookmarks drawer.
3489 drawerLayout.closeDrawer(GravityCompat.END);
3492 // Close the `Cursor`.
3493 bookmarkCursor.close();
3496 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
3497 // Convert the database ID from `long` to `int`.
3498 int databaseId = (int) id;
3500 // Find out if the selected bookmark is a folder.
3501 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
3504 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
3505 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
3507 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
3508 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3509 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
3511 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
3512 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3513 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
3516 // Consume the event.
3520 // Get the status bar pixel size.
3521 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
3522 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
3524 // Get the resource density.
3525 float screenDensity = getResources().getDisplayMetrics().density;
3527 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
3528 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
3529 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
3530 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
3532 // The drawer listener is used to update the navigation menu.`
3533 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
3535 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
3539 public void onDrawerOpened(@NonNull View drawerView) {
3543 public void onDrawerClosed(@NonNull View drawerView) {
3547 public void onDrawerStateChanged(int newState) {
3548 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
3549 // Get handles for the drawer headers.
3550 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
3551 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
3553 // 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.
3554 if (navigationHeaderTextView != null) {
3555 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
3558 // 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.
3559 if (bookmarksHeaderTextView != null) {
3560 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
3563 // Update the navigation menu items if the WebView is not null.
3564 if (currentWebView != null) {
3565 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
3566 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
3567 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
3568 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
3570 // Hide the keyboard (if displayed).
3571 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3574 // 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.
3575 urlEditText.clearFocus();
3576 currentWebView.clearFocus();
3581 // 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).
3582 customHeaders.put("X-Requested-With", "");
3584 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
3585 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
3587 // Get a handle for the WebView.
3588 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
3590 // Store the default user agent.
3591 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
3593 // Destroy the bare WebView.
3594 bareWebView.destroy();
3597 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3598 @SuppressLint("SetJavaScriptEnabled")
3599 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) {
3600 // Store a copy of the current user agent to track changes for the return boolean.
3601 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3603 // Store the current URL.
3604 nestedScrollWebView.setCurrentUrl(url);
3606 // Parse the URL into a URI.
3607 Uri uri = Uri.parse(url);
3609 // Extract the domain from `uri`.
3610 String newHostName = uri.getHost();
3612 // Strings don't like to be null.
3613 if (newHostName == null) {
3617 // 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.
3618 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3619 // Set the new host name as the current domain name.
3620 nestedScrollWebView.setCurrentDomainName(newHostName);
3622 // Reset the ignoring of pinned domain information.
3623 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3625 // Clear any pinned SSL certificate or IP addresses.
3626 nestedScrollWebView.clearPinnedSslCertificate();
3627 nestedScrollWebView.clearPinnedIpAddresses();
3629 // Reset the favorite icon if specified.
3631 // Initialize the favorite icon.
3632 nestedScrollWebView.initializeFavoriteIcon();
3634 // Get the current page position.
3635 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3637 // Get a handle for the tab layout.
3638 TabLayout tabLayout = findViewById(R.id.tablayout);
3640 // Get the corresponding tab.
3641 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3643 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3645 // Get the tab custom view.
3646 View tabCustomView = tab.getCustomView();
3648 // Remove the warning below that the tab custom view might be null.
3649 assert tabCustomView != null;
3651 // Get the tab views.
3652 ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3653 TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3655 // Set the default favorite icon as the favorite icon for this tab.
3656 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3658 // Set the loading title text.
3659 tabTitleTextView.setText(R.string.loading);
3663 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3664 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3666 // Get a full cursor from `domainsDatabaseHelper`.
3667 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3669 // Initialize `domainSettingsSet`.
3670 Set<String> domainSettingsSet = new HashSet<>();
3672 // Get the domain name column index.
3673 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3675 // Populate `domainSettingsSet`.
3676 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3677 // Move `domainsCursor` to the current row.
3678 domainNameCursor.moveToPosition(i);
3680 // Store the domain name in `domainSettingsSet`.
3681 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3684 // Close `domainNameCursor.
3685 domainNameCursor.close();
3687 // Initialize the domain name in database variable.
3688 String domainNameInDatabase = null;
3690 // Check the hostname against the domain settings set.
3691 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3692 // Record the domain name in the database.
3693 domainNameInDatabase = newHostName;
3695 // Set the domain settings applied tracker to true.
3696 nestedScrollWebView.setDomainSettingsApplied(true);
3697 } else { // The hostname is not contained in the domain settings set.
3698 // Set the domain settings applied tracker to false.
3699 nestedScrollWebView.setDomainSettingsApplied(false);
3702 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3703 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3704 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3705 // Set the domain settings applied tracker to true.
3706 nestedScrollWebView.setDomainSettingsApplied(true);
3708 // Store the applied domain names as it appears in the database.
3709 domainNameInDatabase = "*." + newHostName;
3712 // Strip out the lowest subdomain of of the host name.
3713 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3717 // Get a handle for the shared preferences.
3718 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3720 // Store the general preference information.
3721 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3722 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3723 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3724 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3725 boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true);
3726 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3728 // Get a handle for the cookie manager.
3729 CookieManager cookieManager = CookieManager.getInstance();
3731 // Get handles for the views.
3732 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3733 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3735 // Initialize the user agent array adapter and string array.
3736 ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3737 String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3739 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3740 // Get a cursor for the current host and move it to the first position.
3741 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3742 currentDomainSettingsCursor.moveToFirst();
3744 // Get the settings from the cursor.
3745 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3746 nestedScrollWebView.setDomainSettingsJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3747 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3748 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3749 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3750 // Form data can be removed once the minimum API >= 26.
3751 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3752 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST,
3753 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3754 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY,
3755 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3756 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3757 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3758 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3759 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3760 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)) == 1);
3761 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY,
3762 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3763 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3764 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3765 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3766 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3767 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3768 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3769 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT));
3770 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3771 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3772 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3773 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3774 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3775 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3776 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3777 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3778 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3779 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3781 // Create the pinned SSL date variables.
3782 Date pinnedSslStartDate;
3783 Date pinnedSslEndDate;
3785 // 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.
3786 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3787 pinnedSslStartDate = null;
3789 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3792 // 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.
3793 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3794 pinnedSslEndDate = null;
3796 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3799 // If there is a pinned SSL certificate, store it in the WebView.
3800 if (pinnedSslCertificate) {
3801 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3802 pinnedSslStartDate, pinnedSslEndDate);
3805 // If there is a pinned IP address, store it in the WebView.
3806 if (pinnedIpAddresses) {
3807 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3810 // Set night mode according to the night mode int.
3811 switch (nightModeInt) {
3812 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3813 // Set night mode according to the current default.
3814 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3817 case DomainsDatabaseHelper.ENABLED:
3818 // Enable night mode.
3819 nestedScrollWebView.setNightMode(true);
3822 case DomainsDatabaseHelper.DISABLED:
3823 // Disable night mode.
3824 nestedScrollWebView.setNightMode(false);
3828 // Enable JavaScript if night mode is enabled.
3829 if (nestedScrollWebView.getNightMode()) {
3830 // Enable JavaScript.
3831 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3833 // Set JavaScript according to the domain settings.
3834 nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled());
3837 // Close the current host domain settings cursor.
3838 currentDomainSettingsCursor.close();
3840 // Apply the domain settings.
3841 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3843 // Set third-party cookies status if API >= 21.
3844 if (Build.VERSION.SDK_INT >= 21) {
3845 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3848 // Apply the form data setting if the API < 26.
3849 if (Build.VERSION.SDK_INT < 26) {
3850 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3853 // Apply the font size.
3854 if (fontSize == 0) { // Apply the default font size.
3855 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3856 } else { // Apply the specified font size.
3857 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3860 // Set the user agent.
3861 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3862 // Get the array position of the default user agent name.
3863 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3865 // Set the user agent according to the system default.
3866 switch (defaultUserAgentArrayPosition) {
3867 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3868 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3869 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3872 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3873 // Set the user agent to `""`, which uses the default value.
3874 nestedScrollWebView.getSettings().setUserAgentString("");
3877 case SETTINGS_CUSTOM_USER_AGENT:
3878 // Set the default custom user agent.
3879 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3883 // Get the user agent string from the user agent data array
3884 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3886 } else { // Set the user agent according to the stored name.
3887 // Get the array position of the user agent name.
3888 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3890 switch (userAgentArrayPosition) {
3891 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3892 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3895 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3896 // Set the user agent to `""`, which uses the default value.
3897 nestedScrollWebView.getSettings().setUserAgentString("");
3901 // Get the user agent string from the user agent data array.
3902 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3906 // Set swipe to refresh.
3907 switch (swipeToRefreshInt) {
3908 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3909 // Store the swipe to refresh status in the nested scroll WebView.
3910 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3912 // 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.
3913 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3916 case DomainsDatabaseHelper.ENABLED:
3917 // Store the swipe to refresh status in the nested scroll WebView.
3918 nestedScrollWebView.setSwipeToRefresh(true);
3920 // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3921 swipeRefreshLayout.setEnabled(true);
3924 case DomainsDatabaseHelper.DISABLED:
3925 // Store the swipe to refresh status in the nested scroll WebView.
3926 nestedScrollWebView.setSwipeToRefresh(false);
3928 // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3929 swipeRefreshLayout.setEnabled(false);
3932 // Set the viewport.
3933 switch (wideViewportInt) {
3934 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3935 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
3938 case DomainsDatabaseHelper.ENABLED:
3939 nestedScrollWebView.getSettings().setUseWideViewPort(true);
3942 case DomainsDatabaseHelper.DISABLED:
3943 nestedScrollWebView.getSettings().setUseWideViewPort(false);
3947 // Set the loading of webpage images.
3948 switch (displayWebpageImagesInt) {
3949 case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3950 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3953 case DomainsDatabaseHelper.ENABLED:
3954 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3957 case DomainsDatabaseHelper.DISABLED:
3958 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3962 // 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.
3964 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3966 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3968 } else { // The new URL does not have custom domain settings. Load the defaults.
3969 // Store the values from the shared preferences.
3970 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3971 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
3972 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3973 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3974 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3975 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true));
3976 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3977 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3978 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3979 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, sharedPreferences.getBoolean("ultralist", true));
3980 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3981 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3982 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3984 // Enable JavaScript if night mode is enabled.
3985 if (nestedScrollWebView.getNightMode()) {
3986 // Enable JavaScript.
3987 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3989 // Set JavaScript according to the domain settings.
3990 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3993 // Apply the default settings.
3994 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3995 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3997 // Apply the form data setting if the API < 26.
3998 if (Build.VERSION.SDK_INT < 26) {
3999 nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
4002 // Store the swipe to refresh status in the nested scroll WebView.
4003 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
4005 // Apply swipe to refresh according to the default.
4006 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
4008 // Reset the pinned variables.
4009 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
4011 // Set third-party cookies status if API >= 21.
4012 if (Build.VERSION.SDK_INT >= 21) {
4013 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
4016 // Get the array position of the user agent name.
4017 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4019 // Set the user agent.
4020 switch (userAgentArrayPosition) {
4021 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
4022 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4023 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4026 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4027 // Set the user agent to `""`, which uses the default value.
4028 nestedScrollWebView.getSettings().setUserAgentString("");
4031 case SETTINGS_CUSTOM_USER_AGENT:
4032 // Set the default custom user agent.
4033 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4037 // Get the user agent string from the user agent data array
4038 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4041 // Set the viewport.
4042 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4044 // Set the loading of webpage images.
4045 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4047 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
4048 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4051 // Close the domains database helper.
4052 domainsDatabaseHelper.close();
4054 // Update the privacy icons.
4055 updatePrivacyIcons(true);
4058 // Reload the website if returning from the Domains activity.
4059 if (reloadWebsite) {
4060 nestedScrollWebView.reload();
4063 // Return the user agent changed status.
4064 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
4067 private void applyProxyThroughOrbot(boolean reloadWebsite) {
4068 // Get a handle for the shared preferences.
4069 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4071 // Get the search and theme preferences.
4072 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
4073 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
4074 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
4075 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4076 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4078 // Get a handle for the app bar layout.
4079 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4081 // Set the homepage, search, and proxy options.
4082 if (proxyThroughOrbot) { // Set the Tor options.
4083 // Set the search URL.
4084 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
4085 searchURL = torSearchCustomUrlString;
4086 } else { // Use the string from the pre-built list.
4087 searchURL = torSearchString;
4090 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
4091 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4093 // Set the app bar background to indicate proxying through Orbot is enabled.
4095 appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4097 appBarLayout.setBackgroundResource(R.color.blue_50);
4100 // Check to see if Orbot is ready.
4101 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
4102 // Set `waitingForOrbot`.
4103 waitingForOrbot = true;
4105 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4106 currentWebView.getSettings().setUseWideViewPort(false);
4108 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
4109 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
4110 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
4111 // Reload the website.
4112 currentWebView.reload();
4114 } else { // Set the non-Tor options.
4115 // Set the search URL.
4116 if (searchString.equals("Custom URL")) { // Get the custom URL string.
4117 searchURL = searchCustomUrlString;
4118 } else { // Use the string from the pre-built list.
4119 searchURL = searchString;
4122 // Reset the proxy to default. The host is `""` and the port is `"0"`.
4123 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4125 // Set the default app bar layout background.
4127 appBarLayout.setBackgroundResource(R.color.gray_900);
4129 appBarLayout.setBackgroundResource(R.color.gray_100);
4132 // Reset `waitingForOrbot.
4133 waitingForOrbot = false;
4135 // Reload the WebViews if requested.
4136 if (reloadWebsite) {
4137 // Reload the WebViews.
4138 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4139 // Get the WebView tab fragment.
4140 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4142 // Get the fragment view.
4143 View fragmentView = webViewTabFragment.getView();
4145 // Only reload the WebViews if they exist.
4146 if (fragmentView != null) {
4147 // Get the nested scroll WebView from the tab fragment.
4148 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4150 // Reload the WebView.
4151 nestedScrollWebView.reload();
4158 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4159 // Only update the privacy icons if the options menu and the current WebView have already been populated.
4160 if ((optionsMenu != null) && (currentWebView != null)) {
4161 // Get a handle for the shared preferences.
4162 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4164 // Get the theme and screenshot preferences.
4165 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4167 // Get handles for the menu items.
4168 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
4169 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
4170 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
4171 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
4173 // Update the privacy icon.
4174 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
4175 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4176 } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled.
4177 privacyMenuItem.setIcon(R.drawable.warning);
4178 } else { // All the dangerous features are disabled.
4179 privacyMenuItem.setIcon(R.drawable.privacy_mode);
4182 // Update the first-party cookies icon.
4183 if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled.
4184 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4185 } else { // First-party cookies are disabled.
4187 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4189 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4193 // Update the DOM storage icon.
4194 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
4195 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4196 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
4198 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4200 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4202 } else { // JavaScript is disabled, so DOM storage is ghosted.
4204 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4206 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4210 // Update the refresh icon.
4212 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4214 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4217 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4218 if (runInvalidateOptionsMenu) {
4219 invalidateOptionsMenu();
4224 private void openUrlWithExternalApp(String url) {
4225 // Create a download intent. Not specifying the action type will display the maximum number of options.
4226 Intent downloadIntent = new Intent();
4228 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4229 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4231 // Flag the intent to open in a new task.
4232 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4234 // Show the chooser.
4235 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4238 private void highlightUrlText() {
4239 // Get a handle for the URL edit text.
4240 EditText urlEditText = findViewById(R.id.url_edittext);
4242 // Only highlight the URL text if the box is not currently selected.
4243 if (!urlEditText.hasFocus()) {
4244 // Get the URL string.
4245 String urlString = urlEditText.getText().toString();
4247 // Highlight the URL according to the protocol.
4248 if (urlString.startsWith("file://")) { // This is a file URL.
4249 // De-emphasize only the protocol.
4250 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4251 } else if (urlString.startsWith("content://")) {
4252 // De-emphasize only the protocol.
4253 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4254 } else { // This is a web URL.
4255 // Get the index of the `/` immediately after the domain name.
4256 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4258 // Create a base URL string.
4261 // Get the base URL.
4262 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4263 // Get the base URL.
4264 baseUrl = urlString.substring(0, endOfDomainName);
4265 } else { // There are no characters after the base URL.
4266 // Set the base URL to be the entire URL string.
4267 baseUrl = urlString;
4270 // Get the index of the last `.` in the domain.
4271 int lastDotIndex = baseUrl.lastIndexOf(".");
4273 // Get the index of the penultimate `.` in the domain.
4274 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4276 // Markup the beginning of the URL.
4277 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4278 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4280 // De-emphasize subdomains.
4281 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4282 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4284 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4285 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4286 // De-emphasize the protocol and the additional subdomains.
4287 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4288 } else { // There is only one subdomain in the domain name.
4289 // De-emphasize only the protocol.
4290 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4294 // De-emphasize the text after the domain name.
4295 if (endOfDomainName > 0) {
4296 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4302 private void loadBookmarksFolder() {
4303 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4304 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4306 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4307 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4309 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4310 // Inflate the individual item layout. `false` does not attach it to the root.
4311 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4315 public void bindView(View view, Context context, Cursor cursor) {
4316 // Get handles for the views.
4317 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4318 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4320 // Get the favorite icon byte array from the cursor.
4321 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4323 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4324 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4326 // Display the bitmap in `bookmarkFavoriteIcon`.
4327 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4329 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4330 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4331 bookmarkNameTextView.setText(bookmarkNameString);
4333 // Make the font bold for folders.
4334 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4335 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4336 } else { // Reset the font to default for normal bookmarks.
4337 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4342 // Get a handle for the bookmarks list view.
4343 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4345 // Populate the list view with the adapter.
4346 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4348 // Get a handle for the bookmarks title text view.
4349 TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4351 // Set the bookmarks drawer title.
4352 if (currentBookmarksFolder.isEmpty()) {
4353 bookmarksTitleTextView.setText(R.string.bookmarks);
4355 bookmarksTitleTextView.setText(currentBookmarksFolder);
4359 private void openWithApp(String url) {
4360 // Create the open with intent with `ACTION_VIEW`.
4361 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4363 // Set the URI but not the MIME type. This should open all available apps.
4364 openWithAppIntent.setData(Uri.parse(url));
4366 // Flag the intent to open in a new task.
4367 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4370 // Show the chooser.
4371 startActivity(openWithAppIntent);
4372 } catch (ActivityNotFoundException exception) {
4373 // Show a snackbar with the error.
4374 Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
4378 private void openWithBrowser(String url) {
4379 // Create the open with intent with `ACTION_VIEW`.
4380 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4382 // Set the URI and the MIME type. `"text/html"` should load browser options.
4383 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4385 // Flag the intent to open in a new task.
4386 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4389 // Show the chooser.
4390 startActivity(openWithBrowserIntent);
4391 } catch (ActivityNotFoundException exception) {
4392 // Show a snackbar with the error.
4393 Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
4397 private String sanitizeUrl(String url) {
4398 // Sanitize Google Analytics.
4399 if (sanitizeGoogleAnalytics) {
4401 if (url.contains("?utm_")) {
4402 url = url.substring(0, url.indexOf("?utm_"));
4406 if (url.contains("&utm_")) {
4407 url = url.substring(0, url.indexOf("&utm_"));
4411 // Sanitize Facebook Click IDs.
4412 if (sanitizeFacebookClickIds) {
4413 // Remove `?fbclid=`.
4414 if (url.contains("?fbclid=")) {
4415 url = url.substring(0, url.indexOf("?fbclid="));
4418 // Remove `&fbclid=`.
4419 if (url.contains("&fbclid=")) {
4420 url = url.substring(0, url.indexOf("&fbclid="));
4424 // Sanitize Twitter AMP redirects.
4425 if (sanitizeTwitterAmpRedirects) {
4427 if (url.contains("?amp=1")) {
4428 url = url.substring(0, url.indexOf("?amp=1"));
4432 // Return the sanitized URL.
4436 public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4437 // Store the blocklists.
4438 easyList = combinedBlocklists.get(0);
4439 easyPrivacy = combinedBlocklists.get(1);
4440 fanboysAnnoyanceList = combinedBlocklists.get(2);
4441 fanboysSocialList = combinedBlocklists.get(3);
4442 ultraList = combinedBlocklists.get(4);
4443 ultraPrivacy = combinedBlocklists.get(5);
4445 // Add the first tab.
4446 addNewTab("", true);
4449 public void addTab(View view) {
4450 // Add a new tab with a blank URL.
4451 addNewTab("", true);
4454 private void addNewTab(String url, boolean moveToTab) {
4455 // Sanitize the URL.
4456 url = sanitizeUrl(url);
4458 // Get a handle for the tab layout and the view pager.
4459 TabLayout tabLayout = findViewById(R.id.tablayout);
4460 ViewPager webViewPager = findViewById(R.id.webviewpager);
4462 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4463 int newTabNumber = tabLayout.getTabCount();
4466 tabLayout.addTab(tabLayout.newTab());
4469 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4471 // Remove the lint warning below that the current tab might be null.
4472 assert newTab != null;
4474 // Set a custom view on the new tab.
4475 newTab.setCustomView(R.layout.tab_custom_view);
4477 // Add the new WebView page.
4478 webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab);
4481 public void closeTab(View view) {
4482 // Get a handle for the tab layout.
4483 TabLayout tabLayout = findViewById(R.id.tablayout);
4485 // Run the command according to the number of tabs.
4486 if (tabLayout.getTabCount() > 1) { // There is more than one tab open.
4487 // Close the current tab.
4489 } else { // There is only one tab open.
4494 private void closeCurrentTab() {
4495 // Get handles for the views.
4496 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4497 TabLayout tabLayout = findViewById(R.id.tablayout);
4498 ViewPager webViewPager = findViewById(R.id.webviewpager);
4500 // Get the current tab number.
4501 int currentTabNumber = tabLayout.getSelectedTabPosition();
4503 // Delete the current tab.
4504 tabLayout.removeTabAt(currentTabNumber);
4506 // 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.
4507 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4508 setCurrentWebView(currentTabNumber);
4511 // Expand the app bar if it is currently collapsed.
4512 appBarLayout.setExpanded(true);
4515 private void clearAndExit() {
4516 // Get a handle for the shared preferences.
4517 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4519 // Close the bookmarks cursor and database.
4520 bookmarksCursor.close();
4521 bookmarksDatabaseHelper.close();
4523 // Get the status of the clear everything preference.
4524 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
4526 // Get a handle for the runtime.
4527 Runtime runtime = Runtime.getRuntime();
4529 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4530 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4531 String privateDataDirectoryString = getApplicationInfo().dataDir;
4534 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
4535 // The command to remove cookies changed slightly in API 21.
4536 if (Build.VERSION.SDK_INT >= 21) {
4537 CookieManager.getInstance().removeAllCookies(null);
4539 CookieManager.getInstance().removeAllCookie();
4542 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4544 // Two commands must be used because `Runtime.exec()` does not like `*`.
4545 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4546 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4548 // Wait until the processes have finished.
4549 deleteCookiesProcess.waitFor();
4550 deleteCookiesJournalProcess.waitFor();
4551 } catch (Exception exception) {
4552 // Do nothing if an error is thrown.
4556 // Clear DOM storage.
4557 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
4558 // Ask `WebStorage` to clear the DOM storage.
4559 WebStorage webStorage = WebStorage.getInstance();
4560 webStorage.deleteAllData();
4562 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4564 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4565 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4567 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4568 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4569 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4570 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4571 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4573 // Wait until the processes have finished.
4574 deleteLocalStorageProcess.waitFor();
4575 deleteIndexProcess.waitFor();
4576 deleteQuotaManagerProcess.waitFor();
4577 deleteQuotaManagerJournalProcess.waitFor();
4578 deleteDatabaseProcess.waitFor();
4579 } catch (Exception exception) {
4580 // Do nothing if an error is thrown.
4584 // Clear form data if the API < 26.
4585 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
4586 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4587 webViewDatabase.clearFormData();
4589 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4591 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4592 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4593 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4595 // Wait until the processes have finished.
4596 deleteWebDataProcess.waitFor();
4597 deleteWebDataJournalProcess.waitFor();
4598 } catch (Exception exception) {
4599 // Do nothing if an error is thrown.
4604 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
4605 // Clear the cache from each WebView.
4606 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4607 // Get the WebView tab fragment.
4608 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4610 // Get the fragment view.
4611 View fragmentView = webViewTabFragment.getView();
4613 // Only clear the cache if the WebView exists.
4614 if (fragmentView != null) {
4615 // Get the nested scroll WebView from the tab fragment.
4616 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4618 // Clear the cache for this WebView.
4619 nestedScrollWebView.clearCache(true);
4623 // Manually delete the cache directories.
4625 // Delete the main cache directory.
4626 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4628 // Delete the secondary `Service Worker` cache directory.
4629 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4630 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
4632 // Wait until the processes have finished.
4633 deleteCacheProcess.waitFor();
4634 deleteServiceWorkerProcess.waitFor();
4635 } catch (Exception exception) {
4636 // Do nothing if an error is thrown.
4640 // Wipe out each WebView.
4641 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4642 // Get the WebView tab fragment.
4643 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4645 // Get the fragment view.
4646 View fragmentView = webViewTabFragment.getView();
4648 // Only wipe out the WebView if it exists.
4649 if (fragmentView != null) {
4650 // Get the nested scroll WebView from the tab fragment.
4651 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4653 // Clear SSL certificate preferences for this WebView.
4654 nestedScrollWebView.clearSslPreferences();
4656 // Clear the back/forward history for this WebView.
4657 nestedScrollWebView.clearHistory();
4659 // Destroy the internal state of `mainWebView`.
4660 nestedScrollWebView.destroy();
4664 // Clear the custom headers.
4665 customHeaders.clear();
4667 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4668 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4669 if (clearEverything) {
4671 // Delete the folder.
4672 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
4674 // Wait until the process has finished.
4675 deleteAppWebviewProcess.waitFor();
4676 } catch (Exception exception) {
4677 // Do nothing if an error is thrown.
4681 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4682 if (Build.VERSION.SDK_INT >= 21) {
4683 finishAndRemoveTask();
4688 // Remove the terminated program from RAM. The status code is `0`.
4692 private void setCurrentWebView(int pageNumber) {
4693 // Get a handle for the shared preferences.
4694 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4696 // Get the theme preference.
4697 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4699 // Get handles for the URL views.
4700 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4701 EditText urlEditText = findViewById(R.id.url_edittext);
4702 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4704 // Stop the swipe to refresh indicator if it is running
4705 swipeRefreshLayout.setRefreshing(false);
4707 // Get the WebView tab fragment.
4708 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4710 // Get the fragment view.
4711 View fragmentView = webViewTabFragment.getView();
4713 // Set the current WebView if the fragment view is not null.
4714 if (fragmentView != null) { // The fragment has been populated.
4715 // Store the current WebView.
4716 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4718 // Update the status of swipe to refresh.
4719 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
4720 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
4721 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4722 } else { // Swipe to refresh is disabled.
4723 // Disable the swipe refresh layout.
4724 swipeRefreshLayout.setEnabled(false);
4727 // Get a handle for the cookie manager.
4728 CookieManager cookieManager = CookieManager.getInstance();
4730 // Set the first-party cookie status.
4731 cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
4733 // Update the privacy icons. `true` redraws the icons in the app bar.
4734 updatePrivacyIcons(true);
4736 // Get a handle for the input method manager.
4737 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4739 // Remove the lint warning below that the input method manager might be null.
4740 assert inputMethodManager != null;
4742 // Get the current URL.
4743 String url = currentWebView.getUrl();
4745 // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4746 if (!loadingNewIntent) { // A new intent is not being loaded.
4747 if ((url == null) || url.equals("about:blank")) { // The WebView is blank.
4748 // Display the hint in the URL edit text.
4749 urlEditText.setText("");
4751 // Request focus for the URL text box.
4752 urlEditText.requestFocus();
4754 // Display the keyboard.
4755 inputMethodManager.showSoftInput(urlEditText, 0);
4756 } else { // The WebView has a loaded URL.
4757 // Clear the focus from the URL text box.
4758 urlEditText.clearFocus();
4760 // Hide the soft keyboard.
4761 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4763 // Display the current URL in the URL text box.
4764 urlEditText.setText(url);
4766 // Highlight the URL text.
4769 } else { // A new intent is being loaded.
4770 // Reset the loading new intent tracker.
4771 loadingNewIntent = false;
4774 // Set the background to indicate the domain settings status.
4775 if (currentWebView.getDomainSettingsApplied()) {
4776 // 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.
4778 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4780 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4783 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4785 } else { // The fragment has not been populated. Try again in 100 milliseconds.
4786 // Create a handler to set the current WebView.
4787 Handler setCurrentWebViewHandler = new Handler();
4789 // Create a runnable to set the current WebView.
4790 Runnable setCurrentWebWebRunnable = () -> {
4791 // Set the current WebView.
4792 setCurrentWebView(pageNumber);
4795 // Try setting the current WebView again after 100 milliseconds.
4796 setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
4801 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) {
4802 // Get handles for the activity views.
4803 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4804 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4805 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4806 ActionBar actionBar = getSupportActionBar();
4807 LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
4808 EditText urlEditText = findViewById(R.id.url_edittext);
4809 TabLayout tabLayout = findViewById(R.id.tablayout);
4810 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4812 // Remove the incorrect lint warning below that the action bar might be null.
4813 assert actionBar != null;
4815 // Get a handle for the activity
4816 Activity activity = this;
4818 // Get a handle for the input method manager.
4819 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4821 // Instantiate the blocklist helper.
4822 BlocklistHelper blocklistHelper = new BlocklistHelper();
4824 // Remove the lint warning below that the input method manager might be null.
4825 assert inputMethodManager != null;
4827 // Get a handle for the shared preferences.
4828 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4830 // Initialize the favorite icon.
4831 nestedScrollWebView.initializeFavoriteIcon();
4833 // Set the app bar scrolling.
4834 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4836 // Allow pinch to zoom.
4837 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4839 // Hide zoom controls.
4840 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4842 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4843 if (Build.VERSION.SDK_INT >= 21) {
4844 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4847 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4848 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4850 // Explicitly disable geolocation.
4851 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4853 // Create a double-tap gesture detector to toggle full-screen mode.
4854 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4855 // Override `onDoubleTap()`. All other events are handled using the default settings.
4857 public boolean onDoubleTap(MotionEvent event) {
4858 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4859 // Toggle the full screen browsing mode tracker.
4860 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4862 // Toggle the full screen browsing mode.
4863 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4864 // Store the swipe refresh layout top padding.
4865 swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop();
4867 // Hide the app bar if specified.
4869 // Close the find on page bar if it is visible.
4870 closeFindOnPage(null);
4872 // Hide the tab linear layout.
4873 tabsLinearLayout.setVisibility(View.GONE);
4875 // Hide the action bar.
4878 // Check to see if app bar scrolling is disabled.
4879 if (!scrollAppBar) {
4880 // Remove the padding from the top of the swipe refresh layout.
4881 swipeRefreshLayout.setPadding(0, 0, 0, 0);
4885 // Hide the banner ad in the free flavor.
4886 if (BuildConfig.FLAVOR.contentEquals("free")) {
4887 AdHelper.hideAd(findViewById(R.id.adview));
4890 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4891 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4893 /* Hide the system bars.
4894 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4895 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4896 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4897 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4899 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4900 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4901 } else { // Switch to normal viewing mode.
4902 // Show the tab linear layout.
4903 tabsLinearLayout.setVisibility(View.VISIBLE);
4905 // Show the action bar.
4908 // Check to see if app bar scrolling is disabled.
4909 if (!scrollAppBar) {
4910 // Add the padding from the top of the swipe refresh layout.
4911 swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0);
4914 // Show the banner ad in the free flavor.
4915 if (BuildConfig.FLAVOR.contentEquals("free")) {
4917 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4920 // Remove the `SYSTEM_UI` flags from the root frame layout.
4921 rootFrameLayout.setSystemUiVisibility(0);
4923 // Add the translucent status flag.
4924 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4927 // Consume the double-tap.
4929 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4935 // Pass all touch events on the WebView through the double-tap gesture detector.
4936 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4937 // Call `performClick()` on the view, which is required for accessibility.
4938 view.performClick();
4940 // Send the event to the gesture detector.
4941 return doubleTapGestureDetector.onTouchEvent(event);
4944 // Register the WebView for a context menu. This is used to see link targets and download images.
4945 registerForContextMenu(nestedScrollWebView);
4947 // Allow the downloading of files.
4948 nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4949 // Check if the download should be processed by an external app.
4950 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
4951 // Create a download intent. Not specifying the action type will display the maximum number of options.
4952 Intent downloadIntent = new Intent();
4954 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4955 downloadIntent.setDataAndType(Uri.parse(downloadUrl), "text/html");
4957 // Flag the intent to open in a new task.
4958 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4960 // Show the chooser.
4961 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4962 } else { // Download with Android's download manager.
4963 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4964 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4965 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4967 // Store the variables for future use by `onRequestPermissionsResult()`.
4968 this.downloadUrl = downloadUrl;
4969 downloadContentDisposition = contentDisposition;
4970 downloadContentLength = contentLength;
4972 // Show a dialog if the user has previously denied the permission.
4973 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4974 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4975 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4977 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4978 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4979 } else { // Show the permission request directly.
4980 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4981 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4983 } else { // The storage permission has already been granted.
4984 // Get a handle for the download file alert dialog.
4985 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, contentDisposition, contentLength);
4987 // Show the download file alert dialog.
4988 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4993 // Update the find on page count.
4994 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4995 // Get a handle for `findOnPageCountTextView`.
4996 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4999 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
5000 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
5001 // Set `findOnPageCountTextView` to `0/0`.
5002 findOnPageCountTextView.setText(R.string.zero_of_zero);
5003 } else if (isDoneCounting) { // There are matches.
5004 // `activeMatchOrdinal` is zero-based.
5005 int activeMatch = activeMatchOrdinal + 1;
5007 // Build the match string.
5008 String matchString = activeMatch + "/" + numberOfMatches;
5010 // Set `findOnPageCountTextView`.
5011 findOnPageCountTextView.setText(matchString);
5016 // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. Also reinforce full screen browsing mode.
5017 // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`.
5018 nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
5019 if (nestedScrollWebView.getSwipeToRefresh()) {
5020 // Only enable swipe to refresh if the WebView is scrolled to the top.
5021 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5024 // Reinforce the system UI visibility flags if in full screen browsing mode.
5025 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5026 if (inFullScreenBrowsingMode) {
5027 /* Hide the system bars.
5028 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5029 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5030 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5031 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5033 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5034 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5038 // Set the web chrome client.
5039 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5040 // Update the progress bar when a page is loading.
5042 public void onProgressChanged(WebView view, int progress) {
5043 // Inject the night mode CSS if night mode is enabled.
5044 if (nestedScrollWebView.getNightMode()) { // Night mode is enabled.
5045 // `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
5046 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
5047 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
5048 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
5049 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
5050 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
5051 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
5052 // Initialize a handler to display `mainWebView`.
5053 Handler displayWebViewHandler = new Handler();
5055 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
5056 Runnable displayWebViewRunnable = () -> {
5057 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
5058 if (progressBar.getVisibility() == View.GONE) {
5059 nestedScrollWebView.setVisibility(View.VISIBLE);
5063 // Display the WebView after 500 milliseconds.
5064 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
5066 } else { // Night mode is disabled.
5067 // Display the nested scroll WebView if night mode is disabled.
5068 // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
5069 // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
5070 nestedScrollWebView.setVisibility(View.VISIBLE);
5073 // Update the progress bar.
5074 progressBar.setProgress(progress);
5076 // Set the visibility of the progress bar.
5077 if (progress < 100) {
5078 // Show the progress bar.
5079 progressBar.setVisibility(View.VISIBLE);
5081 // Hide the progress bar.
5082 progressBar.setVisibility(View.GONE);
5084 //Stop the swipe to refresh indicator if it is running
5085 swipeRefreshLayout.setRefreshing(false);
5089 // Set the favorite icon when it changes.
5091 public void onReceivedIcon(WebView view, Bitmap icon) {
5092 // Only update the favorite icon if the website has finished loading.
5093 if (progressBar.getVisibility() == View.GONE) {
5094 // Store the new favorite icon.
5095 nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5097 // Get the current page position.
5098 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5100 // Get the current tab.
5101 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5103 // Check to see if the tab has been populated.
5105 // Get the custom view from the tab.
5106 View tabView = tab.getCustomView();
5108 // Check to see if the custom tab view has been populated.
5109 if (tabView != null) {
5110 // Get the favorite icon image view from the tab.
5111 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5113 // Display the favorite icon in the tab.
5114 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5120 // Save a copy of the title when it changes.
5122 public void onReceivedTitle(WebView view, String title) {
5123 // Get the current page position.
5124 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5126 // Get the current tab.
5127 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5129 // Only populate the title text view if the tab has been fully created.
5131 // Get the custom view from the tab.
5132 View tabView = tab.getCustomView();
5134 // Remove the incorrect warning below that the current tab view might be null.
5135 assert tabView != null;
5137 // Get the title text view from the tab.
5138 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5140 // Set the title according to the URL.
5141 if (title.equals("about:blank")) {
5142 // Set the title to indicate a new tab.
5143 tabTitleTextView.setText(R.string.new_tab);
5145 // Set the title as the tab text.
5146 tabTitleTextView.setText(title);
5151 // Enter full screen video.
5153 public void onShowCustomView(View video, CustomViewCallback callback) {
5154 // Get a handle for the full screen video frame layout.
5155 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5157 // Set the full screen video flag.
5158 displayingFullScreenVideo = true;
5160 // Pause the ad if this is the free flavor.
5161 if (BuildConfig.FLAVOR.contentEquals("free")) {
5162 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
5163 AdHelper.pauseAd(findViewById(R.id.adview));
5166 // Hide the keyboard.
5167 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5169 // Hide the main content relative layout.
5170 mainContentRelativeLayout.setVisibility(View.GONE);
5172 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
5173 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
5175 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
5176 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5178 /* Hide the system bars.
5179 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5180 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5181 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5182 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5184 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5185 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5187 // Disable the sliding drawers.
5188 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5190 // Add the video view to the full screen video frame layout.
5191 fullScreenVideoFrameLayout.addView(video);
5193 // Show the full screen video frame layout.
5194 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5197 // Exit full screen video.
5199 public void onHideCustomView() {
5200 // Get a handle for the full screen video frame layout.
5201 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5203 // Unset the full screen video flag.
5204 displayingFullScreenVideo = false;
5206 // Remove all the views from the full screen video frame layout.
5207 fullScreenVideoFrameLayout.removeAllViews();
5209 // Hide the full screen video frame layout.
5210 fullScreenVideoFrameLayout.setVisibility(View.GONE);
5212 // Enable the sliding drawers.
5213 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
5215 // Show the main content relative layout.
5216 mainContentRelativeLayout.setVisibility(View.VISIBLE);
5218 // Apply the appropriate full screen mode flags.
5219 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
5220 // Hide the app bar if specified.
5222 // Hide the tab linear layout.
5223 tabsLinearLayout.setVisibility(View.GONE);
5225 // Hide the action bar.
5229 // Hide the banner ad in the free flavor.
5230 if (BuildConfig.FLAVOR.contentEquals("free")) {
5231 AdHelper.hideAd(findViewById(R.id.adview));
5234 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
5235 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5237 /* Hide the system bars.
5238 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5239 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5240 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5241 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5243 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5244 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5245 } else { // Switch to normal viewing mode.
5246 // Remove the `SYSTEM_UI` flags from the root frame layout.
5247 rootFrameLayout.setSystemUiVisibility(0);
5249 // Add the translucent status flag.
5250 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5253 // Reload the ad for the free flavor if not in full screen mode.
5254 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
5256 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
5262 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5263 // Show the file chooser if the device is running API >= 21.
5264 if (Build.VERSION.SDK_INT >= 21) {
5265 // Store the file path callback.
5266 fileChooserCallback = filePathCallback;
5268 // Create an intent to open a chooser based ont the file chooser parameters.
5269 Intent fileChooserIntent = fileChooserParams.createIntent();
5271 // Open the file chooser.
5272 startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
5278 nestedScrollWebView.setWebViewClient(new WebViewClient() {
5279 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
5280 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5282 public boolean shouldOverrideUrlLoading(WebView view, String url) {
5283 // Sanitize the url.
5284 url = sanitizeUrl(url);
5286 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
5287 // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed.
5288 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5290 // Check if the user agent has changed.
5291 if (userAgentChanged) {
5292 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
5293 nestedScrollWebView.loadUrl(url, customHeaders);
5295 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5298 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
5301 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
5302 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5303 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5305 // Parse the url and set it as the data for the intent.
5306 emailIntent.setData(Uri.parse(url));
5308 // Open the email program in a new task instead of as part of Privacy Browser.
5309 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5312 startActivity(emailIntent);
5314 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5316 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
5317 // Open the dialer and load the phone number, but wait for the user to place the call.
5318 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5320 // Add the phone number to the intent.
5321 dialIntent.setData(Uri.parse(url));
5323 // Open the dialer in a new task instead of as part of Privacy Browser.
5324 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5327 startActivity(dialIntent);
5329 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5331 } else { // Load a system chooser to select an app that can handle the URL.
5332 // Open an app that can handle the URL.
5333 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5335 // Add the URL to the intent.
5336 genericIntent.setData(Uri.parse(url));
5338 // List all apps that can handle the URL instead of just opening the first one.
5339 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5341 // Open the app in a new task instead of as part of Privacy Browser.
5342 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5344 // Start the app or display a snackbar if no app is available to handle the URL.
5346 startActivity(genericIntent);
5347 } catch (ActivityNotFoundException exception) {
5348 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
5351 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5356 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5358 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
5359 // Check to see if the resource request is for the main URL.
5360 if (url.equals(nestedScrollWebView.getCurrentUrl())) {
5361 // `return null` loads the resource request, which should never be blocked if it is the main URL.
5365 // 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.
5366 while (ultraPrivacy == null) {
5367 // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5368 synchronized (this) {
5370 // Check to see if the blocklists have been populated after 100 ms.
5372 } catch (InterruptedException exception) {
5378 // Sanitize the URL.
5379 url = sanitizeUrl(url);
5381 // Get a handle for the navigation view.
5382 NavigationView navigationView = findViewById(R.id.navigationview);
5384 // Get a handle for the navigation menu.
5385 Menu navigationMenu = navigationView.getMenu();
5387 // Get a handle for the navigation requests menu item. The menu is 0 based.
5388 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
5390 // Create an empty web resource response to be used if the resource request is blocked.
5391 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5393 // Reset the whitelist results tracker.
5394 String[] whitelistResultStringArray = null;
5396 // Initialize the third party request tracker.
5397 boolean isThirdPartyRequest = false;
5399 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5400 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5402 // Store a copy of the current domain for use in later requests.
5403 String currentDomain = currentBaseDomain;
5405 // Nobody is happy when comparing null strings.
5406 if ((currentBaseDomain != null) && (url != null)) {
5407 // Convert the request URL to a URI.
5408 Uri requestUri = Uri.parse(url);
5410 // Get the request host name.
5411 String requestBaseDomain = requestUri.getHost();
5413 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5414 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5415 // Determine the current base domain.
5416 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5417 // Remove the first subdomain.
5418 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5421 // Determine the request base domain.
5422 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5423 // Remove the first subdomain.
5424 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5427 // Update the third party request tracker.
5428 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5432 // Get the current WebView page position.
5433 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5435 // Determine if the WebView is currently displayed.
5436 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5438 // Block third-party requests if enabled.
5439 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
5440 // Add the result to the resource requests.
5441 nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5443 // Increment the blocked requests counters.
5444 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5445 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5447 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5448 if (webViewDisplayed) {
5449 // Updating the UI must be run from the UI thread.
5450 activity.runOnUiThread(() -> {
5451 // Update the menu item titles.
5452 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5454 // Update the options menu if it has been populated.
5455 if (optionsMenu != null) {
5456 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5457 optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5458 getString(R.string.block_all_third_party_requests));
5463 // Return an empty web resource response.
5464 return emptyWebResourceResponse;
5467 // Check UltraList if it is enabled.
5468 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)) {
5469 // Check the URL against UltraList.
5470 String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5472 // Process the UltraList results.
5473 if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraLists's blacklist.
5474 // Add the result to the resource requests.
5475 nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5477 // Increment the blocked requests counters.
5478 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5479 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5481 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5482 if (webViewDisplayed) {
5483 // Updating the UI must be run from the UI thread.
5484 activity.runOnUiThread(() -> {
5485 // Update the menu item titles.
5486 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5488 // Update the options menu if it has been populated.
5489 if (optionsMenu != null) {
5490 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5491 optionsMenu.findItem(R.id.ultralist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5496 // The resource request was blocked. Return an empty web resource response.
5497 return emptyWebResourceResponse;
5498 } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraList's whitelist.
5499 // Add a whitelist entry to the resource requests array.
5500 nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5502 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5507 // Check UltraPrivacy if it is enabled.
5508 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)) {
5509 // Check the URL against UltraPrivacy.
5510 String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5512 // Process the UltraPrivacy results.
5513 if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
5514 // Add the result to the resource requests.
5515 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5516 ultraPrivacyResults[5]});
5518 // Increment the blocked requests counters.
5519 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5520 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5522 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5523 if (webViewDisplayed) {
5524 // Updating the UI must be run from the UI thread.
5525 activity.runOnUiThread(() -> {
5526 // Update the menu item titles.
5527 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5529 // Update the options menu if it has been populated.
5530 if (optionsMenu != null) {
5531 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5532 optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5537 // The resource request was blocked. Return an empty web resource response.
5538 return emptyWebResourceResponse;
5539 } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
5540 // Add a whitelist entry to the resource requests array.
5541 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5542 ultraPrivacyResults[5]});
5544 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5549 // Check EasyList if it is enabled.
5550 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)) {
5551 // Check the URL against EasyList.
5552 String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5554 // Process the EasyList results.
5555 if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
5556 // Add the result to the resource requests.
5557 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5559 // Increment the blocked requests counters.
5560 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5561 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5563 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5564 if (webViewDisplayed) {
5565 // Updating the UI must be run from the UI thread.
5566 activity.runOnUiThread(() -> {
5567 // Update the menu item titles.
5568 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5570 // Update the options menu if it has been populated.
5571 if (optionsMenu != null) {
5572 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5573 optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5578 // The resource request was blocked. Return an empty web resource response.
5579 return emptyWebResourceResponse;
5580 } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
5581 // Update the whitelist result string array tracker.
5582 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5586 // Check EasyPrivacy if it is enabled.
5587 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)) {
5588 // Check the URL against EasyPrivacy.
5589 String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5591 // Process the EasyPrivacy results.
5592 if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
5593 // Add the result to the resource requests.
5594 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5595 easyPrivacyResults[5]});
5597 // Increment the blocked requests counters.
5598 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5599 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5601 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5602 if (webViewDisplayed) {
5603 // Updating the UI must be run from the UI thread.
5604 activity.runOnUiThread(() -> {
5605 // Update the menu item titles.
5606 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5608 // Update the options menu if it has been populated.
5609 if (optionsMenu != null) {
5610 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5611 optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
5616 // The resource request was blocked. Return an empty web resource response.
5617 return emptyWebResourceResponse;
5618 } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
5619 // Update the whitelist result string array tracker.
5620 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5624 // Check Fanboy’s Annoyance List if it is enabled.
5625 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
5626 // Check the URL against Fanboy's Annoyance List.
5627 String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5629 // Process the Fanboy's Annoyance List results.
5630 if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
5631 // Add the result to the resource requests.
5632 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5633 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5635 // Increment the blocked requests counters.
5636 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5637 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5639 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5640 if (webViewDisplayed) {
5641 // Updating the UI must be run from the UI thread.
5642 activity.runOnUiThread(() -> {
5643 // Update the menu item titles.
5644 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5646 // Update the options menu if it has been populated.
5647 if (optionsMenu != null) {
5648 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5649 optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5650 getString(R.string.fanboys_annoyance_list));
5655 // The resource request was blocked. Return an empty web resource response.
5656 return emptyWebResourceResponse;
5657 } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
5658 // Update the whitelist result string array tracker.
5659 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5660 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5662 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5663 // Check the URL against Fanboy's Annoyance List.
5664 String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5666 // Process the Fanboy's Social Blocking List results.
5667 if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5668 // Add the result to the resource requests.
5669 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5670 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5672 // Increment the blocked requests counters.
5673 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5674 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5676 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5677 if (webViewDisplayed) {
5678 // Updating the UI must be run from the UI thread.
5679 activity.runOnUiThread(() -> {
5680 // Update the menu item titles.
5681 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5683 // Update the options menu if it has been populated.
5684 if (optionsMenu != null) {
5685 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5686 optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5687 getString(R.string.fanboys_social_blocking_list));
5692 // The resource request was blocked. Return an empty web resource response.
5693 return emptyWebResourceResponse;
5694 } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5695 // Update the whitelist result string array tracker.
5696 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5697 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5701 // Add the request to the log because it hasn't been processed by any of the previous checks.
5702 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5703 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5704 } else { // The request didn't match any blocklist entry. Log it as a default request.
5705 nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
5708 // The resource request has not been blocked. `return null` loads the requested resource.
5712 // Handle HTTP authentication requests.
5714 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5715 // Store the handler.
5716 nestedScrollWebView.setHttpAuthHandler(handler);
5718 // Instantiate an HTTP authentication dialog.
5719 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5721 // Show the HTTP authentication dialog.
5722 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5726 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5727 // Get the preferences.
5728 boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
5729 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5731 // Get a handler for the app bar layout.
5732 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
5734 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.
5736 // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5737 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5739 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5740 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
5742 // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated.
5743 int appBarHeight = appBarLayout.getHeight();
5745 // The swipe refresh layout must be manually moved below the app bar layout.
5746 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5748 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5749 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5752 // Reset the list of resource requests.
5753 nestedScrollWebView.clearResourceRequests();
5755 // Reset the requests counters.
5756 nestedScrollWebView.resetRequestsCounters();
5758 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5759 if (nestedScrollWebView.getNightMode()) {
5760 nestedScrollWebView.setVisibility(View.INVISIBLE);
5762 nestedScrollWebView.setVisibility(View.VISIBLE);
5765 // Hide the keyboard.
5766 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5768 // Check to see if Privacy Browser is waiting on Orbot.
5769 if (!waitingForOrbot) { // Process the URL.
5770 // Get the current page position.
5771 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5773 // Update the URL text bar if the page is currently selected.
5774 if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5775 // Clear the focus from the URL edit text.
5776 urlEditText.clearFocus();
5778 // Display the formatted URL text.
5779 urlEditText.setText(url);
5781 // Apply text highlighting to `urlTextBox`.
5785 // Reset the list of host IP addresses.
5786 nestedScrollWebView.clearCurrentIpAddresses();
5788 // Get a URI for the current URL.
5789 Uri currentUri = Uri.parse(url);
5791 // Get the IP addresses for the host.
5792 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5794 // Apply any custom domain settings if the URL was loaded by navigating history.
5795 if (nestedScrollWebView.getNavigatingHistory()) {
5796 // Reset navigating history.
5797 nestedScrollWebView.setNavigatingHistory(false);
5799 // Apply the domain settings.
5800 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5802 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5803 if (userAgentChanged) {
5808 // Replace Refresh with Stop if the options menu has been created. (The first WebView typically begins loading before the menu items are instantiated.)
5809 if (optionsMenu != null) {
5810 // Get a handle for the refresh menu item.
5811 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5814 refreshMenuItem.setTitle(R.string.stop);
5816 // Get the app bar and theme preferences.
5817 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5819 // If the icon is displayed in the AppBar, set it according to the theme.
5820 if (displayAdditionalAppBarIcons) {
5822 refreshMenuItem.setIcon(R.drawable.close_dark);
5824 refreshMenuItem.setIcon(R.drawable.close_light);
5832 public void onPageFinished(WebView view, String url) {
5833 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5834 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
5835 CookieManager.getInstance().flush();
5838 // Update the Refresh menu item if the options menu has been created.
5839 if (optionsMenu != null) {
5840 // Get a handle for the refresh menu item.
5841 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5843 // Reset the Refresh title.
5844 refreshMenuItem.setTitle(R.string.refresh);
5846 // Get the app bar and theme preferences.
5847 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5848 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5850 // If the icon is displayed in the AppBar, reset it according to the theme.
5851 if (displayAdditionalAppBarIcons) {
5853 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5855 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5860 // Clear the cache and history if Incognito Mode is enabled.
5861 if (incognitoModeEnabled) {
5862 // Clear the cache. `true` includes disk files.
5863 nestedScrollWebView.clearCache(true);
5865 // Clear the back/forward history.
5866 nestedScrollWebView.clearHistory();
5868 // Manually delete cache folders.
5870 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5871 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5872 String privateDataDirectoryString = getApplicationInfo().dataDir;
5874 // Delete the main cache directory.
5875 Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5877 // Delete the secondary `Service Worker` cache directory.
5878 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5879 Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5880 } catch (IOException e) {
5881 // Do nothing if an error is thrown.
5885 // Update the URL text box and apply domain settings if not waiting on Orbot.
5886 if (!waitingForOrbot) {
5887 // Get the current page position.
5888 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5890 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5891 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5892 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5893 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5896 // 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.
5897 String currentUrl = nestedScrollWebView.getUrl();
5899 // Get the current tab.
5900 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
5902 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5903 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5904 // Probably some sort of race condition when Privacy Browser is being resumed.
5905 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5906 // Check to see if the URL is `about:blank`.
5907 if (currentUrl.equals("about:blank")) { // The WebView is blank.
5908 // Display the hint in the URL edit text.
5909 urlEditText.setText("");
5911 // Request focus for the URL text box.
5912 urlEditText.requestFocus();
5914 // Display the keyboard.
5915 inputMethodManager.showSoftInput(urlEditText, 0);
5917 // Apply the domain settings. This clears any settings from the previous domain.
5918 applyDomainSettings(nestedScrollWebView, "", true, false);
5920 // Only populate the title text view if the tab has been fully created.
5922 // Get the custom view from the tab.
5923 View tabView = tab.getCustomView();
5925 // Remove the incorrect warning below that the current tab view might be null.
5926 assert tabView != null;
5928 // Get the title text view from the tab.
5929 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5931 // Set the title as the tab text.
5932 tabTitleTextView.setText(R.string.new_tab);
5934 } else { // The WebView has loaded a webpage.
5935 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5936 urlEditText.setText(currentUrl);
5938 // Apply text highlighting to the URL.
5941 // Only populate the title text view if the tab has been fully created.
5943 // Get the custom view from the tab.
5944 View tabView = tab.getCustomView();
5946 // Remove the incorrect warning below that the current tab view might be null.
5947 assert tabView != null;
5949 // Get the title text view from the tab.
5950 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5952 // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5953 tabTitleTextView.setText(nestedScrollWebView.getTitle());
5960 // Handle SSL Certificate errors.
5962 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5963 // Get the current website SSL certificate.
5964 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5966 // Extract the individual pieces of information from the current website SSL certificate.
5967 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5968 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5969 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5970 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5971 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5972 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5973 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5974 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5976 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5977 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5978 // Get the pinned SSL certificate.
5979 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5981 // Extract the arrays from the array list.
5982 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5983 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5985 // Check if the current SSL certificate matches the pinned certificate.
5986 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5987 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5988 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5989 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5991 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5994 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5995 // Store the SSL error handler.
5996 nestedScrollWebView.setSslErrorHandler(handler);
5998 // Instantiate an SSL certificate error alert dialog.
5999 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
6001 // Show the SSL certificate error dialog.
6002 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
6007 // Check to see if this is the first page.
6008 if (pageNumber == 0) {
6009 // Set this nested scroll WebView as the current WebView.
6010 currentWebView = nestedScrollWebView;
6012 // Apply the app settings from the shared preferences.
6015 // Load the website if not waiting for Orbot to connect.
6016 if (!waitingForOrbot) {
6017 // Get the intent that started the app.
6018 Intent launchingIntent = getIntent();
6020 // Get the information from the intent.
6021 String launchingIntentAction = launchingIntent.getAction();
6022 Uri launchingIntentUriData = launchingIntent.getData();
6024 // If the intent action is a web search, perform the search.
6025 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
6026 // Create an encoded URL string.
6027 String encodedUrlString;
6029 // Sanitize the search input and convert it to a search.
6031 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6032 } catch (UnsupportedEncodingException exception) {
6033 encodedUrlString = "";
6036 // Load the completed search URL.
6037 loadUrl(searchURL + encodedUrlString);
6038 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
6039 // Load the URL from the intent.
6040 loadUrl(launchingIntentUriData.toString());
6041 } else { // The is no URL in the intent.
6042 // Select the homepage based on the proxy through Orbot status.
6043 if (proxyThroughOrbot) {
6044 // Load the Tor homepage.
6045 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
6047 // Load the normal homepage.
6048 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
6052 } else { // This is not the first tab.
6053 // Apply the domain settings.
6054 applyDomainSettings(nestedScrollWebView, url, false, false);
6057 nestedScrollWebView.loadUrl(url, customHeaders);
6059 // Set the focus and display the keyboard if the URL is blank.
6060 if (url.equals("")) {
6061 // Request focus for the URL text box.
6062 urlEditText.requestFocus();
6064 // Create a display keyboard handler.
6065 Handler displayKeyboardHandler = new Handler();
6067 // Create a display keyboard runnable.
6068 Runnable displayKeyboardRunnable = () -> {
6069 // Display the keyboard.
6070 inputMethodManager.showSoftInput(urlEditText, 0);
6073 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6074 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);