2 * Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
4 * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
6 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
8 * Privacy Browser is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
13 * Privacy Browser is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
22 package com.stoutner.privacybrowser.activities;
24 import android.Manifest;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.DownloadManager;
28 import android.app.SearchManager;
29 import android.content.ActivityNotFoundException;
30 import android.content.BroadcastReceiver;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.res.Configuration;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Typeface;
43 import android.graphics.drawable.BitmapDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.net.Uri;
46 import android.net.http.SslCertificate;
47 import android.net.http.SslError;
48 import android.os.Build;
49 import android.os.Bundle;
50 import android.os.Environment;
51 import android.os.Handler;
52 import android.preference.PreferenceManager;
53 import android.print.PrintDocumentAdapter;
54 import android.print.PrintManager;
55 import android.text.Editable;
56 import android.text.Spanned;
57 import android.text.TextWatcher;
58 import android.text.style.ForegroundColorSpan;
59 import android.util.Patterns;
60 import android.view.ContextMenu;
61 import android.view.GestureDetector;
62 import android.view.KeyEvent;
63 import android.view.Menu;
64 import android.view.MenuItem;
65 import android.view.MotionEvent;
66 import android.view.View;
67 import android.view.ViewGroup;
68 import android.view.WindowManager;
69 import android.view.inputmethod.InputMethodManager;
70 import android.webkit.CookieManager;
71 import android.webkit.HttpAuthHandler;
72 import android.webkit.SslErrorHandler;
73 import android.webkit.ValueCallback;
74 import android.webkit.WebBackForwardList;
75 import android.webkit.WebChromeClient;
76 import android.webkit.WebResourceResponse;
77 import android.webkit.WebSettings;
78 import android.webkit.WebStorage;
79 import android.webkit.WebView;
80 import android.webkit.WebViewClient;
81 import android.webkit.WebViewDatabase;
82 import android.widget.ArrayAdapter;
83 import android.widget.CursorAdapter;
84 import android.widget.EditText;
85 import android.widget.FrameLayout;
86 import android.widget.ImageView;
87 import android.widget.LinearLayout;
88 import android.widget.ListView;
89 import android.widget.ProgressBar;
90 import android.widget.RadioButton;
91 import android.widget.RelativeLayout;
92 import android.widget.TextView;
94 import androidx.annotation.NonNull;
95 import androidx.appcompat.app.ActionBar;
96 import androidx.appcompat.app.ActionBarDrawerToggle;
97 import androidx.appcompat.app.AppCompatActivity;
98 import androidx.appcompat.widget.Toolbar;
99 import androidx.core.app.ActivityCompat;
100 import androidx.core.content.ContextCompat;
101 import androidx.core.view.GravityCompat;
102 import androidx.drawerlayout.widget.DrawerLayout;
103 import androidx.fragment.app.DialogFragment;
104 import androidx.fragment.app.FragmentManager;
105 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
106 import androidx.viewpager.widget.ViewPager;
108 import com.google.android.material.floatingactionbutton.FloatingActionButton;
109 import com.google.android.material.navigation.NavigationView;
110 import com.google.android.material.snackbar.Snackbar;
111 import com.google.android.material.tabs.TabLayout;
113 import com.stoutner.privacybrowser.BuildConfig;
114 import com.stoutner.privacybrowser.R;
115 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
116 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
117 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
118 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
121 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
124 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
125 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
126 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
127 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
128 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
129 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
130 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
131 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
132 import com.stoutner.privacybrowser.helpers.AdHelper;
133 import com.stoutner.privacybrowser.helpers.BlockListHelper;
134 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
135 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
136 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
138 import com.stoutner.privacybrowser.views.NestedScrollWebView;
140 import java.io.ByteArrayInputStream;
141 import java.io.ByteArrayOutputStream;
143 import java.io.IOException;
144 import java.io.UnsupportedEncodingException;
145 import java.net.MalformedURLException;
147 import java.net.URLDecoder;
148 import java.net.URLEncoder;
149 import java.util.ArrayList;
150 import java.util.Date;
151 import java.util.HashMap;
152 import java.util.HashSet;
153 import java.util.List;
154 import java.util.Map;
155 import java.util.Set;
157 // TODO. Store up reloads for tabs that are not visible.
158 // TODO. New tabs are white in dark mode.
159 // TODO. Hide the tabs in full screen mode.
160 // TODO. Find on page.
161 // TODO. Use TabLayout.setScrollPosition to scroll to new tabs.
163 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
164 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
165 DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
166 EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener, PinnedMismatchDialog.PinnedMismatchListener,
167 UrlHistoryDialog.UrlHistoryListener {
169 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
170 public static String orbotStatus;
172 // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`.
173 public static WebViewPagerAdapter webViewPagerAdapter;
175 // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`. They are used in `onRestart()`.
176 public static boolean loadUrlOnRestart;
177 public static String urlToLoadOnRestart;
179 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
180 public static boolean restartFromBookmarksActivity;
182 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
183 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
184 public static String currentBookmarksFolder;
186 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
187 public final static int UNRECOGNIZED_USER_AGENT = -1;
188 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
189 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
190 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
191 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
192 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
196 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
197 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
198 private NestedScrollWebView currentWebView;
200 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
201 private final Map<String, String> customHeaders = new HashMap<>();
203 // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
204 private String searchURL;
206 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
207 private Menu optionsMenu;
209 // The blocklists are populated in `onCreate()` and accessed from `initializeWebView()`.
210 private ArrayList<List<String[]>> easyList;
211 private ArrayList<List<String[]>> easyPrivacy;
212 private ArrayList<List<String[]>> fanboysAnnoyanceList;
213 private ArrayList<List<String[]>> fanboysSocialList;
214 private ArrayList<List<String[]>> ultraPrivacy;
216 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
217 private String webViewDefaultUserAgent;
219 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
220 private boolean proxyThroughOrbot;
222 // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
223 private boolean incognitoModeEnabled;
225 // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
226 private boolean fullScreenBrowsingModeEnabled;
228 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
229 private boolean inFullScreenBrowsingMode;
231 // Hide app bar is used in `applyAppSettings()` and `initializeWebView()`.
232 private boolean hideAppBar;
234 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
235 private boolean reapplyDomainSettingsOnRestart;
237 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
238 private boolean reapplyAppSettingsOnRestart;
240 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
241 private boolean displayingFullScreenVideo;
243 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
244 private BroadcastReceiver orbotStatusBroadcastReceiver;
246 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
247 private boolean waitingForOrbot;
249 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
250 private ActionBarDrawerToggle actionBarDrawerToggle;
252 // The color spans are used in `onCreate()` and `highlightUrlText()`.
253 private ForegroundColorSpan redColorSpan;
254 private ForegroundColorSpan initialGrayColorSpan;
255 private ForegroundColorSpan finalGrayColorSpan;
257 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
258 private int drawerHeaderPaddingLeftAndRight;
259 private int drawerHeaderPaddingTop;
260 private int drawerHeaderPaddingBottom;
262 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
263 // and `loadBookmarksFolder()`.
264 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
266 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
267 private Cursor bookmarksCursor;
269 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
270 private CursorAdapter bookmarksCursorAdapter;
272 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
273 private String oldFolderNameString;
275 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
276 private ValueCallback<Uri[]> fileChooserCallback;
278 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
279 private String downloadUrl;
280 private String downloadContentDisposition;
281 private long downloadContentLength;
283 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
284 private String downloadImageUrl;
286 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
287 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
288 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
291 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
292 @SuppressLint("ClickableViewAccessibility")
293 protected void onCreate(Bundle savedInstanceState) {
294 // Get a handle for the shared preferences.
295 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
297 // Get the theme and screenshot preferences.
298 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
299 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
301 // Disable screenshots if not allowed.
302 if (!allowScreenshots) {
303 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
306 // Set the activity theme.
308 setTheme(R.style.PrivacyBrowserDark);
310 setTheme(R.style.PrivacyBrowserLight);
313 // Run the default commands.
314 super.onCreate(savedInstanceState);
316 // Set the content view.
317 setContentView(R.layout.main_framelayout);
319 // Get a handle for the input method.
320 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
322 // Remove the lint warning below that the input method manager might be null.
323 assert inputMethodManager != null;
325 // Get a handle for the toolbar.
326 Toolbar toolbar = findViewById(R.id.toolbar);
328 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
329 setSupportActionBar(toolbar);
331 // Get a handle for the action bar.
332 ActionBar actionBar = getSupportActionBar();
334 // This is needed to get rid of the Android Studio warning that the action bar might be null.
335 assert actionBar != null;
337 // Add the custom layout, which shows the URL text bar.
338 actionBar.setCustomView(R.layout.url_app_bar);
339 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
341 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
342 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
343 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
344 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
346 // Get handles for the URL views.
347 EditText urlEditText = findViewById(R.id.url_edittext);
349 // Remove the formatting from `urlTextBar` when the user is editing the text.
350 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
351 if (hasFocus) { // The user is editing the URL text box.
352 // Remove the highlighting.
353 urlEditText.getText().removeSpan(redColorSpan);
354 urlEditText.getText().removeSpan(initialGrayColorSpan);
355 urlEditText.getText().removeSpan(finalGrayColorSpan);
356 } else { // The user has stopped editing the URL text box.
357 // Move to the beginning of the string.
358 urlEditText.setSelection(0);
360 // Reapply the highlighting.
365 // Set the go button on the keyboard to load the URL in `urlTextBox`.
366 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
367 // If the event is a key-down event on the `enter` button, load the URL.
368 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
369 // Load the URL into the mainWebView and consume the event.
370 loadUrlFromTextBox();
372 // If the enter key was pressed, consume the event.
375 // If any other key was pressed, do not consume the event.
380 // Initialize the Orbot status and the waiting for Orbot trackers.
381 orbotStatus = "unknown";
382 waitingForOrbot = false;
384 // Create an Orbot status `BroadcastReceiver`.
385 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
387 public void onReceive(Context context, Intent intent) {
388 // Store the content of the status message in `orbotStatus`.
389 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
391 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
392 if (orbotStatus.equals("ON") && waitingForOrbot) {
393 // Reset the waiting for Orbot status.
394 waitingForOrbot = false;
396 // Get the intent that started the app.
397 Intent launchingIntent = getIntent();
399 // Get the information from the intent.
400 String launchingIntentAction = launchingIntent.getAction();
401 Uri launchingIntentUriData = launchingIntent.getData();
403 // If the intent action is a web search, perform the search.
404 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
405 // Create an encoded URL string.
406 String encodedUrlString;
408 // Sanitize the search input and convert it to a search.
410 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
411 } catch (UnsupportedEncodingException exception) {
412 encodedUrlString = "";
415 // Load the completed search URL.
416 loadUrl(searchURL + encodedUrlString);
417 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
418 // Load the URL from the intent.
419 loadUrl(launchingIntentUriData.toString());
420 } else { // The is no URL in the intent.
421 // Select the homepage based on the proxy through Orbot status.
422 if (proxyThroughOrbot) {
423 // Load the Tor homepage.
424 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
426 // Load the normal homepage.
427 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
434 // Register `orbotStatusBroadcastReceiver` on `this` context.
435 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
437 // Instantiate the blocklist helper.
438 BlockListHelper blockListHelper = new BlockListHelper();
440 // Parse the block lists.
441 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
442 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
443 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
444 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
445 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
447 // Get handles for views that need to be modified.
448 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
449 NavigationView navigationView = findViewById(R.id.navigationview);
450 TabLayout tabLayout = findViewById(R.id.tablayout);
451 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
452 ViewPager webViewPager = findViewById(R.id.webviewpager);
453 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
454 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
455 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
456 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
457 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
459 // Listen for touches on the navigation menu.
460 navigationView.setNavigationItemSelectedListener(this);
462 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
463 Menu navigationMenu = navigationView.getMenu();
464 MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
465 MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
466 MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
467 MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
468 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
470 // Initialize the web view pager adapter.
471 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
473 // Set the pager adapter on the web view pager.
474 webViewPager.setAdapter(webViewPagerAdapter);
476 // Store up to 100 tabs in memory.
477 webViewPager.setOffscreenPageLimit(100);
479 // Update the web view pager every time a tab is modified.
480 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
482 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
487 public void onPageSelected(int position) {
488 // Set the current WebView.
489 setCurrentWebView(position);
491 // 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.
492 if (tabLayout.getSelectedTabPosition() != position) {
493 // Get a handle for the corresponding tab.
494 TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
496 // Assert that the corresponding tab is not null.
497 assert correspondingTab != null;
499 // Select the corresponding tab.
500 correspondingTab.select();
505 public void onPageScrollStateChanged(int state) {
510 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
511 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
513 public void onTabSelected(TabLayout.Tab tab) {
514 // Select the same page in the view pager.
515 webViewPager.setCurrentItem(tab.getPosition());
519 public void onTabUnselected(TabLayout.Tab tab) {
524 public void onTabReselected(TabLayout.Tab tab) {
525 // Instantiate the View SSL Certificate dialog.
526 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
528 // Display the View SSL Certificate dialog.
529 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
533 // Add the first tab.
536 // 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.
537 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
539 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
540 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
541 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
542 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
544 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
545 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
546 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
547 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
550 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
551 launchBookmarksActivityFab.setOnClickListener(v -> {
552 // Get a copy of the favorite icon bitmap.
553 Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
555 // Create a favorite icon byte array output stream.
556 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
558 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
559 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
561 // Convert the favorite icon byte array stream to a byte array.
562 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
564 // Create an intent to launch the bookmarks activity.
565 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
567 // Add the extra information to the intent.
568 bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
569 bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
570 bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
571 bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
574 startActivity(bookmarksIntent);
577 // Set the create new bookmark folder FAB to display an alert dialog.
578 createBookmarkFolderFab.setOnClickListener(v -> {
579 // Create a create bookmark folder dialog.
580 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
582 // Show the create bookmark folder dialog.
583 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
586 // Set the create new bookmark FAB to display an alert dialog.
587 createBookmarkFab.setOnClickListener(view -> {
588 // Instantiate the create bookmark dialog.
589 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
591 // Display the create bookmark dialog.
592 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
595 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
596 findOnPageEditText.addTextChangedListener(new TextWatcher() {
598 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
603 public void onTextChanged(CharSequence s, int start, int before, int count) {
608 public void afterTextChanged(Editable s) {
609 // Search for the text in `mainWebView`.
610 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
614 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
615 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
616 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
617 // Hide the soft keyboard.
618 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
620 // Consume the event.
622 } else { // A different key was pressed.
623 // Do not consume the event.
628 // Implement swipe to refresh.
629 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
631 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
632 swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
634 // Set the swipe to refresh color according to the theme.
636 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
637 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
639 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
642 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
643 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
644 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
646 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
647 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
649 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
650 currentBookmarksFolder = "";
652 // Load the home folder, which is `""` in the database.
653 loadBookmarksFolder();
655 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
656 // Convert the id from long to int to match the format of the bookmarks database.
657 int databaseID = (int) id;
659 // Get the bookmark cursor for this ID and move it to the first row.
660 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
661 bookmarkCursor.moveToFirst();
663 // Act upon the bookmark according to the type.
664 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
665 // Store the new folder name in `currentBookmarksFolder`.
666 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
668 // Load the new folder.
669 loadBookmarksFolder();
670 } else { // The selected bookmark is not a folder.
671 // Load the bookmark URL.
672 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
674 // Close the bookmarks drawer.
675 drawerLayout.closeDrawer(GravityCompat.END);
678 // Close the `Cursor`.
679 bookmarkCursor.close();
682 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
683 // Convert the database ID from `long` to `int`.
684 int databaseId = (int) id;
686 // Find out if the selected bookmark is a folder.
687 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
690 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
691 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
693 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
694 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
695 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
697 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
698 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
699 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
702 // Consume the event.
706 // Get the status bar pixel size.
707 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
708 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
710 // Get the resource density.
711 float screenDensity = getResources().getDisplayMetrics().density;
713 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
714 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
715 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
716 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
718 // The drawer listener is used to update the navigation menu.`
719 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
721 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
725 public void onDrawerOpened(@NonNull View drawerView) {
729 public void onDrawerClosed(@NonNull View drawerView) {
733 public void onDrawerStateChanged(int newState) {
734 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
735 // Get handles for the drawer headers.
736 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
737 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
739 // 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.
740 if (navigationHeaderTextView != null) {
741 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
744 // 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.
745 if (bookmarksHeaderTextView != null) {
746 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
749 // Update the navigation menu items.
750 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
751 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
752 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
753 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
754 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
756 // Hide the keyboard (if displayed).
757 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
759 // 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.
760 urlEditText.clearFocus();
761 currentWebView.clearFocus();
766 // Create the hamburger icon at the start of the AppBar.
767 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
769 // 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).
770 customHeaders.put("X-Requested-With", "");
772 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
773 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
775 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
776 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
778 // Get a handle for the WebView.
779 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
781 // Store the default user agent.
782 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
784 // Destroy the bare WebView.
785 bareWebView.destroy();
789 protected void onNewIntent(Intent intent) {
790 // Get the information from the intent.
791 String intentAction = intent.getAction();
792 Uri intentUriData = intent.getData();
794 // Only process the URI if it contains data. If the user pressed the desktop icon after the app was already running the URI will be null.
795 if (intentUriData != null) {
796 // Sets the new intent as the activity intent, which replaces the one that originally started the app.
802 // Create a URL string.
805 // If the intent action is a web search, perform the search.
806 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
807 // Create an encoded URL string.
808 String encodedUrlString;
810 // Sanitize the search input and convert it to a search.
812 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
813 } catch (UnsupportedEncodingException exception) {
814 encodedUrlString = "";
817 // Add the base search URL.
818 url = searchURL + encodedUrlString;
819 } else { // The intent should contain a URL.
820 // Set the intent data as the URL.
821 url = intentUriData.toString();
827 // Get a handle for the drawer layout.
828 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
830 // Close the navigation drawer if it is open.
831 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
832 drawerLayout.closeDrawer(GravityCompat.START);
835 // Close the bookmarks drawer if it is open.
836 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
837 drawerLayout.closeDrawer(GravityCompat.END);
840 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
841 currentWebView.requestFocus();
846 public void onRestart() {
847 // Run the default commands.
850 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
851 if (proxyThroughOrbot) {
852 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
853 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
855 // Send the intent to the Orbot package.
856 orbotIntent.setPackage("org.torproject.android");
859 sendBroadcast(orbotIntent);
862 // Apply the app settings if returning from the Settings activity.
863 if (reapplyAppSettingsOnRestart) {
864 // Reset the reapply app settings on restart tracker.
865 reapplyAppSettingsOnRestart = false;
867 // Apply the app settings.
871 // Apply the domain settings if returning from the settings or domains activity.
872 if (reapplyDomainSettingsOnRestart) {
873 // Reset the reapply domain settings on restart tracker.
874 reapplyDomainSettingsOnRestart = false;
876 // Reapply the domain settings for each tab.
877 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
878 // Get the WebView tab fragment.
879 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
881 // Get the fragment view.
882 View fragmentView = webViewTabFragment.getView();
884 // Only reload the WebViews if they exist.
885 if (fragmentView != null) {
886 // Get the nested scroll WebView from the tab fragment.
887 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
889 // Reset the current domain name so the domain settings will be reapplied.
890 nestedScrollWebView.resetCurrentDomainName();
892 // Reapply the domain settings.
893 applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
898 // Load the URL on restart (used when loading a bookmark).
899 if (loadUrlOnRestart) {
900 // Load the specified URL.
901 loadUrl(urlToLoadOnRestart);
903 // Reset the load on restart tracker.
904 loadUrlOnRestart = false;
907 // Update the bookmarks drawer if returning from the Bookmarks activity.
908 if (restartFromBookmarksActivity) {
909 // Get a handle for the drawer layout.
910 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
912 // Close the bookmarks drawer.
913 drawerLayout.closeDrawer(GravityCompat.END);
915 // Reload the bookmarks drawer.
916 loadBookmarksFolder();
918 // Reset `restartFromBookmarksActivity`.
919 restartFromBookmarksActivity = false;
922 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
923 updatePrivacyIcons(true);
926 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
928 public void onResume() {
929 // Run the default commands.
932 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
933 // Get the WebView tab fragment.
934 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
936 // Get the fragment view.
937 View fragmentView = webViewTabFragment.getView();
939 // Only resume the WebViews if they exist (they won't when the app is first created).
940 if (fragmentView != null) {
941 // Get the nested scroll WebView from the tab fragment.
942 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
944 // Resume the nested scroll WebView JavaScript timers.
945 nestedScrollWebView.resumeTimers();
947 // Resume the nested scroll WebView.
948 nestedScrollWebView.onResume();
952 // Display a message to the user if waiting for Orbot.
953 if (waitingForOrbot && !orbotStatus.equals("ON")) {
954 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
955 currentWebView.getSettings().setUseWideViewPort(false);
957 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
958 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
961 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
962 // Get a handle for the root frame layouts.
963 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
965 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
966 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
968 /* Hide the system bars.
969 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
970 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
971 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
972 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
974 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
975 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
976 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
978 AdHelper.resumeAd(findViewById(R.id.adview));
983 public void onPause() {
984 // Run the default commands.
987 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
988 // Get the WebView tab fragment.
989 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
991 // Get the fragment view.
992 View fragmentView = webViewTabFragment.getView();
994 // Only pause the WebViews if they exist (they won't when the app is first created).
995 if (fragmentView != null) {
996 // Get the nested scroll WebView from the tab fragment.
997 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
999 // Pause the nested scroll WebView.
1000 nestedScrollWebView.onPause();
1002 // Pause the nested scroll WebView JavaScript timers.
1003 nestedScrollWebView.pauseTimers();
1007 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1008 if (BuildConfig.FLAVOR.contentEquals("free")) {
1010 AdHelper.pauseAd(findViewById(R.id.adview));
1015 public void onDestroy() {
1016 // Unregister the Orbot status broadcast receiver.
1017 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1019 // Close the bookmarks cursor and database.
1020 bookmarksCursor.close();
1021 bookmarksDatabaseHelper.close();
1023 // Run the default commands.
1028 public boolean onCreateOptionsMenu(Menu menu) {
1029 // Inflate the menu. This adds items to the action bar if it is present.
1030 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1032 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
1035 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1036 updatePrivacyIcons(false);
1038 // Get handles for the menu items.
1039 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1040 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1041 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1042 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1043 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1044 MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
1045 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1047 // Only display third-party cookies if API >= 21
1048 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1050 // Only display the form data menu items if the API < 26.
1051 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1052 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1054 // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
1055 clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
1057 // Only show Ad Consent if this is the free flavor.
1058 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1060 // Get the shared preference values.
1061 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1063 // Get the dark theme and app bar preferences..
1064 boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1065 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
1067 // 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.
1068 if (displayAdditionalAppBarIcons) {
1069 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1070 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1071 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1072 } else { //Do not display the additional icons.
1073 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1074 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1075 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1078 // Replace Refresh with Stop if a URL is already loading.
1079 if (currentWebView != null && currentWebView.getProgress() != 100) {
1081 refreshMenuItem.setTitle(R.string.stop);
1083 // If the icon is displayed in the AppBar, set it according to the theme.
1084 if (displayAdditionalAppBarIcons) {
1086 refreshMenuItem.setIcon(R.drawable.close_dark);
1088 refreshMenuItem.setIcon(R.drawable.close_light);
1097 public boolean onPrepareOptionsMenu(Menu menu) {
1098 // Get handles for the menu items.
1099 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1100 MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1101 MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1102 MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1103 MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1104 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1105 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1106 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1107 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1108 MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
1109 MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
1110 MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1111 MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1112 MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1113 MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1114 MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1115 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1116 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1117 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1118 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1119 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1121 // Get a handle for the cookie manager.
1122 CookieManager cookieManager = CookieManager.getInstance();
1124 // Initialize the current user agent string and the font size.
1125 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1128 // 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.
1129 if (currentWebView != null) {
1130 // Set the add or edit domain text.
1131 if (currentWebView.getDomainSettingsApplied()) {
1132 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1134 addOrEditDomain.setTitle(R.string.add_domain_settings);
1137 // Get the current user agent from the WebView.
1138 currentUserAgent = currentWebView.getSettings().getUserAgentString();
1140 // Get the current font size from the
1141 fontSize = currentWebView.getSettings().getTextZoom();
1143 // Set the status of the menu item checkboxes.
1144 domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1145 saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26.
1146 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1147 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1148 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1149 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1150 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1151 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1152 swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1153 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1154 nightModeMenuItem.setChecked(currentWebView.getNightMode());
1156 // Initialize the display names for the blocklists with the number of blocked requests.
1157 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1158 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1159 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1160 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1161 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1162 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1163 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1165 // Only modify third-party cookies if the API >= 21.
1166 if (Build.VERSION.SDK_INT >= 21) {
1167 // Set the status of the third-party cookies checkbox.
1168 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1170 // Enable third-party cookies if first-party cookies are enabled.
1171 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
1174 // Enable DOM Storage if JavaScript is enabled.
1175 domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1178 // Set the status of the menu item checkboxes.
1179 firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1180 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1182 // Enable Clear Cookies if there are any.
1183 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1185 // 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`.
1186 String privateDataDirectoryString = getApplicationInfo().dataDir;
1188 // Get a count of the number of files in the Local Storage directory.
1189 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1190 int localStorageDirectoryNumberOfFiles = 0;
1191 if (localStorageDirectory.exists()) {
1192 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1195 // Get a count of the number of files in the IndexedDB directory.
1196 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1197 int indexedDBDirectoryNumberOfFiles = 0;
1198 if (indexedDBDirectory.exists()) {
1199 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1202 // Enable Clear DOM Storage if there is any.
1203 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1205 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1206 if (Build.VERSION.SDK_INT < 26) {
1207 // Get the WebView database.
1208 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1210 // Enable the clear form data menu item if there is anything to clear.
1211 clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1214 // Enable Clear Data if any of the submenu items are enabled.
1215 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1217 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1218 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1220 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1221 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1222 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1223 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1224 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1225 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1226 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1227 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1228 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1229 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1230 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1231 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1232 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1233 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1234 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1235 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1236 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1237 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1238 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1239 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1240 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1241 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1242 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1243 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1244 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1245 } else { // Custom user agent.
1246 menu.findItem(R.id.user_agent_custom).setChecked(true);
1249 // Instantiate the font size title and the selected font size menu item.
1250 String fontSizeTitle;
1251 MenuItem selectedFontSizeMenuItem;
1253 // Prepare the font size title and current size menu item.
1256 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1257 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1261 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1262 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1266 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1267 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1271 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1272 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1276 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1277 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1281 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1282 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1286 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1287 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1291 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1292 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1296 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1297 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1301 // Set the font size title and select the current size menu item.
1302 fontSizeMenuItem.setTitle(fontSizeTitle);
1303 selectedFontSizeMenuItem.setChecked(true);
1305 // Run all the other default commands.
1306 super.onPrepareOptionsMenu(menu);
1308 // Display the menu.
1313 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1314 @SuppressLint("SetJavaScriptEnabled")
1315 public boolean onOptionsItemSelected(MenuItem menuItem) {
1316 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1317 if (inFullScreenBrowsingMode) {
1318 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1319 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1321 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1323 /* Hide the system bars.
1324 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1325 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1326 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1327 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1329 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1330 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1333 // Get the selected menu item ID.
1334 int menuItemId = menuItem.getItemId();
1336 // Get a handle for the shared preferences.
1337 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1339 // Get a handle for the cookie manager.
1340 CookieManager cookieManager = CookieManager.getInstance();
1342 // Run the commands that correlate to the selected menu item.
1343 switch (menuItemId) {
1344 case R.id.toggle_javascript:
1345 // Toggle the JavaScript status.
1346 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1348 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1349 updatePrivacyIcons(true);
1351 // Display a `Snackbar`.
1352 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
1353 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1354 } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled.
1355 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1356 } else { // Privacy mode.
1357 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1360 // Reload the current WebView.
1361 currentWebView.reload();
1364 case R.id.add_or_edit_domain:
1365 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1366 // Reapply the domain settings on returning to `MainWebViewActivity`.
1367 reapplyDomainSettingsOnRestart = true;
1369 // Create an intent to launch the domains activity.
1370 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1372 // Add the extra information to the intent.
1373 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1374 domainsIntent.putExtra("close_on_back", true);
1375 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1377 // Get the current certificate.
1378 SslCertificate sslCertificate = currentWebView.getCertificate();
1380 // Check to see if the SSL certificate is populated.
1381 if (sslCertificate != null) {
1382 // Extract the certificate to strings.
1383 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1384 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1385 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1386 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1387 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1388 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1389 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1390 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1392 // Add the certificate to the intent.
1393 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1394 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1395 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1396 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1397 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1398 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1399 domainsIntent.putExtra("ssl_start_date", startDateLong);
1400 domainsIntent.putExtra("ssl_end_date", endDateLong);
1403 // Check to see if the current IP addresses have been received.
1404 if (currentWebView.hasCurrentIpAddresses()) {
1405 // Add the current IP addresses to the intent.
1406 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1410 startActivity(domainsIntent);
1411 } else { // Add a new domain.
1412 // Apply the new domain settings on returning to `MainWebViewActivity`.
1413 reapplyDomainSettingsOnRestart = true;
1415 // Get the current domain
1416 Uri currentUri = Uri.parse(currentWebView.getUrl());
1417 String currentDomain = currentUri.getHost();
1419 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1420 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1422 // Create the domain and store the database ID.
1423 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1425 // Create an intent to launch the domains activity.
1426 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1428 // Add the extra information to the intent.
1429 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1430 domainsIntent.putExtra("close_on_back", true);
1431 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1433 // Get the current certificate.
1434 SslCertificate sslCertificate = currentWebView.getCertificate();
1436 // Check to see if the SSL certificate is populated.
1437 if (sslCertificate != null) {
1438 // Extract the certificate to strings.
1439 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1440 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1441 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1442 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1443 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1444 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1445 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1446 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1448 // Add the certificate to the intent.
1449 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1450 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1451 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1452 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1453 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1454 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1455 domainsIntent.putExtra("ssl_start_date", startDateLong);
1456 domainsIntent.putExtra("ssl_end_date", endDateLong);
1459 // Check to see if the current IP addresses have been received.
1460 if (currentWebView.hasCurrentIpAddresses()) {
1461 // Add the current IP addresses to the intent.
1462 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1466 startActivity(domainsIntent);
1470 case R.id.toggle_first_party_cookies:
1471 // Switch the first-party cookie status.
1472 cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1474 // Store the first-party cookie status.
1475 currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1477 // Update the menu checkbox.
1478 menuItem.setChecked(cookieManager.acceptCookie());
1480 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1481 updatePrivacyIcons(true);
1483 // Display a snackbar.
1484 if (cookieManager.acceptCookie()) { // First-party cookies are enabled.
1485 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1486 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1487 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1488 } else { // Privacy mode.
1489 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1492 // Reload the current WebView.
1493 currentWebView.reload();
1496 case R.id.toggle_third_party_cookies:
1497 if (Build.VERSION.SDK_INT >= 21) {
1498 // Switch the status of thirdPartyCookiesEnabled.
1499 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1501 // Update the menu checkbox.
1502 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1504 // Display a snackbar.
1505 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1506 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1508 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1511 // Reload the current WebView.
1512 currentWebView.reload();
1513 } // Else do nothing because SDK < 21.
1516 case R.id.toggle_dom_storage:
1517 // Toggle the status of domStorageEnabled.
1518 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1520 // Update the menu checkbox.
1521 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1523 // Update the privacy icon. `true` refreshes the app bar icons.
1524 updatePrivacyIcons(true);
1526 // Display a snackbar.
1527 if (currentWebView.getSettings().getDomStorageEnabled()) {
1528 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1530 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1533 // Reload the current WebView.
1534 currentWebView.reload();
1537 // Form data can be removed once the minimum API >= 26.
1538 case R.id.toggle_save_form_data:
1539 // Switch the status of saveFormDataEnabled.
1540 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1542 // Update the menu checkbox.
1543 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1545 // Display a snackbar.
1546 if (currentWebView.getSettings().getSaveFormData()) {
1547 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1549 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1552 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1553 updatePrivacyIcons(true);
1555 // Reload the current WebView.
1556 currentWebView.reload();
1559 case R.id.clear_cookies:
1560 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1561 .setAction(R.string.undo, v -> {
1562 // Do nothing because everything will be handled by `onDismissed()` below.
1564 .addCallback(new Snackbar.Callback() {
1565 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1567 public void onDismissed(Snackbar snackbar, int event) {
1568 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1569 // Delete the cookies, which command varies by SDK.
1570 if (Build.VERSION.SDK_INT < 21) {
1571 cookieManager.removeAllCookie();
1573 cookieManager.removeAllCookies(null);
1581 case R.id.clear_dom_storage:
1582 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1583 .setAction(R.string.undo, v -> {
1584 // Do nothing because everything will be handled by `onDismissed()` below.
1586 .addCallback(new Snackbar.Callback() {
1587 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1589 public void onDismissed(Snackbar snackbar, int event) {
1590 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1591 // Delete the DOM Storage.
1592 WebStorage webStorage = WebStorage.getInstance();
1593 webStorage.deleteAllData();
1595 // Initialize a handler to manually delete the DOM storage files and directories.
1596 Handler deleteDomStorageHandler = new Handler();
1598 // Setup a runnable to manually delete the DOM storage files and directories.
1599 Runnable deleteDomStorageRunnable = () -> {
1601 // Get a handle for the runtime.
1602 Runtime runtime = Runtime.getRuntime();
1604 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1605 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1606 String privateDataDirectoryString = getApplicationInfo().dataDir;
1608 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1609 Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1611 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1612 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1613 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1614 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1615 Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1617 // Wait for the processes to finish.
1618 deleteLocalStorageProcess.waitFor();
1619 deleteIndexProcess.waitFor();
1620 deleteQuotaManagerProcess.waitFor();
1621 deleteQuotaManagerJournalProcess.waitFor();
1622 deleteDatabasesProcess.waitFor();
1623 } catch (Exception exception) {
1624 // Do nothing if an error is thrown.
1628 // Manually delete the DOM storage files after 200 milliseconds.
1629 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1636 // Form data can be remove once the minimum API >= 26.
1637 case R.id.clear_form_data:
1638 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1639 .setAction(R.string.undo, v -> {
1640 // Do nothing because everything will be handled by `onDismissed()` below.
1642 .addCallback(new Snackbar.Callback() {
1643 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1645 public void onDismissed(Snackbar snackbar, int event) {
1646 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1647 // Delete the form data.
1648 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1649 mainWebViewDatabase.clearFormData();
1657 // Toggle the EasyList status.
1658 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1660 // Update the menu checkbox.
1661 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1663 // Reload the current WebView.
1664 currentWebView.reload();
1667 case R.id.easyprivacy:
1668 // Toggle the EasyPrivacy status.
1669 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1671 // Update the menu checkbox.
1672 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1674 // Reload the current WebView.
1675 currentWebView.reload();
1678 case R.id.fanboys_annoyance_list:
1679 // Toggle Fanboy's Annoyance List status.
1680 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1682 // Update the menu checkbox.
1683 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1685 // Update the staus of Fanboy's Social Blocking List.
1686 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1687 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1689 // Reload the current WebView.
1690 currentWebView.reload();
1693 case R.id.fanboys_social_blocking_list:
1694 // Toggle Fanboy's Social Blocking List status.
1695 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1697 // Update the menu checkbox.
1698 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1700 // Reload the current WebView.
1701 currentWebView.reload();
1704 case R.id.ultraprivacy:
1705 // Toggle the UltraPrivacy status.
1706 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1708 // Update the menu checkbox.
1709 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1711 // Reload the current WebView.
1712 currentWebView.reload();
1715 case R.id.block_all_third_party_requests:
1716 //Toggle the third-party requests blocker status.
1717 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1719 // Update the menu checkbox.
1720 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1722 // Reload the current WebView.
1723 currentWebView.reload();
1726 case R.id.user_agent_privacy_browser:
1727 // Update the user agent.
1728 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1730 // Reload the current WebView.
1731 currentWebView.reload();
1734 case R.id.user_agent_webview_default:
1735 // Update the user agent.
1736 currentWebView.getSettings().setUserAgentString("");
1738 // Reload the current WebView.
1739 currentWebView.reload();
1742 case R.id.user_agent_firefox_on_android:
1743 // Update the user agent.
1744 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1746 // Reload the current WebView.
1747 currentWebView.reload();
1750 case R.id.user_agent_chrome_on_android:
1751 // Update the user agent.
1752 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1754 // Reload the current WebView.
1755 currentWebView.reload();
1758 case R.id.user_agent_safari_on_ios:
1759 // Update the user agent.
1760 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1762 // Reload the current WebView.
1763 currentWebView.reload();
1766 case R.id.user_agent_firefox_on_linux:
1767 // Update the user agent.
1768 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1770 // Reload the current WebView.
1771 currentWebView.reload();
1774 case R.id.user_agent_chromium_on_linux:
1775 // Update the user agent.
1776 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1778 // Reload the current WebView.
1779 currentWebView.reload();
1782 case R.id.user_agent_firefox_on_windows:
1783 // Update the user agent.
1784 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1786 // Reload the current WebView.
1787 currentWebView.reload();
1790 case R.id.user_agent_chrome_on_windows:
1791 // Update the user agent.
1792 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1794 // Reload the current WebView.
1795 currentWebView.reload();
1798 case R.id.user_agent_edge_on_windows:
1799 // Update the user agent.
1800 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1802 // Reload the current WebView.
1803 currentWebView.reload();
1806 case R.id.user_agent_internet_explorer_on_windows:
1807 // Update the user agent.
1808 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1810 // Reload the current WebView.
1811 currentWebView.reload();
1814 case R.id.user_agent_safari_on_macos:
1815 // Update the user agent.
1816 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1818 // Reload the current WebView.
1819 currentWebView.reload();
1822 case R.id.user_agent_custom:
1823 // Update the user agent.
1824 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1826 // Reload the current WebView.
1827 currentWebView.reload();
1830 case R.id.font_size_twenty_five_percent:
1831 currentWebView.getSettings().setTextZoom(25);
1834 case R.id.font_size_fifty_percent:
1835 currentWebView.getSettings().setTextZoom(50);
1838 case R.id.font_size_seventy_five_percent:
1839 currentWebView.getSettings().setTextZoom(75);
1842 case R.id.font_size_one_hundred_percent:
1843 currentWebView.getSettings().setTextZoom(100);
1846 case R.id.font_size_one_hundred_twenty_five_percent:
1847 currentWebView.getSettings().setTextZoom(125);
1850 case R.id.font_size_one_hundred_fifty_percent:
1851 currentWebView.getSettings().setTextZoom(150);
1854 case R.id.font_size_one_hundred_seventy_five_percent:
1855 currentWebView.getSettings().setTextZoom(175);
1858 case R.id.font_size_two_hundred_percent:
1859 currentWebView.getSettings().setTextZoom(200);
1862 case R.id.swipe_to_refresh:
1863 // Toggle the stored status of swipe to refresh.
1864 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1866 // Get a handle for the swipe refresh layout.
1867 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1869 // Update the swipe refresh layout.
1870 if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
1871 if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, the status of the scroll refresh listener is continuously updated by the on scroll change listener.
1872 // Only enable the swipe refresh layout if the WebView is scrolled to the top.
1873 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1874 } else { // For API < 23, the swipe refresh layout is always enabled.
1875 // Enable the swipe refresh layout.
1876 swipeRefreshLayout.setEnabled(true);
1878 } else { // Swipe to refresh is disabled.
1879 // Disable the swipe refresh layout.
1880 swipeRefreshLayout.setEnabled(false);
1884 case R.id.display_images:
1885 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1886 // Disable loading of images.
1887 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1889 // Reload the website to remove existing images.
1890 currentWebView.reload();
1891 } else { // Images are not currently loaded automatically.
1892 // Enable loading of images. Missing images will be loaded without the need for a reload.
1893 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1897 case R.id.night_mode:
1898 // Toggle night mode.
1899 currentWebView.setNightMode(!currentWebView.getNightMode());
1901 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1902 if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript.
1903 // Enable JavaScript.
1904 currentWebView.getSettings().setJavaScriptEnabled(true);
1905 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
1906 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1907 currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
1908 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
1909 // Apply the JavaScript preference.
1910 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1913 // Update the privacy icons.
1914 updatePrivacyIcons(false);
1916 // Reload the website.
1917 currentWebView.reload();
1920 case R.id.find_on_page:
1921 // Get a handle for the views.
1922 Toolbar toolbar = findViewById(R.id.toolbar);
1923 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1924 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1926 // Hide the toolbar.
1927 toolbar.setVisibility(View.GONE);
1929 // Show the find on page linear layout.
1930 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1932 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1933 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1934 findOnPageEditText.postDelayed(() -> {
1935 // Set the focus on `findOnPageEditText`.
1936 findOnPageEditText.requestFocus();
1938 // Get a handle for the input method manager.
1939 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1941 // Remove the lint warning below that the input method manager might be null.
1942 assert inputMethodManager != null;
1944 // Display the keyboard. `0` sets no input flags.
1945 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1949 case R.id.view_source:
1950 // Create an intent to launch the view source activity.
1951 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1953 // Add the variables to the intent.
1954 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1955 viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
1958 startActivity(viewSourceIntent);
1961 case R.id.share_url:
1962 // Setup the share string.
1963 String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1965 // Create the share intent.
1966 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1967 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1968 shareIntent.setType("text/plain");
1971 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1975 // Get a print manager instance.
1976 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1978 // Remove the lint error below that print manager might be null.
1979 assert printManager != null;
1981 // Create a print document adapter from the current WebView.
1982 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1984 // Print the document.
1985 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
1988 case R.id.open_with_app:
1989 openWithApp(currentWebView.getUrl());
1992 case R.id.open_with_browser:
1993 openWithBrowser(currentWebView.getUrl());
1996 case R.id.add_to_homescreen:
1997 // Instantiate the create home screen shortcut dialog.
1998 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1999 currentWebView.getFavoriteOrDefaultIcon());
2001 // Show the create home screen shortcut dialog.
2002 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2005 case R.id.proxy_through_orbot:
2006 // Toggle the proxy through Orbot variable.
2007 proxyThroughOrbot = !proxyThroughOrbot;
2009 // Apply the proxy through Orbot settings.
2010 applyProxyThroughOrbot(true);
2014 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2015 // Reload the current WebView.
2016 currentWebView.reload();
2017 } else { // The stop button was pushed.
2018 // Stop the loading of the WebView.
2019 currentWebView.stopLoading();
2023 case R.id.ad_consent:
2024 // Display the ad consent dialog.
2025 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2026 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2030 // Don't consume the event.
2031 return super.onOptionsItemSelected(menuItem);
2035 // removeAllCookies is deprecated, but it is required for API < 21.
2037 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2038 // Get the menu item ID.
2039 int menuItemId = menuItem.getItemId();
2041 // Get a handle for the shared preferences.
2042 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2044 // Run the commands that correspond to the selected menu item.
2045 switch (menuItemId) {
2046 case R.id.close_tab:
2047 // Get a handle for the tab layout and the view pager.
2048 TabLayout tabLayout = findViewById(R.id.tablayout);
2049 ViewPager webViewPager = findViewById(R.id.webviewpager);
2051 // Get the current tab number.
2052 int currentTabNumber = tabLayout.getSelectedTabPosition();
2054 // Delete the current tab.
2055 tabLayout.removeTabAt(currentTabNumber);
2057 // 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.
2058 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
2059 setCurrentWebView(currentTabNumber);
2063 case R.id.clear_and_exit:
2064 // Close the bookmarks cursor and database.
2065 bookmarksCursor.close();
2066 bookmarksDatabaseHelper.close();
2068 // Get the status of the clear everything preference.
2069 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2071 // Get a handle for the runtime.
2072 Runtime runtime = Runtime.getRuntime();
2074 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
2075 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
2076 String privateDataDirectoryString = getApplicationInfo().dataDir;
2079 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2080 // The command to remove cookies changed slightly in API 21.
2081 if (Build.VERSION.SDK_INT >= 21) {
2082 CookieManager.getInstance().removeAllCookies(null);
2084 CookieManager.getInstance().removeAllCookie();
2087 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2089 // Two commands must be used because `Runtime.exec()` does not like `*`.
2090 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2091 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2093 // Wait until the processes have finished.
2094 deleteCookiesProcess.waitFor();
2095 deleteCookiesJournalProcess.waitFor();
2096 } catch (Exception exception) {
2097 // Do nothing if an error is thrown.
2101 // Clear DOM storage.
2102 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2103 // Ask `WebStorage` to clear the DOM storage.
2104 WebStorage webStorage = WebStorage.getInstance();
2105 webStorage.deleteAllData();
2107 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2109 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2110 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2112 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2113 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2114 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2115 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2116 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2118 // Wait until the processes have finished.
2119 deleteLocalStorageProcess.waitFor();
2120 deleteIndexProcess.waitFor();
2121 deleteQuotaManagerProcess.waitFor();
2122 deleteQuotaManagerJournalProcess.waitFor();
2123 deleteDatabaseProcess.waitFor();
2124 } catch (Exception exception) {
2125 // Do nothing if an error is thrown.
2129 // Clear form data if the API < 26.
2130 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2131 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2132 webViewDatabase.clearFormData();
2134 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2136 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2137 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2138 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2140 // Wait until the processes have finished.
2141 deleteWebDataProcess.waitFor();
2142 deleteWebDataJournalProcess.waitFor();
2143 } catch (Exception exception) {
2144 // Do nothing if an error is thrown.
2149 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2150 // Clear the cache from each WebView.
2151 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2152 // Get the WebView tab fragment.
2153 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2155 // Get the fragment view.
2156 View fragmentView = webViewTabFragment.getView();
2158 // Only clear the cache if the WebView exists.
2159 if (fragmentView != null) {
2160 // Get the nested scroll WebView from the tab fragment.
2161 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2163 // Clear the cache for this WebView.
2164 nestedScrollWebView.clearCache(true);
2168 // Manually delete the cache directories.
2170 // Delete the main cache directory.
2171 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2173 // Delete the secondary `Service Worker` cache directory.
2174 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2175 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2177 // Wait until the processes have finished.
2178 deleteCacheProcess.waitFor();
2179 deleteServiceWorkerProcess.waitFor();
2180 } catch (Exception exception) {
2181 // Do nothing if an error is thrown.
2185 // Wipe out each WebView.
2186 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2187 // Get the WebView tab fragment.
2188 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2190 // Get the fragment view.
2191 View fragmentView = webViewTabFragment.getView();
2193 // Only wipe out the WebView if it exists.
2194 if (fragmentView != null) {
2195 // Get the nested scroll WebView from the tab fragment.
2196 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2198 // Clear SSL certificate preferences for this WebView.
2199 nestedScrollWebView.clearSslPreferences();
2201 // Clear the back/forward history for this WebView.
2202 nestedScrollWebView.clearHistory();
2204 // Destroy the internal state of `mainWebView`.
2205 nestedScrollWebView.destroy();
2209 // Clear the custom headers.
2210 customHeaders.clear();
2212 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2213 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2214 if (clearEverything) {
2216 // Delete the folder.
2217 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2219 // Wait until the process has finished.
2220 deleteAppWebviewProcess.waitFor();
2221 } catch (Exception exception) {
2222 // Do nothing if an error is thrown.
2226 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2227 if (Build.VERSION.SDK_INT >= 21) {
2228 finishAndRemoveTask();
2233 // Remove the terminated program from RAM. The status code is `0`.
2238 // Select the homepage based on the proxy through Orbot status.
2239 if (proxyThroughOrbot) {
2240 // Load the Tor homepage.
2241 loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
2243 // Load the normal homepage.
2244 loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2249 if (currentWebView.canGoBack()) {
2250 // Reset the current domain name so that navigation works if third-party requests are blocked.
2251 currentWebView.resetCurrentDomainName();
2253 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2254 currentWebView.setNavigatingHistory(true);
2256 // Load the previous website in the history.
2257 currentWebView.goBack();
2262 if (currentWebView.canGoForward()) {
2263 // Reset the current domain name so that navigation works if third-party requests are blocked.
2264 currentWebView.resetCurrentDomainName();
2266 // Set navigating history so that the domain settings are applied when the new URL is loaded.
2267 currentWebView.setNavigatingHistory(true);
2269 // Load the next website in the history.
2270 currentWebView.goForward();
2275 // Get the `WebBackForwardList`.
2276 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2278 // Show the URL history dialog and name this instance `R.string.history`.
2279 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2280 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2284 // Populate the resource requests.
2285 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2287 // Create an intent to launch the Requests activity.
2288 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2290 // Add the block third-party requests status to the intent.
2291 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2294 startActivity(requestsIntent);
2297 case R.id.downloads:
2298 // Launch the system Download Manager.
2299 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2301 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2302 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2304 startActivity(downloadManagerIntent);
2308 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2309 reapplyDomainSettingsOnRestart = true;
2311 // Launch the domains activity.
2312 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2314 // Add the extra information to the intent.
2315 domainsIntent.putExtra("current_url", currentWebView.getUrl());
2317 // Get the current certificate.
2318 SslCertificate sslCertificate = currentWebView.getCertificate();
2320 // Check to see if the SSL certificate is populated.
2321 if (sslCertificate != null) {
2322 // Extract the certificate to strings.
2323 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2324 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2325 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2326 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2327 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2328 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2329 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2330 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2332 // Add the certificate to the intent.
2333 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2334 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2335 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2336 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2337 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2338 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2339 domainsIntent.putExtra("ssl_start_date", startDateLong);
2340 domainsIntent.putExtra("ssl_end_date", endDateLong);
2343 // Check to see if the current IP addresses have been received.
2344 if (currentWebView.hasCurrentIpAddresses()) {
2345 // Add the current IP addresses to the intent.
2346 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2350 startActivity(domainsIntent);
2354 // Set the flag to reapply app settings on restart when returning from Settings.
2355 reapplyAppSettingsOnRestart = true;
2357 // Set the flag to reapply the domain settings on restart when returning from Settings.
2358 reapplyDomainSettingsOnRestart = true;
2360 // Launch the settings activity.
2361 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2362 startActivity(settingsIntent);
2365 case R.id.import_export:
2366 // Launch the import/export activity.
2367 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2368 startActivity(importExportIntent);
2372 // Launch the logcat activity.
2373 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2374 startActivity(logcatIntent);
2378 // Launch `GuideActivity`.
2379 Intent guideIntent = new Intent(this, GuideActivity.class);
2380 startActivity(guideIntent);
2384 // Create an intent to launch the about activity.
2385 Intent aboutIntent = new Intent(this, AboutActivity.class);
2387 // Create a string array for the blocklist versions.
2388 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],
2389 ultraPrivacy.get(0).get(0)[0]};
2391 // Add the blocklist versions to the intent.
2392 aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2395 startActivity(aboutIntent);
2399 // Get a handle for the drawer layout.
2400 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2402 // Close the navigation drawer.
2403 drawerLayout.closeDrawer(GravityCompat.START);
2408 public void onPostCreate(Bundle savedInstanceState) {
2409 // Run the default commands.
2410 super.onPostCreate(savedInstanceState);
2412 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2413 actionBarDrawerToggle.syncState();
2417 public void onConfigurationChanged(Configuration newConfig) {
2418 // Run the default commands.
2419 super.onConfigurationChanged(newConfig);
2421 // Get the status bar pixel size.
2422 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2423 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2425 // Get the resource density.
2426 float screenDensity = getResources().getDisplayMetrics().density;
2428 // Recalculate the drawer header padding.
2429 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2430 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2431 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2433 // Reload the ad for the free flavor if not in full screen mode.
2434 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2435 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2436 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2439 // `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:
2440 // https://code.google.com/p/android/issues/detail?id=20493#c8
2441 // ActivityCompat.invalidateOptionsMenu(this);
2445 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2446 // Store the hit test result.
2447 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2449 // Create the URL strings.
2450 final String imageUrl;
2451 final String linkUrl;
2453 // Get handles for the system managers.
2454 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2455 FragmentManager fragmentManager = getSupportFragmentManager();
2456 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2458 // Remove the lint errors below that the clipboard manager might be null.
2459 assert clipboardManager != null;
2461 // Process the link according to the type.
2462 switch (hitTestResult.getType()) {
2463 // `SRC_ANCHOR_TYPE` is a link.
2464 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2465 // Get the target URL.
2466 linkUrl = hitTestResult.getExtra();
2468 // Set the target URL as the title of the `ContextMenu`.
2469 menu.setHeaderTitle(linkUrl);
2471 // Add a Load URL entry.
2472 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2481 // Add an Open with App entry.
2482 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2483 openWithApp(linkUrl);
2487 // Add an Open with Browser entry.
2488 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2489 openWithBrowser(linkUrl);
2493 // Add a Copy URL entry.
2494 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2495 // Save the link URL in a `ClipData`.
2496 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2498 // Set the `ClipData` as the clipboard's primary clip.
2499 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2503 // Add a Download URL entry.
2504 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2505 // Check if the download should be processed by an external app.
2506 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2507 openUrlWithExternalApp(linkUrl);
2508 } else { // Download with Android's download manager.
2509 // Check to see if the storage permission has already been granted.
2510 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2511 // Store the variables for future use by `onRequestPermissionsResult()`.
2512 downloadUrl = linkUrl;
2513 downloadContentDisposition = "none";
2514 downloadContentLength = -1;
2516 // Show a dialog if the user has previously denied the permission.
2517 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2518 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2519 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2521 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2522 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2523 } else { // Show the permission request directly.
2524 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2525 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2527 } else { // The storage permission has already been granted.
2528 // Get a handle for the download file alert dialog.
2529 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2531 // Show the download file alert dialog.
2532 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2538 // Add a Cancel entry, which by default closes the context menu.
2539 menu.add(R.string.cancel);
2542 case WebView.HitTestResult.EMAIL_TYPE:
2543 // Get the target URL.
2544 linkUrl = hitTestResult.getExtra();
2546 // Set the target URL as the title of the `ContextMenu`.
2547 menu.setHeaderTitle(linkUrl);
2549 // Add a Write Email entry.
2550 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2551 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2552 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2554 // Parse the url and set it as the data for the `Intent`.
2555 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2557 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2558 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2561 startActivity(emailIntent);
2565 // Add a Copy Email Address entry.
2566 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2567 // Save the email address in a `ClipData`.
2568 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2570 // Set the `ClipData` as the clipboard's primary clip.
2571 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2575 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2576 menu.add(R.string.cancel);
2579 // `IMAGE_TYPE` is an image.
2580 case WebView.HitTestResult.IMAGE_TYPE:
2581 // Get the image URL.
2582 imageUrl = hitTestResult.getExtra();
2584 // Set the image URL as the title of the `ContextMenu`.
2585 menu.setHeaderTitle(imageUrl);
2587 // Add a View Image entry.
2588 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2593 // Add a Download Image entry.
2594 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2595 // Check if the download should be processed by an external app.
2596 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2597 openUrlWithExternalApp(imageUrl);
2598 } else { // Download with Android's download manager.
2599 // Check to see if the storage permission has already been granted.
2600 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2601 // Store the image URL for use by `onRequestPermissionResult()`.
2602 downloadImageUrl = imageUrl;
2604 // Show a dialog if the user has previously denied the permission.
2605 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2606 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2607 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2609 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2610 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2611 } else { // Show the permission request directly.
2612 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2613 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2615 } else { // The storage permission has already been granted.
2616 // Get a handle for the download image alert dialog.
2617 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2619 // Show the download image alert dialog.
2620 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2626 // Add a Copy URL entry.
2627 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2628 // Save the image URL in a `ClipData`.
2629 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2631 // Set the `ClipData` as the clipboard's primary clip.
2632 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2636 // Add an Open with App entry.
2637 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2638 openWithApp(imageUrl);
2642 // Add an Open with Browser entry.
2643 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2644 openWithBrowser(imageUrl);
2648 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2649 menu.add(R.string.cancel);
2653 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2654 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2655 // Get the image URL.
2656 imageUrl = hitTestResult.getExtra();
2658 // Set the image URL as the title of the `ContextMenu`.
2659 menu.setHeaderTitle(imageUrl);
2661 // Add a `View Image` entry.
2662 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2667 // Add a `Download Image` entry.
2668 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2669 // Check if the download should be processed by an external app.
2670 if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
2671 openUrlWithExternalApp(imageUrl);
2672 } else { // Download with Android's download manager.
2673 // Check to see if the storage permission has already been granted.
2674 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2675 // Store the image URL for use by `onRequestPermissionResult()`.
2676 downloadImageUrl = imageUrl;
2678 // Show a dialog if the user has previously denied the permission.
2679 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2680 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2681 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2683 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2684 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2685 } else { // Show the permission request directly.
2686 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2687 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2689 } else { // The storage permission has already been granted.
2690 // Get a handle for the download image alert dialog.
2691 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2693 // Show the download image alert dialog.
2694 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2700 // Add a `Copy URL` entry.
2701 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2702 // Save the image URL in a `ClipData`.
2703 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2705 // Set the `ClipData` as the clipboard's primary clip.
2706 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2710 // Add an Open with App entry.
2711 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2712 openWithApp(imageUrl);
2716 // Add an Open with Browser entry.
2717 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2718 openWithBrowser(imageUrl);
2722 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2723 menu.add(R.string.cancel);
2729 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2730 // Get a handle for the bookmarks list view.
2731 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2733 // Get the views from the dialog fragment.
2734 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2735 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2737 // Extract the strings from the edit texts.
2738 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2739 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2741 // Create a favorite icon byte array output stream.
2742 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2744 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2745 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2747 // Convert the favorite icon byte array stream to a byte array.
2748 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2750 // Display the new bookmark below the current items in the (0 indexed) list.
2751 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2753 // Create the bookmark.
2754 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2756 // Update the bookmarks cursor with the current contents of this folder.
2757 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2759 // Update the list view.
2760 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2762 // Scroll to the new bookmark.
2763 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2767 public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2768 // Get a handle for the bookmarks list view.
2769 ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2771 // Get handles for the views in the dialog fragment.
2772 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2773 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2774 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2776 // Get new folder name string.
2777 String folderNameString = createFolderNameEditText.getText().toString();
2779 // Create a folder icon bitmap.
2780 Bitmap folderIconBitmap;
2782 // Set the folder icon bitmap according to the dialog.
2783 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2784 // Get the default folder icon drawable.
2785 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2787 // Convert the folder icon drawable to a bitmap drawable.
2788 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2790 // Convert the folder icon bitmap drawable to a bitmap.
2791 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2792 } else { // Use the WebView favorite icon.
2793 // Copy the favorite icon bitmap to the folder icon bitmap.
2794 folderIconBitmap = favoriteIconBitmap;
2797 // Create a folder icon byte array output stream.
2798 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2800 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2801 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2803 // Convert the folder icon byte array stream to a byte array.
2804 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2806 // Move all the bookmarks down one in the display order.
2807 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2808 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2809 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2812 // Create the folder, which will be placed at the top of the `ListView`.
2813 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2815 // Update the bookmarks cursor with the current contents of this folder.
2816 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2818 // Update the `ListView`.
2819 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2821 // Scroll to the new folder.
2822 bookmarksListView.setSelection(0);
2826 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2827 // Get handles for the views from `dialogFragment`.
2828 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2829 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2830 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2832 // Store the bookmark strings.
2833 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2834 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2836 // Update the bookmark.
2837 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2838 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2839 } else { // Update the bookmark using the `WebView` favorite icon.
2840 // Create a favorite icon byte array output stream.
2841 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2843 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2844 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2846 // Convert the favorite icon byte array stream to a byte array.
2847 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2849 // Update the bookmark and the favorite icon.
2850 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2853 // Update the bookmarks cursor with the current contents of this folder.
2854 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2856 // Update the list view.
2857 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2861 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2862 // Get handles for the views from `dialogFragment`.
2863 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2864 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2865 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2866 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2868 // Get the new folder name.
2869 String newFolderNameString = editFolderNameEditText.getText().toString();
2871 // Check if the favorite icon has changed.