2 * Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
4 * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
6 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
8 * Privacy Browser is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
13 * Privacy Browser is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
22 package com.stoutner.privacybrowser.activities;
24 import android.Manifest;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.DownloadManager;
28 import android.app.SearchManager;
29 import android.content.ActivityNotFoundException;
30 import android.content.BroadcastReceiver;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.res.Configuration;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Typeface;
43 import android.graphics.drawable.BitmapDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.net.Uri;
46 import android.net.http.SslCertificate;
47 import android.net.http.SslError;
48 import android.os.Build;
49 import android.os.Bundle;
50 import android.os.Environment;
51 import android.os.Handler;
52 import android.preference.PreferenceManager;
53 import android.print.PrintDocumentAdapter;
54 import android.print.PrintManager;
55 import android.text.Editable;
56 import android.text.Spanned;
57 import android.text.TextWatcher;
58 import android.text.style.ForegroundColorSpan;
59 import android.util.Patterns;
60 import android.view.ContextMenu;
61 import android.view.GestureDetector;
62 import android.view.KeyEvent;
63 import android.view.Menu;
64 import android.view.MenuItem;
65 import android.view.MotionEvent;
66 import android.view.View;
67 import android.view.ViewGroup;
68 import android.view.WindowManager;
69 import android.view.inputmethod.InputMethodManager;
70 import android.webkit.CookieManager;
71 import android.webkit.HttpAuthHandler;
72 import android.webkit.SslErrorHandler;
73 import android.webkit.ValueCallback;
74 import android.webkit.WebBackForwardList;
75 import android.webkit.WebChromeClient;
76 import android.webkit.WebResourceResponse;
77 import android.webkit.WebSettings;
78 import android.webkit.WebStorage;
79 import android.webkit.WebView;
80 import android.webkit.WebViewClient;
81 import android.webkit.WebViewDatabase;
82 import android.widget.ArrayAdapter;
83 import android.widget.CursorAdapter;
84 import android.widget.EditText;
85 import android.widget.FrameLayout;
86 import android.widget.ImageView;
87 import android.widget.LinearLayout;
88 import android.widget.ListView;
89 import android.widget.ProgressBar;
90 import android.widget.RadioButton;
91 import android.widget.RelativeLayout;
92 import android.widget.TextView;
94 import androidx.annotation.NonNull;
95 import androidx.appcompat.app.ActionBar;
96 import androidx.appcompat.app.ActionBarDrawerToggle;
97 import androidx.appcompat.app.AppCompatActivity;
98 import androidx.appcompat.widget.Toolbar;
99 import androidx.core.app.ActivityCompat;
100 import androidx.core.content.ContextCompat;
101 import androidx.core.view.GravityCompat;
102 import androidx.drawerlayout.widget.DrawerLayout;
103 import androidx.fragment.app.DialogFragment;
104 import androidx.fragment.app.FragmentManager;
105 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
106 import androidx.viewpager.widget.ViewPager;
108 import com.google.android.material.floatingactionbutton.FloatingActionButton;
109 import com.google.android.material.navigation.NavigationView;
110 import com.google.android.material.snackbar.Snackbar;
111 import com.google.android.material.tabs.TabLayout;
113 import com.stoutner.privacybrowser.BuildConfig;
114 import com.stoutner.privacybrowser.R;
115 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
116 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
117 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
118 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
121 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
124 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
125 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
126 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
127 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
128 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
129 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
130 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
131 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
132 import com.stoutner.privacybrowser.helpers.AdHelper;
133 import com.stoutner.privacybrowser.helpers.BlockListHelper;
134 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
135 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
136 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
138 import com.stoutner.privacybrowser.views.NestedScrollWebView;
140 import java.io.ByteArrayInputStream;
141 import java.io.ByteArrayOutputStream;
143 import java.io.IOException;
144 import java.io.UnsupportedEncodingException;
145 import java.net.MalformedURLException;
147 import java.net.URLDecoder;
148 import java.net.URLEncoder;
149 import java.util.ArrayList;
150 import java.util.Date;
151 import java.util.HashMap;
152 import java.util.HashSet;
153 import java.util.List;
154 import java.util.Map;
155 import java.util.Set;
157 // TODO. The swipe refresh indicator needs to be enabled/disabled when switching tabs.
159 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
160 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
161 DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
162 EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener,
163 PinnedMismatchDialog.PinnedMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
165 // `darkTheme` is public static so it can be accessed from everywhere.
166 public static boolean darkTheme;
168 // `allowScreenshots` is public static so it can be accessed from everywhere. It is also used in `onCreate()`.
169 public static boolean allowScreenshots;
171 // `favoriteIconBitmap` is public static so it can be accessed from `BookmarksActivity`, `BookmarksDatabaseViewActivity`, `CreateBookmarkFolderDialog`,
172 // `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, and `ViewSslCertificateDialog`. It is also used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
173 // `onCreateHomeScreenShortcut()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `applyDomainSettings()`.
174 public static Bitmap favoriteIconBitmap;
176 // `favoriteIconDefaultBitmap` public static so it can be accessed from `PinnedMismatchDialog`. It is also used in `onCreate()` and `applyDomainSettings`.
177 public static Bitmap favoriteIconDefaultBitmap;
180 // `formattedUrlString` is public static so it can be accessed from `AddDomainDialog`, `BookmarksActivity`, `DomainSettingsFragment`, and `PinnedMismatchDialog`.
181 // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, `loadUrlFromTextBox()`, and `applyProxyThroughOrbot()`.
182 public static String formattedUrlString;
184 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
185 public static String orbotStatus;
187 // The WebView pager adapter is accessed from `PinnedMismatchDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`.
188 public static WebViewPagerAdapter webViewPagerAdapter;
190 // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()`
191 public static boolean reloadOnRestart;
193 // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`. It is also used in `onRestart()`.
194 public static boolean loadUrlOnRestart;
196 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
197 public static boolean restartFromBookmarksActivity;
199 // The blocklist versions are public static so they can be accessed from `AboutTabFragment`. They are also used in `onCreate()`.
200 public static String easyListVersion;
201 public static String easyPrivacyVersion;
202 public static String fanboysAnnoyanceVersion;
203 public static String fanboysSocialVersion;
204 public static String ultraPrivacyVersion;
206 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
207 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
208 public static String currentBookmarksFolder;
210 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
211 public final static int UNRECOGNIZED_USER_AGENT = -1;
212 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
213 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
214 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
215 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
216 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
220 // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
221 private boolean navigatingHistory;
223 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
224 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
225 private NestedScrollWebView currentWebView;
227 // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
228 private FrameLayout fullScreenVideoFrameLayout;
230 // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`.
231 private CookieManager cookieManager;
233 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
234 private final Map<String, String> customHeaders = new HashMap<>();
236 // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`.
237 private boolean firstPartyCookiesEnabled;
239 // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. It can be removed once the minimum API >= 26.
240 private boolean saveFormDataEnabled;
242 // TODO Move to NestedScrollWebView.
243 // `nightMode` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
244 private boolean nightMode;
246 // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`.
247 private String homepage;
249 // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`.
250 private String searchURL;
252 // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()` and `updatePrivacyIcons()`.
253 private Menu optionsMenu;
255 // The refresh menu item is set in `onCreateOptionsMenu()` and accessed from `initializeWebView()`.
256 // It must be this way because `initializeWebView()` runs before the menu is created but doesn't actually modify the menu until later.
257 private MenuItem refreshMenuItem;
259 // The navigation requests menu item is used in `onCreate()` and accessed from `WebViewPagerAdapter`.
260 private MenuItem navigationRequestsMenuItem; // TODO.
262 // TODO. This could probably be removed.
263 // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
264 private BlockListHelper blockListHelper;
266 // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
267 private ArrayList<List<String[]>> easyList;
268 private ArrayList<List<String[]>> easyPrivacy;
269 private ArrayList<List<String[]>> fanboysAnnoyanceList;
270 private ArrayList<List<String[]>> fanboysSocialList;
271 private ArrayList<List<String[]>> ultraPrivacy;
273 // The blocklist menu items are used in `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, and `initializeWebView()`.
274 private MenuItem blocklistsMenuItem;
275 private MenuItem easyListMenuItem;
276 private MenuItem easyPrivacyMenuItem;
277 private MenuItem fanboysAnnoyanceListMenuItem;
278 private MenuItem fanboysSocialBlockingListMenuItem;
279 private MenuItem ultraPrivacyMenuItem;
280 private MenuItem blockAllThirdPartyRequestsMenuItem;
282 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
283 private String webViewDefaultUserAgent;
285 // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
286 private Runtime privacyBrowserRuntime;
288 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
289 private boolean proxyThroughOrbot;
291 // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
292 private boolean incognitoModeEnabled;
294 // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
295 private boolean fullScreenBrowsingModeEnabled;
297 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
298 private boolean inFullScreenBrowsingMode;
300 // Hide app bar is used in `onCreate()` and `applyAppSettings()`.
301 private boolean hideAppBar;
303 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
304 private boolean reapplyDomainSettingsOnRestart;
306 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
307 private boolean reapplyAppSettingsOnRestart;
309 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
310 private boolean displayingFullScreenVideo;
312 // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`.
313 private boolean downloadWithExternalApp;
315 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
316 private BroadcastReceiver orbotStatusBroadcastReceiver;
318 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
319 private boolean waitingForOrbot;
321 // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`.
322 private Boolean domainSettingsJavaScriptEnabled;
324 // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`.
325 private String waitingForOrbotHtmlString;
327 // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`.
328 private String privateDataDirectoryString;
330 // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
331 private EditText findOnPageEditText;
333 // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`.
334 private boolean displayAdditionalAppBarIcons;
336 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
337 private ActionBarDrawerToggle actionBarDrawerToggle;
339 // The color spans are used in `onCreate()` and `highlightUrlText()`.
340 private ForegroundColorSpan redColorSpan;
341 private ForegroundColorSpan initialGrayColorSpan;
342 private ForegroundColorSpan finalGrayColorSpan;
344 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
345 private int drawerHeaderPaddingLeftAndRight;
346 private int drawerHeaderPaddingTop;
347 private int drawerHeaderPaddingBottom;
349 // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
350 private SslErrorHandler sslErrorHandler;
352 // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`.
353 private static HttpAuthHandler httpAuthHandler;
355 // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
356 private InputMethodManager inputMethodManager;
358 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
359 // and `loadBookmarksFolder()`.
360 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
362 // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
363 private ListView bookmarksListView;
365 // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`.
366 private TextView bookmarksTitleTextView;
368 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
369 private Cursor bookmarksCursor;
371 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
372 private CursorAdapter bookmarksCursorAdapter;
374 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
375 private String oldFolderNameString;
377 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
378 private ValueCallback<Uri[]> fileChooserCallback;
380 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
381 private String downloadUrl;
382 private String downloadContentDisposition;
383 private long downloadContentLength;
385 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
386 private String downloadImageUrl;
388 // The user agent variables are used in `onCreate()` and `applyDomainSettings()`.
389 private ArrayAdapter<CharSequence> userAgentNamesArray;
390 private String[] userAgentDataArray;
392 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
393 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
394 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
397 // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
398 @SuppressLint("ClickableViewAccessibility")
399 protected void onCreate(Bundle savedInstanceState) {
400 // Get a handle for the shared preferences.
401 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
403 // Get the theme and screenshot preferences.
404 darkTheme = sharedPreferences.getBoolean("dark_theme", false);
405 allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
407 // Disable screenshots if not allowed.
408 if (!allowScreenshots) {
409 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
412 // Set the activity theme.
414 setTheme(R.style.PrivacyBrowserDark);
416 setTheme(R.style.PrivacyBrowserLight);
419 // Run the default commands.
420 super.onCreate(savedInstanceState);
422 // Set the content view.
423 setContentView(R.layout.main_framelayout);
425 // Get handles for the input method manager and toolbar.
426 inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
427 Toolbar toolbar = findViewById(R.id.toolbar);
429 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
430 setSupportActionBar(toolbar);
431 ActionBar actionBar = getSupportActionBar();
433 // This is needed to get rid of the Android Studio warning that the action bar might be null.
434 assert actionBar != null;
436 // Add the custom layout, which shows the URL text bar.
437 actionBar.setCustomView(R.layout.url_app_bar);
438 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
440 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
441 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
442 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
443 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
445 // Get handles for the URL views.
446 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
447 EditText urlEditText = findViewById(R.id.url_edittext);
449 // Remove the formatting from `urlTextBar` when the user is editing the text.
450 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
451 if (hasFocus) { // The user is editing the URL text box.
452 // Remove the highlighting.
453 urlEditText.getText().removeSpan(redColorSpan);
454 urlEditText.getText().removeSpan(initialGrayColorSpan);
455 urlEditText.getText().removeSpan(finalGrayColorSpan);
456 } else { // The user has stopped editing the URL text box.
457 // Move to the beginning of the string.
458 urlEditText.setSelection(0);
460 // Reapply the highlighting.
465 // Set the go button on the keyboard to load the URL in `urlTextBox`.
466 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
467 // If the event is a key-down event on the `enter` button, load the URL.
468 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
469 // Load the URL into the mainWebView and consume the event.
470 loadUrlFromTextBox();
472 // If the enter key was pressed, consume the event.
475 // If any other key was pressed, do not consume the event.
480 // Set `waitingForOrbotHTMLString`.
481 waitingForOrbotHtmlString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
483 // Initialize the Orbot status and the waiting for Orbot trackers.
484 orbotStatus = "unknown";
485 waitingForOrbot = false;
487 // Create an Orbot status `BroadcastReceiver`.
488 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
490 public void onReceive(Context context, Intent intent) {
491 // Store the content of the status message in `orbotStatus`.
492 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
494 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
495 if (orbotStatus.equals("ON") && waitingForOrbot) {
496 // Reset `waitingForOrbot`.
497 waitingForOrbot = false;
499 // Load `formattedUrlString
500 loadUrl(formattedUrlString);
505 // Register `orbotStatusBroadcastReceiver` on `this` context.
506 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
508 // Instantiate the block list helper.
509 blockListHelper = new BlockListHelper();
511 // Parse the block lists.
512 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
513 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
514 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
515 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
516 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
518 // Store the list versions.
519 easyListVersion = easyList.get(0).get(0)[0];
520 easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
521 fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
522 fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
523 ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
525 // Get handles for views that need to be modified.
526 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
527 final NavigationView navigationView = findViewById(R.id.navigationview);
528 TabLayout tabLayout = findViewById(R.id.tablayout);
529 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
530 ViewPager webViewPager = findViewById(R.id.webviewpager);
531 bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
532 bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
533 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
534 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
535 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
536 findOnPageEditText = findViewById(R.id.find_on_page_edittext);
537 fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
539 // Listen for touches on the navigation menu.
540 navigationView.setNavigationItemSelectedListener(this);
542 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
543 final Menu navigationMenu = navigationView.getMenu();
544 final MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
545 final MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
546 final MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
547 final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
548 navigationRequestsMenuItem = navigationMenu.getItem(6);
550 // Initialize the web view pager adapter.
551 webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
553 // Set the pager adapter on the web view pager.
554 webViewPager.setAdapter(webViewPagerAdapter);
556 // Store up to 100 tabs in memory.
557 webViewPager.setOffscreenPageLimit(100);
559 // Update the web view pager every time a tab is modified.
560 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
562 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
567 public void onPageSelected(int position) {
568 // Get the WebView tab fragment.
569 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(position);
571 // Get the fragment view.
572 View fragmentView = webViewTabFragment.getView();
574 // Remove the incorrect lint warning below that the fragment view might be null.
575 assert fragmentView != null;
577 // Store the current WebView.
578 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
580 // Update the privacy icons. `true` redraws the icons in the app bar.
581 updatePrivacyIcons(true);
583 // Store the current formatted URL string.
584 formattedUrlString = currentWebView.getUrl();
586 // Clear the focus from the URL text box.
587 urlEditText.clearFocus();
589 // Hide the soft keyboard.
590 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
592 // Display the current URL in the URL text box.
593 urlEditText.setText(formattedUrlString);
595 // Highlight the URL text.
598 // Set the background to indicate the domain settings status.
599 if (currentWebView.getDomainSettingsApplied()) {
600 // Set a green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
602 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
604 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
607 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
610 // 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.
611 if (tabLayout.getSelectedTabPosition() != position) {
612 // Get a handle for the corresponding tab.
613 TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
615 // Assert that the corresponding tab is not null.
616 assert correspondingTab != null;
618 // Select the corresponding tab.
619 correspondingTab.select();
624 public void onPageScrollStateChanged(int state) {
629 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
630 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
632 public void onTabSelected(TabLayout.Tab tab) {
633 // Select the same page in the view pager.
634 webViewPager.setCurrentItem(tab.getPosition());
638 public void onTabUnselected(TabLayout.Tab tab) {
643 public void onTabReselected(TabLayout.Tab tab) {
644 // Instantiate the View SSL Certificate dialog.
645 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
647 // Display the View SSL Certificate dialog.
648 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
652 // Add the first tab.
655 // 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.
656 // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
658 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
659 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
660 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
661 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
663 launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
664 createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
665 createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
666 bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
669 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
670 launchBookmarksActivityFab.setOnClickListener(v -> {
671 // Store the current WebView url and title in the bookmarks activity.
672 BookmarksActivity.currentWebViewUrl = currentWebView.getUrl();
673 BookmarksActivity.currentWebViewTitle = currentWebView.getTitle();
675 // Create an intent to launch the bookmarks activity.
676 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
678 // Include the current folder with the `Intent`.
679 bookmarksIntent.putExtra("Current Folder", currentBookmarksFolder);
682 startActivity(bookmarksIntent);
685 // Set the create new bookmark folder FAB to display an alert dialog.
686 createBookmarkFolderFab.setOnClickListener(v -> {
687 // Show the create bookmark folder dialog and name the instance `@string/create_folder`.
688 DialogFragment createBookmarkFolderDialog = new CreateBookmarkFolderDialog();
689 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
692 // Set the create new bookmark FAB to display an alert dialog.
693 createBookmarkFab.setOnClickListener(view -> {
694 // Instantiate the create bookmark dialog.
695 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), favoriteIconBitmap);
697 // Display the create bookmark dialog.
698 createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
701 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
702 findOnPageEditText.addTextChangedListener(new TextWatcher() {
704 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
709 public void onTextChanged(CharSequence s, int start, int before, int count) {
714 public void afterTextChanged(Editable s) {
715 // Search for the text in `mainWebView`.
716 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
720 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
721 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
722 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
723 // Hide the soft keyboard.
724 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
726 // Consume the event.
728 } else { // A different key was pressed.
729 // Do not consume the event.
734 // Implement swipe to refresh.
735 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
737 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
738 swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
740 // Set the swipe to refresh color according to the theme.
742 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
743 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
745 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
748 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
749 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
750 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
752 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
753 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
755 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
756 currentBookmarksFolder = "";
758 // Load the home folder, which is `""` in the database.
759 loadBookmarksFolder();
761 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
762 // Convert the id from long to int to match the format of the bookmarks database.
763 int databaseID = (int) id;
765 // Get the bookmark cursor for this ID and move it to the first row.
766 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
767 bookmarkCursor.moveToFirst();
769 // Act upon the bookmark according to the type.
770 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
771 // Store the new folder name in `currentBookmarksFolder`.
772 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
774 // Load the new folder.
775 loadBookmarksFolder();
776 } else { // The selected bookmark is not a folder.
777 // Load the bookmark URL.
778 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
780 // Close the bookmarks drawer.
781 drawerLayout.closeDrawer(GravityCompat.END);
784 // Close the `Cursor`.
785 bookmarkCursor.close();
788 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
789 // Convert the database ID from `long` to `int`.
790 int databaseId = (int) id;
792 // Find out if the selected bookmark is a folder.
793 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
796 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
797 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
799 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
800 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId);
801 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
803 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
804 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId);
805 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
808 // Consume the event.
812 // Get the status bar pixel size.
813 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
814 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
816 // Get the resource density.
817 float screenDensity = getResources().getDisplayMetrics().density;
819 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
820 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
821 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
822 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
824 // The drawer listener is used to update the navigation menu.`
825 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
827 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
831 public void onDrawerOpened(@NonNull View drawerView) {
835 public void onDrawerClosed(@NonNull View drawerView) {
839 public void onDrawerStateChanged(int newState) {
840 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
841 // Get handles for the drawer headers.
842 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
843 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
845 // 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.
846 if (navigationHeaderTextView != null) {
847 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
850 // 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.
851 if (bookmarksHeaderTextView != null) {
852 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
855 // Update the navigation menu items.
856 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
857 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
858 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
859 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
860 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
862 // Hide the keyboard (if displayed).
863 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
865 // 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.
866 urlEditText.clearFocus();
867 currentWebView.clearFocus();
872 // Create the hamburger icon at the start of the AppBar.
873 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
875 // Initialize cookieManager.
876 cookieManager = CookieManager.getInstance();
878 // 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).
879 customHeaders.put("X-Requested-With", "");
881 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
882 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
884 // Get a handle for the `Runtime`.
885 privacyBrowserRuntime = Runtime.getRuntime();
887 // Store the application's private data directory.
888 privateDataDirectoryString = getApplicationInfo().dataDir;
889 // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
891 // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
892 inFullScreenBrowsingMode = false;
894 // Initialize the privacy settings variables.
895 firstPartyCookiesEnabled = false;
896 saveFormDataEnabled = false; // Form data can be removed once the minimum API >= 26.
899 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
900 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
902 // Get a handle for the WebView.
903 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
905 // Store the default user agent.
906 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
908 // Destroy the bare WebView.
909 bareWebView.destroy();
911 // Initialize the favorite icon bitmap. `ContextCompat` must be used until API >= 21.
912 Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
913 BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
914 assert favoriteIconBitmapDrawable != null;
915 favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
917 // If the favorite icon is null, load the default.
918 if (favoriteIconBitmap == null) {
919 favoriteIconBitmap = favoriteIconDefaultBitmap;
922 // Initialize the user agent array adapter and string array.
923 userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
924 userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
926 // Get the intent that started the app.
927 Intent launchingIntent = getIntent();
929 // Get the information from the intent.
930 String launchingIntentAction = launchingIntent.getAction();
931 Uri launchingIntentUriData = launchingIntent.getData();
933 // If the intent action is a web search, perform the search.
934 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
935 // Create an encoded URL string.
936 String encodedUrlString;
938 // Sanitize the search input and convert it to a search.
940 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
941 } catch (UnsupportedEncodingException exception) {
942 encodedUrlString = "";
945 // Add the base search URL.
946 formattedUrlString = searchURL + encodedUrlString;
947 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
948 // Set the formatted URL string.
949 formattedUrlString = launchingIntentUriData.toString();
954 protected void onNewIntent(Intent intent) {
958 // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
961 // Get the information from the intent.
962 String intentAction = intent.getAction();
963 Uri intentUriData = intent.getData();
965 // If the intent action is a web search, perform the search.
966 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
967 // Create an encoded URL string.
968 String encodedUrlString;
970 // Sanitize the search input and convert it to a search.
972 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
973 } catch (UnsupportedEncodingException exception) {
974 encodedUrlString = "";
977 // Add the base search URL.
978 formattedUrlString = searchURL + encodedUrlString;
979 } else if (intentUriData != null){ // Check to see if the intent contains a new URL.
980 // Set the formatted URL string.
981 formattedUrlString = intentUriData.toString();
985 loadUrl(formattedUrlString);
987 // Get a handle for the drawer layout.
988 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
990 // Close the navigation drawer if it is open.
991 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
992 drawerLayout.closeDrawer(GravityCompat.START);
995 // Close the bookmarks drawer if it is open.
996 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
997 drawerLayout.closeDrawer(GravityCompat.END);
1000 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
1001 currentWebView.requestFocus();
1005 public void onRestart() {
1006 // Run the default commands.
1009 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
1010 if (proxyThroughOrbot) {
1011 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
1012 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
1014 // Send the intent to the Orbot package.
1015 orbotIntent.setPackage("org.torproject.android");
1018 sendBroadcast(orbotIntent);
1021 // Apply the app settings if returning from the Settings activity.
1022 if (reapplyAppSettingsOnRestart) {
1023 // Apply the app settings.
1026 // Reload the webpage if displaying of images has been disabled in the Settings activity.
1027 if (reloadOnRestart) {
1028 // Reload the WebViews.
1029 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1030 // Get the WebView tab fragment.
1031 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1033 // Get the fragment view.
1034 View fragmentView = webViewTabFragment.getView();
1036 // Only reload the WebViews if they exist.
1037 if (fragmentView != null) {
1038 // Get the nested scroll WebView from the tab fragment.
1039 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1041 // Reload the WebView. This doesn't seem to work if for WebViews that aren't visible.
1042 nestedScrollWebView.reload();
1046 // Reset `reloadOnRestartBoolean`.
1047 reloadOnRestart = false;
1050 // Reset the return from settings flag.
1051 reapplyAppSettingsOnRestart = false;
1054 // TODO apply to all the tabs.
1055 // Apply the domain settings if returning from the Domains activity.
1056 if (reapplyDomainSettingsOnRestart) {
1057 // Reapply the domain settings.
1058 applyDomainSettings(currentWebView, formattedUrlString, false, true);
1060 // Reset `reapplyDomainSettingsOnRestart`.
1061 reapplyDomainSettingsOnRestart = false;
1064 // Load the URL on restart to apply changes to night mode.
1065 if (loadUrlOnRestart) {
1066 // Load the current `formattedUrlString`.
1067 loadUrl(formattedUrlString);
1069 // Reset `loadUrlOnRestart.
1070 loadUrlOnRestart = false;
1073 // Update the bookmarks drawer if returning from the Bookmarks activity.
1074 if (restartFromBookmarksActivity) {
1075 // Get a handle for the drawer layout.
1076 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1078 // Close the bookmarks drawer.
1079 drawerLayout.closeDrawer(GravityCompat.END);
1081 // Reload the bookmarks drawer.
1082 loadBookmarksFolder();
1084 // Reset `restartFromBookmarksActivity`.
1085 restartFromBookmarksActivity = false;
1088 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
1089 updatePrivacyIcons(true);
1092 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
1094 public void onResume() {
1095 // Run the default commands.
1098 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1099 // Get the WebView tab fragment.
1100 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1102 // Get the fragment view.
1103 View fragmentView = webViewTabFragment.getView();
1105 // Only resume the WebViews if they exist (they won't when the app is first created).
1106 if (fragmentView != null) {
1107 // Get the nested scroll WebView from the tab fragment.
1108 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1110 // Resume the nested scroll WebView JavaScript timers.
1111 nestedScrollWebView.resumeTimers();
1113 // Resume the nested scroll WebView.
1114 nestedScrollWebView.onResume();
1118 // Display a message to the user if waiting for Orbot.
1119 if (waitingForOrbot && !orbotStatus.equals("ON")) {
1120 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
1121 currentWebView.getSettings().setUseWideViewPort(false);
1123 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
1124 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
1127 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
1128 // Get a handle for the root frame layouts.
1129 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1131 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1132 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1134 /* Hide the system bars.
1135 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1136 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1137 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1138 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1140 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1141 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1142 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
1144 AdHelper.resumeAd(findViewById(R.id.adview));
1149 public void onPause() {
1150 // Run the default commands.
1153 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1154 // Get the WebView tab fragment.
1155 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1157 // Get the fragment view.
1158 View fragmentView = webViewTabFragment.getView();
1160 // Only pause the WebViews if they exist (they won't when the app is first created).
1161 if (fragmentView != null) {
1162 // Get the nested scroll WebView from the tab fragment.
1163 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1165 // Pause the nested scroll WebView.
1166 nestedScrollWebView.onPause();
1168 // Pause the nested scroll WebView JavaScript timers.
1169 nestedScrollWebView.pauseTimers();
1173 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1174 if (BuildConfig.FLAVOR.contentEquals("free")) {
1176 AdHelper.pauseAd(findViewById(R.id.adview));
1181 public void onDestroy() {
1182 // Unregister the Orbot status broadcast receiver.
1183 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1185 // Close the bookmarks cursor and database.
1186 bookmarksCursor.close();
1187 bookmarksDatabaseHelper.close();
1189 // Run the default commands.
1194 public boolean onCreateOptionsMenu(Menu menu) {
1195 // Inflate the menu. This adds items to the action bar if it is present.
1196 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1198 // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
1201 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1202 updatePrivacyIcons(false);
1204 // Get handles for the menu items.
1205 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1206 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1207 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1208 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1209 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1210 refreshMenuItem = menu.findItem(R.id.refresh);
1211 blocklistsMenuItem = menu.findItem(R.id.blocklists);
1212 easyListMenuItem = menu.findItem(R.id.easylist);
1213 easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1214 fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1215 fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1216 ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1217 blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1218 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1220 // Only display third-party cookies if API >= 21
1221 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1223 // Only display the form data menu items if the API < 26.
1224 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1225 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1227 // Only show Ad Consent if this is the free flavor.
1228 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1230 // Get the shared preference values.
1231 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1233 // Get the status of the additional AppBar icons.
1234 displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1236 // 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.
1237 if (displayAdditionalAppBarIcons) {
1238 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1239 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1240 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1241 } else { //Do not display the additional icons.
1242 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1243 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1244 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1247 // Replace Refresh with Stop if a URL is already loading.
1248 if (currentWebView != null && currentWebView.getProgress() != 100) {
1250 refreshMenuItem.setTitle(R.string.stop);
1252 // If the icon is displayed in the AppBar, set it according to the theme.
1253 if (displayAdditionalAppBarIcons) {
1255 refreshMenuItem.setIcon(R.drawable.close_dark);
1257 refreshMenuItem.setIcon(R.drawable.close_light);
1266 public boolean onPrepareOptionsMenu(Menu menu) {
1267 // Get a handle for the swipe refresh layout.
1268 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1270 // Get handles for the menu items.
1271 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1272 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1273 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1274 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1275 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1276 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1277 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1278 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1279 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1280 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1281 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1282 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1283 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1284 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1286 // Initialize the current user agent string and the font size.
1287 String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1290 // 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.
1291 if (currentWebView != null) {
1292 // Set the add or edit domain text.
1293 if (currentWebView.getDomainSettingsApplied()) {
1294 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1296 addOrEditDomain.setTitle(R.string.add_domain_settings);
1299 // Get the current user agent from the WebView.
1300 currentUserAgent = currentWebView.getSettings().getUserAgentString();
1302 // Get the current font size from the
1303 fontSize = currentWebView.getSettings().getTextZoom();
1305 // Set the status of the menu item checkboxes.
1306 toggleDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1307 easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1308 easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1309 fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1310 fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1311 ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1312 blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1313 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1315 // Initialize the display names for the blocklists with the number of blocked requests.
1316 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1317 easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1318 easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1319 fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1320 fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1321 ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1322 blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1325 // Set the status of the menu item checkboxes.
1326 toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
1327 toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); // Form data can be removed once the minimum API >= 26.
1328 swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
1329 nightModeMenuItem.setChecked(nightMode);
1330 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1332 // Only modify third-party cookies if the API >= 21.
1333 if (Build.VERSION.SDK_INT >= 21) {
1334 // Set the status of the third-party cookies checkbox.
1335 toggleThirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1337 // Enable third-party cookies if first-party cookies are enabled.
1338 toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
1341 // Enable DOM Storage if JavaScript is enabled.
1342 toggleDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1344 // Enable Clear Cookies if there are any.
1345 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1347 // Get a count of the number of files in the Local Storage directory.
1348 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1349 int localStorageDirectoryNumberOfFiles = 0;
1350 if (localStorageDirectory.exists()) {
1351 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1354 // Get a count of the number of files in the IndexedDB directory.
1355 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1356 int indexedDBDirectoryNumberOfFiles = 0;
1357 if (indexedDBDirectory.exists()) {
1358 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1361 // Enable Clear DOM Storage if there is any.
1362 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1364 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1365 if (Build.VERSION.SDK_INT < 26) {
1366 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
1367 clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
1369 // Disable clear form data because it is not supported on current version of Android.
1370 clearFormDataMenuItem.setEnabled(false);
1373 // Enable Clear Data if any of the submenu items are enabled.
1374 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1376 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1377 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1379 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1380 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1381 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1382 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1383 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1384 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1385 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1386 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1387 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1388 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1389 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1390 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1391 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1392 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1393 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1394 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1395 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1396 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1397 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1398 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1399 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1400 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1401 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1402 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1403 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1404 } else { // Custom user agent.
1405 menu.findItem(R.id.user_agent_custom).setChecked(true);
1408 // Instantiate the font size title and the selected font size menu item.
1409 String fontSizeTitle;
1410 MenuItem selectedFontSizeMenuItem;
1412 // Prepare the font size title and current size menu item.
1415 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1416 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1420 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1421 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1425 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1426 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1430 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1431 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1435 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1436 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1440 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1441 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1445 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1446 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1450 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1451 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1455 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1456 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1460 // Set the font size title and select the current size menu item.
1461 fontSizeMenuItem.setTitle(fontSizeTitle);
1462 selectedFontSizeMenuItem.setChecked(true);
1464 // Run all the other default commands.
1465 super.onPrepareOptionsMenu(menu);
1467 // Display the menu.
1472 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1473 @SuppressLint("SetJavaScriptEnabled")
1474 // removeAllCookies is deprecated, but it is required for API < 21.
1475 @SuppressWarnings("deprecation")
1476 public boolean onOptionsItemSelected(MenuItem menuItem) {
1477 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1478 if (inFullScreenBrowsingMode) {
1479 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1480 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1482 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1484 /* Hide the system bars.
1485 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1486 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1487 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1488 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1490 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1491 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1494 // Get the selected menu item ID.
1495 int menuItemId = menuItem.getItemId();
1497 // Get a handle for the shared preferences.
1498 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1500 // Run the commands that correlate to the selected menu item.
1501 switch (menuItemId) {
1502 case R.id.toggle_javascript:
1503 // Toggle the JavaScript status.
1504 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1506 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1507 updatePrivacyIcons(true);
1509 // Display a `Snackbar`.
1510 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
1511 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1512 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled.
1513 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1514 } else { // Privacy mode.
1515 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1518 // Reload the current WebView.
1519 currentWebView.reload();
1522 case R.id.add_or_edit_domain:
1523 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1524 // Reapply the domain settings on returning to `MainWebViewActivity`.
1525 reapplyDomainSettingsOnRestart = true;
1526 currentWebView.resetCurrentDomainName();
1528 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
1529 // Store the current SSL certificate and IP addresses in the domains activity.
1530 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1531 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1533 // Create an intent to launch the domains activity.
1534 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1536 // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
1537 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1538 domainsIntent.putExtra("close_on_back", true);
1541 startActivity(domainsIntent);
1542 } else { // Add a new domain.
1543 // Apply the new domain settings on returning to `MainWebViewActivity`.
1544 reapplyDomainSettingsOnRestart = true;
1545 currentWebView.resetCurrentDomainName();
1547 // Get the current domain
1548 Uri currentUri = Uri.parse(formattedUrlString);
1549 String currentDomain = currentUri.getHost();
1551 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1552 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1554 // Create the domain and store the database ID.
1555 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1557 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
1558 // Store the current SSL certificate and IP addresses in the domains activity.
1559 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1560 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1562 // Create an intent to launch the domains activity.
1563 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1565 // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
1566 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1567 domainsIntent.putExtra("close_on_back", true);
1570 startActivity(domainsIntent);
1574 case R.id.toggle_first_party_cookies:
1575 // Switch the status of firstPartyCookiesEnabled.
1576 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
1578 // Update the menu checkbox.
1579 menuItem.setChecked(firstPartyCookiesEnabled);
1581 // Apply the new cookie status.
1582 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1584 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1585 updatePrivacyIcons(true);
1587 // Display a `Snackbar`.
1588 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
1589 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1590 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
1591 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1592 } else { // Privacy mode.
1593 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1596 // Reload the current WebView.
1597 currentWebView.reload();
1600 case R.id.toggle_third_party_cookies:
1601 if (Build.VERSION.SDK_INT >= 21) {
1602 // Switch the status of thirdPartyCookiesEnabled.
1603 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1605 // Update the menu checkbox.
1606 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1608 // Display a snackbar.
1609 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1610 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1612 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1615 // Reload the current WebView.
1616 currentWebView.reload();
1617 } // Else do nothing because SDK < 21.
1620 case R.id.toggle_dom_storage:
1621 // Toggle the status of domStorageEnabled.
1622 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1624 // Update the menu checkbox.
1625 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1627 // Update the privacy icon. `true` refreshes the app bar icons.
1628 updatePrivacyIcons(true);
1630 // Display a snackbar.
1631 if (currentWebView.getSettings().getDomStorageEnabled()) {
1632 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1634 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1637 // Reload the current WebView.
1638 currentWebView.reload();
1641 // Form data can be removed once the minimum API >= 26.
1642 case R.id.toggle_save_form_data:
1643 // Switch the status of saveFormDataEnabled.
1644 saveFormDataEnabled = !saveFormDataEnabled;
1646 // Update the menu checkbox.
1647 menuItem.setChecked(saveFormDataEnabled);
1649 // Apply the new form data status.
1650 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
1652 // Display a `Snackbar`.
1653 if (saveFormDataEnabled) {
1654 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1656 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1659 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1660 updatePrivacyIcons(true);
1662 // Reload the current WebView.
1663 currentWebView.reload();
1666 case R.id.clear_cookies:
1667 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1668 .setAction(R.string.undo, v -> {
1669 // Do nothing because everything will be handled by `onDismissed()` below.
1671 .addCallback(new Snackbar.Callback() {
1672 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1674 public void onDismissed(Snackbar snackbar, int event) {
1676 // The user pushed the undo button.
1677 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1681 // The snackbar was dismissed without the undo button being pushed.
1683 // `cookieManager.removeAllCookie()` varies by SDK.
1684 if (Build.VERSION.SDK_INT < 21) {
1685 cookieManager.removeAllCookie();
1687 cookieManager.removeAllCookies(null);
1695 case R.id.clear_dom_storage:
1696 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1697 .setAction(R.string.undo, v -> {
1698 // Do nothing because everything will be handled by `onDismissed()` below.
1700 .addCallback(new Snackbar.Callback() {
1701 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1703 public void onDismissed(Snackbar snackbar, int event) {
1705 // The user pushed the undo button.
1706 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1710 // The snackbar was dismissed without the undo button being pushed.
1712 // Delete the DOM Storage.
1713 WebStorage webStorage = WebStorage.getInstance();
1714 webStorage.deleteAllData();
1716 // Initialize a handler to manually delete the DOM storage files and directories.
1717 Handler deleteDomStorageHandler = new Handler();
1719 // Setup a runnable to manually delete the DOM storage files and directories.
1720 Runnable deleteDomStorageRunnable = () -> {
1722 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1723 Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1725 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1726 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1727 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1728 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1729 Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1731 // Wait for the processes to finish.
1732 deleteLocalStorageProcess.waitFor();
1733 deleteIndexProcess.waitFor();
1734 deleteQuotaManagerProcess.waitFor();
1735 deleteQuotaManagerJournalProcess.waitFor();
1736 deleteDatabasesProcess.waitFor();
1737 } catch (Exception exception) {
1738 // Do nothing if an error is thrown.
1742 // Manually delete the DOM storage files after 200 milliseconds.
1743 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1750 // Form data can be remove once the minimum API >= 26.
1751 case R.id.clear_form_data:
1752 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1753 .setAction(R.string.undo, v -> {
1754 // Do nothing because everything will be handled by `onDismissed()` below.
1756 .addCallback(new Snackbar.Callback() {
1757 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1759 public void onDismissed(Snackbar snackbar, int event) {
1761 // The user pushed the undo button.
1762 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1766 // The snackbar was dismissed without the `Undo` button being pushed.
1768 // Delete the form data.
1769 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1770 mainWebViewDatabase.clearFormData();
1778 // Toggle the EasyList status.
1779 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1781 // Update the menu checkbox.
1782 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1784 // Reload the current WebView.
1785 currentWebView.reload();
1788 case R.id.easyprivacy:
1789 // Toggle the EasyPrivacy status.
1790 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1792 // Update the menu checkbox.
1793 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1795 // Reload the current WebView.
1796 currentWebView.reload();
1799 case R.id.fanboys_annoyance_list:
1800 // Toggle Fanboy's Annoyance List status.
1801 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1803 // Update the menu checkbox.
1804 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1806 // Update the staus of Fanboy's Social Blocking List.
1807 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1808 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1810 // Reload the current WebView.
1811 currentWebView.reload();
1814 case R.id.fanboys_social_blocking_list:
1815 // Toggle Fanboy's Social Blocking List status.
1816 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1818 // Update the menu checkbox.
1819 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1821 // Reload the current WebView.
1822 currentWebView.reload();
1825 case R.id.ultraprivacy:
1826 // Toggle the UltraPrivacy status.
1827 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1829 // Update the menu checkbox.
1830 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1832 // Reload the current WebView.
1833 currentWebView.reload();
1836 case R.id.block_all_third_party_requests:
1837 //Toggle the third-party requests blocker status.
1838 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1840 // Update the menu checkbox.
1841 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1843 // Reload the current WebView.
1844 currentWebView.reload();
1847 case R.id.user_agent_privacy_browser:
1848 // Update the user agent.
1849 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1851 // Reload the current WebView.
1852 currentWebView.reload();
1855 case R.id.user_agent_webview_default:
1856 // Update the user agent.
1857 currentWebView.getSettings().setUserAgentString("");
1859 // Reload the current WebView.
1860 currentWebView.reload();
1863 case R.id.user_agent_firefox_on_android:
1864 // Update the user agent.
1865 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1867 // Reload the current WebView.
1868 currentWebView.reload();
1871 case R.id.user_agent_chrome_on_android:
1872 // Update the user agent.
1873 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1875 // Reload the current WebView.
1876 currentWebView.reload();
1879 case R.id.user_agent_safari_on_ios:
1880 // Update the user agent.
1881 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1883 // Reload the current WebView.
1884 currentWebView.reload();
1887 case R.id.user_agent_firefox_on_linux:
1888 // Update the user agent.
1889 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1891 // Reload the current WebView.
1892 currentWebView.reload();
1895 case R.id.user_agent_chromium_on_linux:
1896 // Update the user agent.
1897 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1899 // Reload the current WebView.
1900 currentWebView.reload();
1903 case R.id.user_agent_firefox_on_windows:
1904 // Update the user agent.
1905 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1907 // Reload the current WebView.
1908 currentWebView.reload();
1911 case R.id.user_agent_chrome_on_windows:
1912 // Update the user agent.
1913 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1915 // Reload the current WebView.
1916 currentWebView.reload();
1919 case R.id.user_agent_edge_on_windows:
1920 // Update the user agent.
1921 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1923 // Reload the current WebView.
1924 currentWebView.reload();
1927 case R.id.user_agent_internet_explorer_on_windows:
1928 // Update the user agent.
1929 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1931 // Reload the current WebView.
1932 currentWebView.reload();
1935 case R.id.user_agent_safari_on_macos:
1936 // Update the user agent.
1937 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1939 // Reload the current WebView.
1940 currentWebView.reload();
1943 case R.id.user_agent_custom:
1944 // Update the user agent.
1945 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1947 // Reload the current WebView.
1948 currentWebView.reload();
1951 case R.id.font_size_twenty_five_percent:
1952 currentWebView.getSettings().setTextZoom(25);
1955 case R.id.font_size_fifty_percent:
1956 currentWebView.getSettings().setTextZoom(50);
1959 case R.id.font_size_seventy_five_percent:
1960 currentWebView.getSettings().setTextZoom(75);
1963 case R.id.font_size_one_hundred_percent:
1964 currentWebView.getSettings().setTextZoom(100);
1967 case R.id.font_size_one_hundred_twenty_five_percent:
1968 currentWebView.getSettings().setTextZoom(125);
1971 case R.id.font_size_one_hundred_fifty_percent:
1972 currentWebView.getSettings().setTextZoom(150);
1975 case R.id.font_size_one_hundred_seventy_five_percent:
1976 currentWebView.getSettings().setTextZoom(175);
1979 case R.id.font_size_two_hundred_percent:
1980 currentWebView.getSettings().setTextZoom(200);
1983 case R.id.swipe_to_refresh:
1984 // Get a handle for the swipe refresh layout.
1985 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1987 // Toggle swipe to refresh.
1988 swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
1991 case R.id.display_images:
1992 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
1993 // Disable loading of images.
1994 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1996 // Reload the website to remove existing images.
1997 currentWebView.reload();
1998 } else { // Images are not currently loaded automatically.
1999 // Enable loading of images. Missing images will be loaded without the need for a reload.
2000 currentWebView.getSettings().setLoadsImagesAutomatically(true);
2004 case R.id.night_mode:
2005 // Toggle night mode.
2006 nightMode = !nightMode;
2008 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
2009 if (nightMode) { // Night mode is enabled, which requires JavaScript.
2010 // Enable JavaScript.
2011 currentWebView.getSettings().setJavaScriptEnabled(true);
2012 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
2013 // Apply the JavaScript preference that was stored the last time domain settings were loaded.
2014 currentWebView.getSettings().setJavaScriptEnabled(domainSettingsJavaScriptEnabled);
2015 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
2016 // Apply the JavaScript preference.
2017 currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
2020 // Update the privacy icons.
2021 updatePrivacyIcons(false);
2023 // Reload the website.
2024 currentWebView.reload();
2027 case R.id.find_on_page:
2028 // Get a handle for the views.
2029 Toolbar toolbar = findViewById(R.id.toolbar);
2030 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2032 // Hide the toolbar.
2033 toolbar.setVisibility(View.GONE);
2035 // Show the find on page linear layout.
2036 findOnPageLinearLayout.setVisibility(View.VISIBLE);
2038 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
2039 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
2040 findOnPageEditText.postDelayed(() -> {
2041 // Set the focus on `findOnPageEditText`.
2042 findOnPageEditText.requestFocus();
2044 // Display the keyboard. `0` sets no input flags.
2045 inputMethodManager.showSoftInput(findOnPageEditText, 0);
2049 case R.id.view_source:
2050 // Create an intent to launch the view source activity.
2051 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
2053 // Add the user agent as an extra to the intent.
2054 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
2057 startActivity(viewSourceIntent);
2060 case R.id.share_url:
2061 // Setup the share string.
2062 String shareString = currentWebView.getTitle() + " – " + formattedUrlString;
2064 // Create the share intent.
2065 Intent shareIntent = new Intent(Intent.ACTION_SEND);
2066 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2067 shareIntent.setType("text/plain");
2070 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2074 // Get a `PrintManager` instance.
2075 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2077 // Create a print document adapter form the current WebView.
2078 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2080 // Remove the lint error below that `printManager` might be `null`.
2081 assert printManager != null;
2083 // Print the document. The print attributes are `null`.
2084 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2087 case R.id.open_with_app:
2088 openWithApp(formattedUrlString);
2091 case R.id.open_with_browser:
2092 openWithBrowser(formattedUrlString);
2095 case R.id.add_to_homescreen:
2096 // Instantiate the create home screen shortcut dialog.
2097 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, favoriteIconBitmap);
2099 // Show the create home screen shortcut dialog.
2100 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2103 case R.id.proxy_through_orbot:
2104 // Toggle the proxy through Orbot variable.
2105 proxyThroughOrbot = !proxyThroughOrbot;
2107 // Apply the proxy through Orbot settings.
2108 applyProxyThroughOrbot(true);
2112 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2113 // Reload the current WebView.
2114 currentWebView.reload();
2115 } else { // The stop button was pushed.
2116 // Stop the loading of the WebView.
2117 currentWebView.stopLoading();
2121 case R.id.ad_consent:
2122 // Display the ad consent dialog.
2123 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2124 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2128 // Don't consume the event.
2129 return super.onOptionsItemSelected(menuItem);
2133 // removeAllCookies is deprecated, but it is required for API < 21.
2134 @SuppressWarnings("deprecation")
2136 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2137 // Get the menu item ID.
2138 int menuItemId = menuItem.getItemId();
2140 // Run the commands that correspond to the selected menu item.
2141 switch (menuItemId) {
2142 case R.id.close_tab:
2143 // Get a handle for the tab layout.
2144 TabLayout tabLayout = findViewById(R.id.tablayout);
2146 // Get the current tab number.
2147 int currentTabNumber = tabLayout.getSelectedTabPosition();
2149 // Delete the current tab.
2150 tabLayout.removeTabAt(currentTabNumber);
2152 // Delete the current page.
2153 webViewPagerAdapter.deletePage(currentTabNumber);
2156 case R.id.clear_and_exit:
2157 // Close the bookmarks cursor and database.
2158 bookmarksCursor.close();
2159 bookmarksDatabaseHelper.close();
2161 // Get a handle for the shared preferences.
2162 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2164 // Get the status of the clear everything preference.
2165 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2168 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2169 // The command to remove cookies changed slightly in API 21.
2170 if (Build.VERSION.SDK_INT >= 21) {
2171 cookieManager.removeAllCookies(null);
2173 cookieManager.removeAllCookie();
2176 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2178 // Two commands must be used because `Runtime.exec()` does not like `*`.
2179 Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2180 Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2182 // Wait until the processes have finished.
2183 deleteCookiesProcess.waitFor();
2184 deleteCookiesJournalProcess.waitFor();
2185 } catch (Exception exception) {
2186 // Do nothing if an error is thrown.
2190 // Clear DOM storage.
2191 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2192 // Ask `WebStorage` to clear the DOM storage.
2193 WebStorage webStorage = WebStorage.getInstance();
2194 webStorage.deleteAllData();
2196 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2198 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2199 Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2201 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2202 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2203 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2204 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2205 Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2207 // Wait until the processes have finished.
2208 deleteLocalStorageProcess.waitFor();
2209 deleteIndexProcess.waitFor();
2210 deleteQuotaManagerProcess.waitFor();
2211 deleteQuotaManagerJournalProcess.waitFor();
2212 deleteDatabaseProcess.waitFor();
2213 } catch (Exception exception) {
2214 // Do nothing if an error is thrown.
2218 // Clear form data if the API < 26.
2219 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2220 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2221 webViewDatabase.clearFormData();
2223 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2225 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2226 Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2227 Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2229 // Wait until the processes have finished.
2230 deleteWebDataProcess.waitFor();
2231 deleteWebDataJournalProcess.waitFor();
2232 } catch (Exception exception) {
2233 // Do nothing if an error is thrown.
2238 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2239 // Clear the cache from each WebView.
2240 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2241 // Get the WebView tab fragment.
2242 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2244 // Get the fragment view.
2245 View fragmentView = webViewTabFragment.getView();
2247 // Only clear the cache if the WebView exists.
2248 if (fragmentView != null) {
2249 // Get the nested scroll WebView from the tab fragment.
2250 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2252 // Clear the cache for this WebView.
2253 nestedScrollWebView.clearCache(true);
2257 // Manually delete the cache directories.
2259 // Delete the main cache directory.
2260 Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2262 // Delete the secondary `Service Worker` cache directory.
2263 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2264 Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2266 // Wait until the processes have finished.
2267 deleteCacheProcess.waitFor();
2268 deleteServiceWorkerProcess.waitFor();
2269 } catch (Exception exception) {
2270 // Do nothing if an error is thrown.
2274 // Wipe out each WebView.
2275 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2276 // Get the WebView tab fragment.
2277 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2279 // Get the fragment view.
2280 View fragmentView = webViewTabFragment.getView();
2282 // Only wipe out the WebView if it exists.
2283 if (fragmentView != null) {
2284 // Get the nested scroll WebView from the tab fragment.
2285 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2287 // Clear SSL certificate preferences for this WebView.
2288 nestedScrollWebView.clearSslPreferences();
2290 // Clear the back/forward history for this WebView.
2291 nestedScrollWebView.clearHistory();
2293 // Destroy the internal state of `mainWebView`.
2294 nestedScrollWebView.destroy();
2298 // Clear the formatted URL string.
2299 formattedUrlString = null;
2301 // Clear the custom headers.
2302 customHeaders.clear();
2304 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2305 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2306 if (clearEverything) {
2308 // Delete the folder.
2309 Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2311 // Wait until the process has finished.
2312 deleteAppWebviewProcess.waitFor();
2313 } catch (Exception exception) {
2314 // Do nothing if an error is thrown.
2318 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2319 if (Build.VERSION.SDK_INT >= 21) {
2320 finishAndRemoveTask();
2325 // Remove the terminated program from RAM. The status code is `0`.
2334 if (currentWebView.canGoBack()) {
2335 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2336 formattedUrlString = "";
2338 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2339 navigatingHistory = true;
2341 // Load the previous website in the history.
2342 currentWebView.goBack();
2347 if (currentWebView.canGoForward()) {
2348 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2349 formattedUrlString = "";
2351 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2352 navigatingHistory = true;
2354 // Load the next website in the history.
2355 currentWebView.goForward();
2360 // Get the `WebBackForwardList`.
2361 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2363 // Show the URL history dialog and name this instance `R.string.history`.
2364 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2365 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2369 // Populate the resource requests.
2370 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2372 // Create an intent to launch the Requests activity.
2373 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2375 // Add the block third-party requests status to the intent.
2376 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2379 startActivity(requestsIntent);
2382 case R.id.downloads:
2383 // Launch the system Download Manager.
2384 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2386 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2387 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2389 startActivity(downloadManagerIntent);
2393 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2394 reapplyDomainSettingsOnRestart = true;
2395 currentWebView.resetCurrentDomainName(); // TODO. Do this for all tabs.
2397 // TODO. Move these to `putExtra`. The certificate can be stored as strings.
2398 // Store the current SSL certificate and IP addresses in the domains activity.
2399 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
2400 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
2402 // Launch the domains activity.
2403 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2404 startActivity(domainsIntent);
2408 // Set the flag to reapply app settings on restart when returning from Settings.
2409 reapplyAppSettingsOnRestart = true;
2411 // Set the flag to reapply the domain settings on restart when returning from Settings.
2412 reapplyDomainSettingsOnRestart = true;
2413 currentWebView.resetCurrentDomainName(); // TODO. Do this for all tabs.
2415 // Launch the settings activity.
2416 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2417 startActivity(settingsIntent);
2420 case R.id.import_export:
2421 // Launch the import/export activity.
2422 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2423 startActivity(importExportIntent);
2427 // Launch the logcat activity.
2428 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2429 startActivity(logcatIntent);
2433 // Launch `GuideActivity`.
2434 Intent guideIntent = new Intent(this, GuideActivity.class);
2435 startActivity(guideIntent);
2439 // Launch `AboutActivity`.
2440 Intent aboutIntent = new Intent(this, AboutActivity.class);
2441 startActivity(aboutIntent);
2445 // Get a handle for the drawer layout.
2446 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2448 // Close the navigation drawer.
2449 drawerLayout.closeDrawer(GravityCompat.START);
2454 public void onPostCreate(Bundle savedInstanceState) {
2455 // Run the default commands.
2456 super.onPostCreate(savedInstanceState);
2458 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2459 actionBarDrawerToggle.syncState();
2463 public void onConfigurationChanged(Configuration newConfig) {
2464 // Run the default commands.
2465 super.onConfigurationChanged(newConfig);
2467 // Get the status bar pixel size.
2468 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2469 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2471 // Get the resource density.
2472 float screenDensity = getResources().getDisplayMetrics().density;
2474 // Recalculate the drawer header padding.
2475 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2476 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2477 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2479 // Reload the ad for the free flavor if not in full screen mode.
2480 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2481 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2482 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2485 // `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:
2486 // https://code.google.com/p/android/issues/detail?id=20493#c8
2487 // ActivityCompat.invalidateOptionsMenu(this);
2491 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2492 // Store the `HitTestResult`.
2493 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2496 final String imageUrl;
2497 final String linkUrl;
2499 // Get a handle for the the clipboard and fragment managers.
2500 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2501 FragmentManager fragmentManager = getSupportFragmentManager();
2503 // Remove the lint errors below that `clipboardManager` might be `null`.
2504 assert clipboardManager != null;
2506 switch (hitTestResult.getType()) {
2507 // `SRC_ANCHOR_TYPE` is a link.
2508 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2509 // Get the target URL.
2510 linkUrl = hitTestResult.getExtra();
2512 // Set the target URL as the title of the `ContextMenu`.
2513 menu.setHeaderTitle(linkUrl);
2515 // Add a Load URL entry.
2516 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
2521 // Add a Copy URL entry.
2522 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2523 // Save the link URL in a `ClipData`.
2524 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2526 // Set the `ClipData` as the clipboard's primary clip.
2527 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2531 // Add a Download URL entry.
2532 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2533 // Check if the download should be processed by an external app.
2534 if (downloadWithExternalApp) { // Download with an external app.
2535 openUrlWithExternalApp(linkUrl);
2536 } else { // Download with Android's download manager.
2537 // Check to see if the storage permission has already been granted.
2538 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2539 // Store the variables for future use by `onRequestPermissionsResult()`.
2540 downloadUrl = linkUrl;
2541 downloadContentDisposition = "none";
2542 downloadContentLength = -1;
2544 // Show a dialog if the user has previously denied the permission.
2545 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2546 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2547 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2549 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2550 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2551 } else { // Show the permission request directly.
2552 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2553 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2555 } else { // The storage permission has already been granted.
2556 // Get a handle for the download file alert dialog.
2557 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2559 // Show the download file alert dialog.
2560 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2566 // Add an Open with App entry.
2567 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2568 openWithApp(linkUrl);
2572 // Add an Open with Browser entry.
2573 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2574 openWithBrowser(linkUrl);
2578 // Add a Cancel entry, which by default closes the context menu.
2579 menu.add(R.string.cancel);
2582 case WebView.HitTestResult.EMAIL_TYPE:
2583 // Get the target URL.
2584 linkUrl = hitTestResult.getExtra();
2586 // Set the target URL as the title of the `ContextMenu`.
2587 menu.setHeaderTitle(linkUrl);
2589 // Add a Write Email entry.
2590 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2591 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2592 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2594 // Parse the url and set it as the data for the `Intent`.
2595 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2597 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2598 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2601 startActivity(emailIntent);
2605 // Add a Copy Email Address entry.
2606 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2607 // Save the email address in a `ClipData`.
2608 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2610 // Set the `ClipData` as the clipboard's primary clip.
2611 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2615 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2616 menu.add(R.string.cancel);
2619 // `IMAGE_TYPE` is an image.
2620 case WebView.HitTestResult.IMAGE_TYPE:
2621 // Get the image URL.
2622 imageUrl = hitTestResult.getExtra();
2624 // Set the image URL as the title of the `ContextMenu`.
2625 menu.setHeaderTitle(imageUrl);
2627 // Add a View Image entry.
2628 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2633 // Add a Download Image entry.
2634 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2635 // Check if the download should be processed by an external app.
2636 if (downloadWithExternalApp) { // Download with an external app.
2637 openUrlWithExternalApp(imageUrl);
2638 } else { // Download with Android's download manager.
2639 // Check to see if the storage permission has already been granted.
2640 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2641 // Store the image URL for use by `onRequestPermissionResult()`.
2642 downloadImageUrl = imageUrl;
2644 // Show a dialog if the user has previously denied the permission.
2645 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2646 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2647 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2649 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2650 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2651 } else { // Show the permission request directly.
2652 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2653 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2655 } else { // The storage permission has already been granted.
2656 // Get a handle for the download image alert dialog.
2657 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2659 // Show the download image alert dialog.
2660 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2666 // Add a Copy URL entry.
2667 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2668 // Save the image URL in a `ClipData`.
2669 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2671 // Set the `ClipData` as the clipboard's primary clip.
2672 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2676 // Add an Open with App entry.
2677 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2678 openWithApp(imageUrl);
2682 // Add an Open with Browser entry.
2683 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2684 openWithBrowser(imageUrl);
2688 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2689 menu.add(R.string.cancel);
2693 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2694 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2695 // Get the image URL.
2696 imageUrl = hitTestResult.getExtra();
2698 // Set the image URL as the title of the `ContextMenu`.
2699 menu.setHeaderTitle(imageUrl);
2701 // Add a `View Image` entry.
2702 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2707 // Add a `Download Image` entry.
2708 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2709 // Check if the download should be processed by an external app.
2710 if (downloadWithExternalApp) { // Download with an external app.
2711 openUrlWithExternalApp(imageUrl);
2712 } else { // Download with Android's download manager.
2713 // Check to see if the storage permission has already been granted.
2714 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2715 // Store the image URL for use by `onRequestPermissionResult()`.
2716 downloadImageUrl = imageUrl;
2718 // Show a dialog if the user has previously denied the permission.
2719 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2720 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2721 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2723 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2724 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2725 } else { // Show the permission request directly.
2726 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2727 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2729 } else { // The storage permission has already been granted.
2730 // Get a handle for the download image alert dialog.
2731 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2733 // Show the download image alert dialog.
2734 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2740 // Add a `Copy URL` entry.
2741 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2742 // Save the image URL in a `ClipData`.
2743 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2745 // Set the `ClipData` as the clipboard's primary clip.
2746 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2750 // Add an Open with App entry.
2751 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2752 openWithApp(imageUrl);
2756 // Add an Open with Browser entry.
2757 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2758 openWithBrowser(imageUrl);
2762 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2763 menu.add(R.string.cancel);
2769 public void onCreateBookmark(DialogFragment dialogFragment) {
2770 // Get the views from the dialog fragment.
2771 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2772 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2774 // Extract the strings from the edit texts.
2775 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2776 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2778 // Get a copy of the favorite icon bitmap.
2779 Bitmap favoriteIcon = favoriteIconBitmap;
2781 // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2782 if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2783 favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2786 // Create a favorite icon byte array output stream.
2787 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2789 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2790 favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2792 // Convert the favorite icon byte array stream to a byte array.
2793 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2795 // Display the new bookmark below the current items in the (0 indexed) list.
2796 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2798 // Create the bookmark.
2799 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2801 // Update the bookmarks cursor with the current contents of this folder.
2802 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2804 // Update the list view.
2805 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2807 // Scroll to the new bookmark.
2808 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2812 public void onCreateBookmarkFolder(DialogFragment dialogFragment) {
2813 // Get handles for the views in the dialog fragment.
2814 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2815 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2816 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2818 // Get new folder name string.
2819 String folderNameString = createFolderNameEditText.getText().toString();
2821 // Create a folder icon bitmap.
2822 Bitmap folderIconBitmap;
2824 // Set the folder icon bitmap according to the dialog.
2825 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2826 // Get the default folder icon drawable.
2827 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2829 // Convert the folder icon drawable to a bitmap drawable.
2830 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2832 // Convert the folder icon bitmap drawable to a bitmap.
2833 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2834 } else { // Use the WebView favorite icon.
2835 // Get a copy of the favorite icon bitmap.
2836 folderIconBitmap = favoriteIconBitmap;
2838 // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2839 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2840 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2844 // Create a folder icon byte array output stream.
2845 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2847 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2848 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2850 // Convert the folder icon byte array stream to a byte array.
2851 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2853 // Move all the bookmarks down one in the display order.
2854 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2855 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2856 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2859 // Create the folder, which will be placed at the top of the `ListView`.
2860 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2862 // Update the bookmarks cursor with the current contents of this folder.
2863 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2865 // Update the `ListView`.
2866 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2868 // Scroll to the new folder.
2869 bookmarksListView.setSelection(0);
2873 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
2874 // Get handles for the views from `dialogFragment`.
2875 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2876 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2877 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2879 // Store the bookmark strings.
2880 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2881 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2883 // Update the bookmark.
2884 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2885 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2886 } else { // Update the bookmark using the `WebView` favorite icon.
2887 // Get a copy of the favorite icon bitmap.
2888 Bitmap favoriteIcon = favoriteIconBitmap;
2890 // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2891 if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2892 favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2895 // Create a favorite icon byte array output stream.
2896 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2898 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2899 favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2901 // Convert the favorite icon byte array stream to a byte array.
2902 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2904 // Update the bookmark and the favorite icon.
2905 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2908 // Update the bookmarks cursor with the current contents of this folder.
2909 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2911 // Update the list view.
2912 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2916 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId) {
2917 // Get handles for the views from `dialogFragment`.
2918 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2919 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2920 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2921 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2923 // Get the new folder name.
2924 String newFolderNameString = editFolderNameEditText.getText().toString();
2926 // Check if the favorite icon has changed.
2927 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2928 // Update the name in the database.
2929 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2930 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2931 // Create the new folder icon Bitmap.
2932 Bitmap folderIconBitmap;
2934 // Populate the new folder icon bitmap.
2935 if (defaultFolderIconRadioButton.isChecked()) {
2936 // Get the default folder icon drawable.
2937 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2939 // Convert the folder icon drawable to a bitmap drawable.
2940 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2942 // Convert the folder icon bitmap drawable to a bitmap.
2943 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2944 } else { // Use the `WebView` favorite icon.
2945 // Get a copy of the favorite icon bitmap.
2946 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
2948 // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2949 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2950 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2954 // Create a folder icon byte array output stream.
2955 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2957 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2958 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2960 // Convert the folder icon byte array stream to a byte array.
2961 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2963 // Update the folder icon in the database.
2964 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2965 } else { // The folder icon and the name have changed.
2966 // Get the new folder icon `Bitmap`.
2967 Bitmap folderIconBitmap;
2968 if (defaultFolderIconRadioButton.isChecked()) {
2969 // Get the default folder icon drawable.
2970 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2972 // Convert the folder icon drawable to a bitmap drawable.
2973 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2975 // Convert the folder icon bitmap drawable to a bitmap.
2976 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2977 } else { // Use the `WebView` favorite icon.
2978 // Get a copy of the favorite icon bitmap.
2979 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
2981 // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2982 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2983 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2987 // Create a folder icon byte array output stream.
2988 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2990 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2991 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2993 // Convert the folder icon byte array stream to a byte array.
2994 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2996 // Update the folder name and icon in the database.
2997 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
3000 // Update the bookmarks cursor with the current contents of this folder.
3001 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3003 // Update the `ListView`.
3004 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3008 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
3009 switch (downloadType) {
3010 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
3011 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
3012 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3015 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
3016 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
3017 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3023 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
3024 // Get a handle for the fragment manager.
3025 FragmentManager fragmentManager = getSupportFragmentManager();
3027 switch (requestCode) {
3028 case DOWNLOAD_FILE_REQUEST_CODE:
3029 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
3030 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
3032 // On API 23, displaying the fragment must be delayed or the app will crash.
3033 if (Build.VERSION.SDK_INT == 23) {
3034 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3036 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
3039 // Reset the download variables.
3041 downloadContentDisposition = "";
3042 downloadContentLength = 0;
3045 case DOWNLOAD_IMAGE_REQUEST_CODE:
3046 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
3047 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
3049 // On API 23, displaying the fragment must be delayed or the app will crash.
3050 if (Build.VERSION.SDK_INT == 23) {
3051 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3053 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
3056 // Reset the image URL variable.
3057 downloadImageUrl = "";
3063 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
3064 // Download the image if it has an HTTP or HTTPS URI.
3065 if (imageUrl.startsWith("http")) {
3066 // Get a handle for the system `DOWNLOAD_SERVICE`.
3067 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3069 // Parse `imageUrl`.
3070 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3072 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
3073 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3074 if (firstPartyCookiesEnabled) {
3075 // Get the cookies for `imageUrl`.
3076 String cookies = cookieManager.getCookie(imageUrl);
3078 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3079 downloadRequest.addRequestHeader("Cookie", cookies);
3082 // Get the file name from the dialog fragment.
3083 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3084 String imageName = downloadImageNameEditText.getText().toString();
3086 // Specify the download location.
3087 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3088 // Download to the public download directory.
3089 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3090 } else { // External write permission denied.
3091 // Download to the app's external download directory.
3092 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3095 // Allow `MediaScanner` to index the download if it is a media file.
3096 downloadRequest.allowScanningByMediaScanner();
3098 // Add the URL as the description for the download.
3099 downloadRequest.setDescription(imageUrl);
3101 // Show the download notification after the download is completed.
3102 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3104 // Remove the lint warning below that `downloadManager` might be `null`.
3105 assert downloadManager != null;
3107 // Initiate the download.
3108 downloadManager.enqueue(downloadRequest);
3109 } else { // The image is not an HTTP or HTTPS URI.
3110 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3115 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3116 // Download the file if it has an HTTP or HTTPS URI.
3117 if (downloadUrl.startsWith("http")) {
3118 // Get a handle for the system `DOWNLOAD_SERVICE`.
3119 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3121 // Parse `downloadUrl`.
3122 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3124 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
3125 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3126 if (firstPartyCookiesEnabled) {
3127 // Get the cookies for `downloadUrl`.
3128 String cookies = cookieManager.getCookie(downloadUrl);
3130 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3131 downloadRequest.addRequestHeader("Cookie", cookies);
3134 // Get the file name from the dialog fragment.
3135 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3136 String fileName = downloadFileNameEditText.getText().toString();
3138 // Specify the download location.
3139 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3140 // Download to the public download directory.
3141 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3142 } else { // External write permission denied.
3143 // Download to the app's external download directory.
3144 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3147 // Allow `MediaScanner` to index the download if it is a media file.
3148 downloadRequest.allowScanningByMediaScanner();
3150 // Add the URL as the description for the download.
3151 downloadRequest.setDescription(downloadUrl);
3153 // Show the download notification after the download is completed.
3154 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3156 // Remove the lint warning below that `downloadManager` might be `null`.
3157 assert downloadManager != null;
3159 // Initiate the download.
3160 downloadManager.enqueue(downloadRequest);
3161 } else { // The download is not an HTTP or HTTPS URI.
3162 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3167 public void onHttpAuthenticationCancel() {
3168 // Cancel the `HttpAuthHandler`.
3169 httpAuthHandler.cancel();
3173 public void onHttpAuthenticationProceed(DialogFragment dialogFragment) {
3174 // Get handles for the `EditTexts`.
3175 EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3176 EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3178 // Proceed with the HTTP authentication.
3179 httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3183 public void onSslErrorCancel() { // TODO. How to handle this with multiple tabs? There could be multiple errors at once.
3184 sslErrorHandler.cancel();
3188 public void onSslErrorProceed() { // TODO. How to handle this with multiple tabs? There could be multiple errors at once.
3189 sslErrorHandler.proceed();
3193 public void onPinnedMismatchBack() { // TODO. Move this logic to the dialog.
3194 if (currentWebView.canGoBack()) { // There is a back page in the history.
3195 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3196 formattedUrlString = ""; // TODO.
3198 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3199 navigatingHistory = true; // TODO.
3202 currentWebView.goBack();
3203 } else { // There are no pages to go back to.
3204 // Load a blank page
3210 public void onPinnedMismatchProceed() { // TODO. Move this logic to the dialog.
3211 // Do not check the pinned information for this domain again until the domain changes.
3212 currentWebView.setIgnorePinnedDomainInformation(true);
3216 public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3217 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3218 formattedUrlString = "";
3220 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3221 navigatingHistory = true;
3223 // Load the history entry.
3224 currentWebView.goBackOrForward(moveBackOrForwardSteps);
3228 public void onClearHistory() {
3229 // Clear the history.
3230 currentWebView.clearHistory();
3233 // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3235 public void onBackPressed() {
3236 // Get a handle for the drawer layout.
3237 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3239 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
3240 // Close the navigation drawer.
3241 drawerLayout.closeDrawer(GravityCompat.START);
3242 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
3243 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
3244 // close the bookmarks drawer.
3245 drawerLayout.closeDrawer(GravityCompat.END);
3246 } else { // A subfolder is displayed.
3247 // Place the former parent folder in `currentFolder`.
3248 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3250 // Load the new folder.
3251 loadBookmarksFolder();
3254 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
3255 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3256 formattedUrlString = "";
3258 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3259 navigatingHistory = true;
3262 currentWebView.goBack();
3263 } else { // There isn't anything to do in Privacy Browser.
3264 // Pass `onBackPressed()` to the system.
3265 super.onBackPressed();
3269 // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
3271 public void onActivityResult(int requestCode, int resultCode, Intent data) {
3272 // File uploads only work on API >= 21.
3273 if (Build.VERSION.SDK_INT >= 21) {
3274 // Pass the file to the WebView.
3275 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3279 private void loadUrlFromTextBox() {
3280 // Get a handle for the URL edit text.
3281 EditText urlEditText = findViewById(R.id.url_edittext);
3283 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
3284 String unformattedUrlString = urlEditText.getText().toString().trim();
3286 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
3287 if (unformattedUrlString.startsWith("content://")) {
3288 // Load the entire content URL.
3289 formattedUrlString = unformattedUrlString;
3290 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3291 || unformattedUrlString.startsWith("file://")) {
3292 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
3293 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3294 unformattedUrlString = "https://" + unformattedUrlString;
3297 // Initialize `unformattedUrl`.
3298 URL unformattedUrl = null;
3300 // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
3302 unformattedUrl = new URL(unformattedUrlString);
3303 } catch (MalformedURLException e) {
3304 e.printStackTrace();
3307 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3308 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3309 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3310 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3311 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3312 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3315 Uri.Builder formattedUri = new Uri.Builder();
3316 formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3318 // Decode `formattedUri` as a `String` in `UTF-8`.
3320 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
3321 } catch (UnsupportedEncodingException exception) {
3322 // Load a blank string.
3323 formattedUrlString = "";
3325 } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
3326 // Load a blank string.
3327 formattedUrlString = "";
3328 } else { // Search for the contents of the URL box.
3329 // Create an encoded URL String.
3330 String encodedUrlString;
3332 // Sanitize the search input.
3334 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3335 } catch (UnsupportedEncodingException exception) {
3336 encodedUrlString = "";
3339 // Add the base search URL.
3340 formattedUrlString = searchURL + encodedUrlString;
3343 // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
3344 urlEditText.clearFocus();
3347 loadUrl(formattedUrlString);
3350 private void loadUrl(String url) {// Apply any custom domain settings.
3351 // Set the URL as the formatted URL string so that checking third-party requests works correctly.
3352 formattedUrlString = url;
3354 // Apply the domain settings.
3355 applyDomainSettings(currentWebView, url, true, false);
3358 currentWebView.loadUrl(url, customHeaders);
3361 public void findPreviousOnPage(View view) {
3362 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
3363 currentWebView.findNext(false);
3366 public void findNextOnPage(View view) {
3367 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3368 currentWebView.findNext(true);
3371 public void closeFindOnPage(View view) {
3372 // Get a handle for the views.
3373 Toolbar toolbar = findViewById(R.id.toolbar);
3374 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3376 // Delete the contents of `find_on_page_edittext`.
3377 findOnPageEditText.setText(null);
3379 // Clear the highlighted phrases.
3380 currentWebView.clearMatches();
3382 // Hide the find on page linear layout.
3383 findOnPageLinearLayout.setVisibility(View.GONE);
3385 // Show the toolbar.
3386 toolbar.setVisibility(View.VISIBLE);
3388 // Hide the keyboard.
3389 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3392 private void applyAppSettings() {
3393 // Get a handle for the shared preferences.
3394 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3396 // Store the values from the shared preferences in variables.
3397 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3398 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3399 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3400 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3401 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3402 downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
3404 // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
3405 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3406 ActionBar actionBar = getSupportActionBar();
3408 // Remove the incorrect lint warnings below that the action bar might be null.
3409 assert actionBar != null;
3411 // Apply the proxy through Orbot settings.
3412 applyProxyThroughOrbot(false);
3414 // Set Do Not Track status.
3415 if (doNotTrackEnabled) {
3416 customHeaders.put("DNT", "1");
3418 customHeaders.remove("DNT");
3421 // Set the app bar scrolling for each WebView.
3422 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3423 // Get the WebView tab fragment.
3424 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3426 // Get the fragment view.
3427 View fragmentView = webViewTabFragment.getView();
3429 // Only modify the WebViews if they exist.
3430 if (fragmentView != null) {
3431 // Get the nested scroll WebView from the tab fragment.
3432 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3434 // Set the app bar scrolling.
3435 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
3439 // Update the full screen browsing mode settings.
3440 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3441 // Update the visibility of the app bar, which might have changed in the settings.
3448 // Hide the banner ad in the free flavor.
3449 if (BuildConfig.FLAVOR.contentEquals("free")) {
3450 AdHelper.hideAd(findViewById(R.id.adview));
3453 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3454 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3456 /* Hide the system bars.
3457 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3458 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3459 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3460 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3462 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3463 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3464 } else { // Privacy Browser is not in full screen browsing mode.
3465 // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3466 inFullScreenBrowsingMode = false;
3468 // Show the app bar.
3471 // Show the banner ad in the free flavor.
3472 if (BuildConfig.FLAVOR.contentEquals("free")) {
3473 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3474 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3477 // Remove the `SYSTEM_UI` flags from the root frame layout.
3478 rootFrameLayout.setSystemUiVisibility(0);
3480 // Add the translucent status flag.
3481 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3486 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3487 @SuppressLint("SetJavaScriptEnabled")
3488 private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
3489 // Get a handle for the URL relative layout.
3490 RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3492 // Store a copy of the current user agent to track changes for the return boolean.
3493 String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString();
3495 // Parse the URL into a URI.
3496 Uri uri = Uri.parse(url);
3498 // Extract the domain from `uri`.
3499 String newHostName = uri.getHost();
3501 // Strings don't like to be null.
3502 if (newHostName == null) {
3506 // Only apply the domain settings if a new domain is being loaded. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3507 if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName)) {
3508 // Set the new host name as the current domain name.
3509 nestedScrollWebView.setCurrentDomainName(newHostName);
3511 // Reset the ignoring of pinned domain information.
3512 nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3514 // Clear any pinned SSL certificate or IP addresses.
3515 nestedScrollWebView.clearPinnedSslCertificate();
3516 nestedScrollWebView.clearPinnedIpAddresses();
3518 // Reset the favorite icon if specified.
3519 if (resetFavoriteIcon) {
3520 // Store the favorite icon bitmap.
3521 favoriteIconBitmap = favoriteIconDefaultBitmap; // TODO.
3523 // Get a handle for the tab layout.
3524 TabLayout tabLayout = findViewById(R.id.tablayout);
3526 // Get the current tab.
3527 TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition()); // TODO. We need to get the tab for this WebView, which might not be the current tab.
3529 // Remove the warning below that the current tab might be null.
3530 assert currentTab != null;
3532 // Get the current tab custom view.
3533 View currentTabCustomView = currentTab.getCustomView();
3535 // Remove the warning below that the current tab custom view might be null.
3536 assert currentTabCustomView != null;
3538 // Get the current tab favorite icon image view.
3539 ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
3541 // Set the default favorite icon as the favorite icon for this tab.
3542 currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
3545 // Get a handle for the swipe refresh layout.
3546 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3548 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3549 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3551 // Get a full cursor from `domainsDatabaseHelper`.
3552 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3554 // Initialize `domainSettingsSet`.
3555 Set<String> domainSettingsSet = new HashSet<>();
3557 // Get the domain name column index.
3558 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3560 // Populate `domainSettingsSet`.
3561 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3562 // Move `domainsCursor` to the current row.
3563 domainNameCursor.moveToPosition(i);
3565 // Store the domain name in `domainSettingsSet`.
3566 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3569 // Close `domainNameCursor.
3570 domainNameCursor.close();
3572 // Initialize the domain name in database variable.
3573 String domainNameInDatabase = null;
3575 // Check the hostname against the domain settings set.
3576 if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3577 // Record the domain name in the database.
3578 domainNameInDatabase = newHostName;
3580 // Set the domain settings applied tracker to true.
3581 nestedScrollWebView.setDomainSettingsApplied(true);
3582 } else { // The hostname is not contained in the domain settings set.
3583 // Set the domain settings applied tracker to false.
3584 nestedScrollWebView.setDomainSettingsApplied(false);
3587 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3588 while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3589 if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
3590 // Set the domain settings applied tracker to true.
3591 nestedScrollWebView.setDomainSettingsApplied(true);
3593 // Store the applied domain names as it appears in the database.
3594 domainNameInDatabase = "*." + newHostName;
3597 // Strip out the lowest subdomain of of the host name.
3598 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3602 // Get a handle for the shared preferences.
3603 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3605 // Store the general preference information.
3606 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3607 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3608 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3609 nightMode = sharedPreferences.getBoolean("night_mode", false); // TODO.
3610 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3612 if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3613 // Get a cursor for the current host and move it to the first position.
3614 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3615 currentDomainSettingsCursor.moveToFirst();
3617 // Get the settings from the cursor.
3618 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3619 boolean domainJavaScriptEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3620 firstPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); // TODO.
3621 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3622 boolean domainDomStorageEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3623 // Form data can be removed once the minimum API >= 26.
3624 saveFormDataEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); // TODO.
3625 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST,
3626 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3627 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY,
3628 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3629 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3630 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3631 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3632 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3633 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY,
3634 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3635 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3636 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3637 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3638 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3639 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3640 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3641 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3642 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3643 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3644 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3645 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3646 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3647 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3648 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3649 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3650 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3652 // Create the pinned SSL date variables.
3653 Date pinnedSslStartDate;
3654 Date pinnedSslEndDate;
3656 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0 because creating a new Date results in an error if the input is 0.
3657 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3658 pinnedSslStartDate = null;
3660 pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3663 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0 because creating a new Date results in an error if the input is 0.
3664 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3665 pinnedSslEndDate = null;
3667 pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3670 // If there is a pinned SSL certificate, store it in the WebView.
3671 if (pinnedSslCertificate) {
3672 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3673 pinnedSslStartDate, pinnedSslEndDate);
3676 // If there is a pinned IP address, store it in the WebView.
3677 if (pinnedIpAddresses) {
3678 nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3681 // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
3682 switch (nightModeInt) {
3683 case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3684 nightMode = true; // TODO.
3687 case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3688 nightMode = false; // TODO.
3693 // Store the domain JavaScript status. This is used by the options menu night mode toggle.
3694 domainSettingsJavaScriptEnabled = domainJavaScriptEnabled;
3696 // Enable JavaScript if night mode is enabled.
3698 // Enable JavaScript.
3699 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3701 // Set JavaScript according to the domain settings.
3702 nestedScrollWebView.getSettings().setJavaScriptEnabled(domainJavaScriptEnabled);
3705 // Close `currentHostDomainSettingsCursor`.
3706 currentDomainSettingsCursor.close();
3708 // Apply the domain settings.
3709 cookieManager.setAcceptCookie(firstPartyCookiesEnabled); //TODO This could be bad.
3710 nestedScrollWebView.getSettings().setDomStorageEnabled(domainDomStorageEnabled); // TODO. Move up.
3712 // Apply the form data setting if the API < 26.
3713 if (Build.VERSION.SDK_INT < 26) {
3714 nestedScrollWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3717 // Apply the font size.
3718 if (fontSize == 0) { // Apply the default font size.
3719 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3720 } else { // Apply the specified font size.
3721 nestedScrollWebView.getSettings().setTextZoom(fontSize);
3724 // Set third-party cookies status if API >= 21.
3725 if (Build.VERSION.SDK_INT >= 21) {
3726 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3729 // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
3730 // <https://redmine.stoutner.com/issues/160>
3731 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3732 // Set the user agent.
3733 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3734 // Get the array position of the default user agent name.
3735 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); // TODO Could this be local.
3737 // Set the user agent according to the system default.
3738 switch (defaultUserAgentArrayPosition) {
3739 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3740 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3741 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3744 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3745 // Set the user agent to `""`, which uses the default value.
3746 nestedScrollWebView.getSettings().setUserAgentString("");
3749 case SETTINGS_CUSTOM_USER_AGENT:
3750 // Set the default custom user agent.
3751 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3755 // Get the user agent string from the user agent data array
3756 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3758 } else { // Set the user agent according to the stored name.
3759 // Get the array position of the user agent name.
3760 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3762 switch (userAgentArrayPosition) {
3763 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3764 nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3767 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3768 // Set the user agent to `""`, which uses the default value.
3769 nestedScrollWebView.getSettings().setUserAgentString("");
3773 // Get the user agent string from the user agent data array.
3774 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3779 // Set swipe to refresh.
3780 switch (swipeToRefreshInt) { // TODO. This needs to be set and updated by tab.
3781 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3782 // Set swipe to refresh according to the default.
3783 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3786 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3787 // Enable swipe to refresh.
3788 swipeRefreshLayout.setEnabled(true);
3791 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3792 // Disable swipe to refresh.
3793 swipeRefreshLayout.setEnabled(false);
3796 // Set the loading of webpage images.
3797 switch (displayWebpageImagesInt) {
3798 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3799 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3802 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3803 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3806 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3807 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3811 // Set a green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3813 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3815 urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3817 } else { // The new URL does not have custom domain settings. Load the defaults.
3818 // Store the values from the shared preferences.
3819 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3820 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false); // TODO.
3821 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3822 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3823 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26. // TODO.
3824 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, sharedPreferences.getBoolean("easylist", true));
3825 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3826 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3827 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3828 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3829 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3831 // Enable JavaScript if night mode is enabled.
3833 // Enable JavaScript.
3834 nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3836 // Set JavaScript according to the domain settings.
3837 nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3840 // Apply the default settings.
3841 cookieManager.setAcceptCookie(firstPartyCookiesEnabled); // TODO.
3842 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3843 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3845 // Apply the form data setting if the API < 26.
3846 if (Build.VERSION.SDK_INT < 26) {
3847 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3850 // Reset the pinned variables.
3851 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
3853 // Set third-party cookies status if API >= 21.
3854 if (Build.VERSION.SDK_INT >= 21) {
3855 cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
3858 // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
3859 // <https://redmine.stoutner.com/issues/160>
3860 if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading.
3861 // Get the array position of the user agent name.
3862 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3864 // Set the user agent.
3865 switch (userAgentArrayPosition) {
3866 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3867 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3868 nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3871 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3872 // Set the user agent to `""`, which uses the default value.
3873 nestedScrollWebView.getSettings().setUserAgentString("");
3876 case SETTINGS_CUSTOM_USER_AGENT:
3877 // Set the default custom user agent.
3878 nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3882 // Get the user agent string from the user agent data array
3883 nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3887 // Set the loading of webpage images.
3888 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3890 // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
3891 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
3894 // Close the domains database helper.
3895 domainsDatabaseHelper.close();
3897 // Update the privacy icons.
3898 updatePrivacyIcons(true);
3901 // Reload the website if returning from the Domains activity.
3902 if (reloadWebsite) {
3903 nestedScrollWebView.reload();
3906 // Return the user agent changed status.
3907 return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
3910 private void applyProxyThroughOrbot(boolean reloadWebsite) {
3911 // Get a handle for the shared preferences.
3912 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3914 // Get the search preferences.
3915 String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
3916 String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
3917 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3918 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3919 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3920 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3922 // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
3923 ActionBar actionBar = getSupportActionBar();
3925 // Remove the incorrect lint warning later that the action bar might be null.
3926 assert actionBar != null;
3928 // Set the homepage, search, and proxy options.
3929 if (proxyThroughOrbot) { // Set the Tor options.
3930 // Set `torHomepageString` as `homepage`.
3931 homepage = torHomepageString;
3933 // If formattedUrlString is null assign the homepage to it.
3934 if (formattedUrlString == null) {
3935 formattedUrlString = homepage;
3938 // Set the search URL.
3939 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
3940 searchURL = torSearchCustomUrlString;
3941 } else { // Use the string from the pre-built list.
3942 searchURL = torSearchString;
3945 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
3946 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
3948 // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context.
3950 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
3952 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
3955 // Check to see if Orbot is ready.
3956 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
3957 // Set `waitingForOrbot`.
3958 waitingForOrbot = true;
3960 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
3961 currentWebView.getSettings().setUseWideViewPort(false);
3963 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
3964 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
3965 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
3966 // Reload the website.
3967 currentWebView.reload();
3969 } else { // Set the non-Tor options.
3970 // Set `homepageString` as `homepage`.
3971 homepage = homepageString;
3973 // If formattedUrlString is null assign the homepage to it.
3974 if (formattedUrlString == null) {
3975 formattedUrlString = homepage;
3978 // Set the search URL.
3979 if (searchString.equals("Custom URL")) { // Get the custom URL string.
3980 searchURL = searchCustomUrlString;
3981 } else { // Use the string from the pre-built list.
3982 searchURL = searchString;
3985 // Reset the proxy to default. The host is `""` and the port is `"0"`.
3986 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
3988 // Set the default `appBar` background. `this` refers to the context.
3990 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
3992 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
3995 // Reset `waitingForOrbot.
3996 waitingForOrbot = false;
3998 // Reload the WebViews if requested.
3999 if (reloadWebsite) {
4000 // Reload the WebViews.
4001 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4002 // Get the WebView tab fragment.
4003 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4005 // Get the fragment view.
4006 View fragmentView = webViewTabFragment.getView();
4008 // Only reload the WebViews if they exist.
4009 if (fragmentView != null) {
4010 // Get the nested scroll WebView from the tab fragment.
4011 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4013 // Reload the WebView.
4014 nestedScrollWebView.reload();
4021 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4022 // Only update the privacy icons if the options menu has already been populated.
4023 if (optionsMenu != null) {
4024 // Get handles for the menu items.
4025 MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
4026 MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
4027 MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
4028 MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
4030 // Update the privacy icon.
4031 if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
4032 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4033 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled.
4034 privacyMenuItem.setIcon(R.drawable.warning);
4035 } else { // All the dangerous features are disabled.
4036 privacyMenuItem.setIcon(R.drawable.privacy_mode);
4039 // Update the first-party cookies icon.
4040 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
4041 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4042 } else { // First-party cookies are disabled.
4044 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4046 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4050 // Update the DOM storage icon.
4051 if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
4052 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4053 } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
4055 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4057 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4059 } else { // JavaScript is disabled, so DOM storage is ghosted.
4061 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4063 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4067 // Update the refresh icon.
4069 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4071 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4074 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4075 if (runInvalidateOptionsMenu) {
4076 invalidateOptionsMenu();
4081 private void openUrlWithExternalApp(String url) {
4082 // Create a download intent. Not specifying the action type will display the maximum number of options.
4083 Intent downloadIntent = new Intent();
4085 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4086 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4088 // Flag the intent to open in a new task.
4089 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4091 // Show the chooser.
4092 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4095 private void highlightUrlText() {
4096 // Get a handle for the URL edit text.
4097 EditText urlEditText = findViewById(R.id.url_edittext);
4099 // Only highlight the URL text if the box is not currently selected.
4100 if (!urlEditText.hasFocus()) {
4101 // Get the URL string.
4102 String urlString = urlEditText.getText().toString();
4104 // Highlight the URL according to the protocol.
4105 if (urlString.startsWith("file://")) { // This is a file URL.
4106 // De-emphasize only the protocol.
4107 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4108 } else if (urlString.startsWith("content://")) {
4109 // De-emphasize only the protocol.
4110 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4111 } else { // This is a web URL.
4112 // Get the index of the `/` immediately after the domain name.
4113 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4115 // Create a base URL string.
4118 // Get the base URL.
4119 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4120 // Get the base URL.
4121 baseUrl = urlString.substring(0, endOfDomainName);
4122 } else { // There are no characters after the base URL.
4123 // Set the base URL to be the entire URL string.
4124 baseUrl = urlString;
4127 // Get the index of the last `.` in the domain.
4128 int lastDotIndex = baseUrl.lastIndexOf(".");
4130 // Get the index of the penultimate `.` in the domain.
4131 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4133 // Markup the beginning of the URL.
4134 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4135 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4137 // De-emphasize subdomains.
4138 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4139 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4141 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4142 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4143 // De-emphasize the protocol and the additional subdomains.
4144 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4145 } else { // There is only one subdomain in the domain name.
4146 // De-emphasize only the protocol.
4147 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4151 // De-emphasize the text after the domain name.
4152 if (endOfDomainName > 0) {
4153 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4159 private void loadBookmarksFolder() {
4160 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4161 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4163 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4164 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4166 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4167 // Inflate the individual item layout. `false` does not attach it to the root.
4168 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4172 public void bindView(View view, Context context, Cursor cursor) {
4173 // Get handles for the views.
4174 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4175 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4177 // Get the favorite icon byte array from the cursor.
4178 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4180 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4181 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4183 // Display the bitmap in `bookmarkFavoriteIcon`.
4184 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4186 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4187 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4188 bookmarkNameTextView.setText(bookmarkNameString);
4190 // Make the font bold for folders.
4191 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4192 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4193 } else { // Reset the font to default for normal bookmarks.
4194 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4199 // Populate the `ListView` with the adapter.
4200 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4202 // Set the bookmarks drawer title.
4203 if (currentBookmarksFolder.isEmpty()) {
4204 bookmarksTitleTextView.setText(R.string.bookmarks);
4206 bookmarksTitleTextView.setText(currentBookmarksFolder);
4210 private void openWithApp(String url) {
4211 // Create the open with intent with `ACTION_VIEW`.
4212 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4214 // Set the URI but not the MIME type. This should open all available apps.
4215 openWithAppIntent.setData(Uri.parse(url));
4217 // Flag the intent to open in a new task.
4218 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4220 // Show the chooser.
4221 startActivity(openWithAppIntent);
4224 private void openWithBrowser(String url) {
4225 // Create the open with intent with `ACTION_VIEW`.
4226 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4228 // Set the URI and the MIME type. `"text/html"` should load browser options.
4229 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4231 // Flag the intent to open in a new task.
4232 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4234 // Show the chooser.
4235 startActivity(openWithBrowserIntent);
4238 public void addTab(View view) {
4239 // Get a handle for the tab layout and the view pager.
4240 TabLayout tabLayout = findViewById(R.id.tablayout);
4241 ViewPager webViewPager = findViewById(R.id.webviewpager);
4243 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4244 int newTabNumber = tabLayout.getTabCount();
4247 tabLayout.addTab(tabLayout.newTab());
4250 TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4252 // Remove the lint warning below that the current tab might be null.
4253 assert newTab != null;
4255 // Set a custom view on the new tab.
4256 newTab.setCustomView(R.layout.custom_tab_view);
4258 // Add the new WebView page.
4259 webViewPagerAdapter.addPage(newTabNumber, webViewPager);
4263 public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar) {
4264 // Get handles for the activity views.
4265 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4266 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4267 RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4268 ActionBar actionBar = getSupportActionBar();
4269 EditText urlEditText = findViewById(R.id.url_edittext);
4270 TabLayout tabLayout = findViewById(R.id.tablayout);
4271 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4273 // Remove the incorrect lint warnings below that the some of the views might be null.
4274 assert actionBar != null;
4276 // Get a handle for the activity
4277 Activity activity = this;
4279 // Get a handle for the shared preferences.
4280 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4282 // Get the relevant preferences.
4283 boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4285 // Set the app bar scrolling.
4286 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4288 // Allow pinch to zoom.
4289 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4291 // Hide zoom controls.
4292 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4294 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4295 if (Build.VERSION.SDK_INT >= 21) {
4296 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4299 // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
4300 nestedScrollWebView.getSettings().setUseWideViewPort(true);
4302 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4303 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4305 // Explicitly disable geolocation.
4306 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4308 // Create a double-tap gesture detector to toggle full-screen mode.
4309 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4310 // Override `onDoubleTap()`. All other events are handled using the default settings.
4312 public boolean onDoubleTap(MotionEvent event) {
4313 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4314 // Toggle the full screen browsing mode tracker.
4315 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4317 // Toggle the full screen browsing mode.
4318 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4319 // Hide the app bar if specified.
4324 // Hide the banner ad in the free flavor.
4325 if (BuildConfig.FLAVOR.contentEquals("free")) {
4326 AdHelper.hideAd(findViewById(R.id.adview));
4329 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4330 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4332 /* Hide the system bars.
4333 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4334 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4335 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4336 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4338 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4339 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4340 } else { // Switch to normal viewing mode.
4341 // Show the app bar.
4344 // Show the banner ad in the free flavor.
4345 if (BuildConfig.FLAVOR.contentEquals("free")) {
4347 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4350 // Remove the `SYSTEM_UI` flags from the root frame layout.
4351 rootFrameLayout.setSystemUiVisibility(0);
4353 // Add the translucent status flag.
4354 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4357 // Consume the double-tap.
4359 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4365 // Pass all touch events on the WebView through the double-tap gesture detector.
4366 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4367 // Call `performClick()` on the view, which is required for accessibility.
4368 view.performClick();
4370 // Send the event to the gesture detector.
4371 return doubleTapGestureDetector.onTouchEvent(event);
4374 // Register the WebView for a context menu. This is used to see link targets and download images.
4375 registerForContextMenu(nestedScrollWebView);
4377 // Allow the downloading of files.
4378 nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4379 // Check if the download should be processed by an external app.
4380 if (downloadWithExternalApp) { // Download with an external app.
4381 // Create a download intent. Not specifying the action type will display the maximum number of options.
4382 Intent downloadIntent = new Intent();
4384 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4385 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4387 // Flag the intent to open in a new task.
4388 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4390 // Show the chooser.
4391 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4392 } else { // Download with Android's download manager.
4393 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4394 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4395 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4397 // Store the variables for future use by `onRequestPermissionsResult()`.
4399 downloadContentDisposition = contentDisposition;
4400 downloadContentLength = contentLength;
4402 // Show a dialog if the user has previously denied the permission.
4403 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4404 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4405 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4407 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4408 downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4409 } else { // Show the permission request directly.
4410 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4411 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4413 } else { // The storage permission has already been granted.
4414 // Get a handle for the download file alert dialog.
4415 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4417 // Show the download file alert dialog.
4418 downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4423 // Update the find on page count.
4424 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4425 // Get a handle for `findOnPageCountTextView`.
4426 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4429 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4430 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4431 // Set `findOnPageCountTextView` to `0/0`.
4432 findOnPageCountTextView.setText(R.string.zero_of_zero);
4433 } else if (isDoneCounting) { // There are matches.
4434 // `activeMatchOrdinal` is zero-based.
4435 int activeMatch = activeMatchOrdinal + 1;
4437 // Build the match string.
4438 String matchString = activeMatch + "/" + numberOfMatches;
4440 // Set `findOnPageCountTextView`.
4441 findOnPageCountTextView.setText(matchString);
4446 // Set the web chrome client.
4447 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4448 // Update the progress bar when a page is loading.
4450 public void onProgressChanged(WebView view, int progress) {
4451 // Inject the night mode CSS if night mode is enabled.
4453 // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links
4454 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
4455 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
4456 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4457 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4458 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4459 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4460 // Initialize a handler to display `mainWebView`.
4461 Handler displayWebViewHandler = new Handler();
4463 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4464 Runnable displayWebViewRunnable = () -> {
4465 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
4466 if (progressBar.getVisibility() == View.GONE) {
4467 nestedScrollWebView.setVisibility(View.VISIBLE);
4471 // Displaying of `mainWebView` after 500 milliseconds.
4472 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4476 // Update the progress bar.
4477 progressBar.setProgress(progress);
4479 // Set the visibility of the progress bar.
4480 if (progress < 100) {
4481 // Show the progress bar.
4482 progressBar.setVisibility(View.VISIBLE);
4484 // Hide the progress bar.
4485 progressBar.setVisibility(View.GONE);
4487 // Display `mainWebView` if night mode is disabled.
4488 // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not
4489 // currently enabled.
4491 nestedScrollWebView.setVisibility(View.VISIBLE);
4494 //Stop the swipe to refresh indicator if it is running
4495 swipeRefreshLayout.setRefreshing(false);
4499 // Set the favorite icon when it changes.
4501 public void onReceivedIcon(WebView view, Bitmap icon) {
4502 // Only update the favorite icon if the website has finished loading.
4503 if (progressBar.getVisibility() == View.GONE) {
4504 // Save a copy of the favorite icon.
4505 favoriteIconBitmap = icon;
4507 // Get the current page position.
4508 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4510 // Get the current tab.
4511 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4513 // Remove the lint warning below that the current tab might be null.
4516 // Get the custom view from the tab.
4517 View tabView = tab.getCustomView();
4519 // Remove the incorrect warning below that the current tab view might be null.
4520 assert tabView != null;
4522 // Get the favorite icon image view from the tab.
4523 ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
4525 // Display the favorite icon in the tab.
4526 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4530 // Save a copy of the title when it changes.
4532 public void onReceivedTitle(WebView view, String title) {
4533 // Get the current page position.
4534 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4536 // Get the current tab.
4537 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4539 // Remove the lint warning below that the current tab might be null.
4542 // Get the custom view from the tab.
4543 View tabView = tab.getCustomView();
4545 // Remove the incorrect warning below that the current tab view might be null.
4546 assert tabView != null;
4548 // Get the title text view from the tab.
4549 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
4551 // Set the title as the tab text.
4552 tabTitleTextView.setText(title);
4555 // Enter full screen video.
4557 public void onShowCustomView(View video, CustomViewCallback callback) {
4558 // Set the full screen video flag.
4559 displayingFullScreenVideo = true;
4561 // Pause the ad if this is the free flavor.
4562 if (BuildConfig.FLAVOR.contentEquals("free")) {
4563 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4564 AdHelper.pauseAd(findViewById(R.id.adview));
4567 // Hide the keyboard.
4568 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4570 // Hide the main content relative layout.
4571 mainContentRelativeLayout.setVisibility(View.GONE);
4573 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4574 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4576 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4577 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4579 /* Hide the system bars.
4580 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4581 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4582 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4583 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4585 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4586 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4588 // Disable the sliding drawers.
4589 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4591 // Add the video view to the full screen video frame layout.
4592 fullScreenVideoFrameLayout.addView(video);
4594 // Show the full screen video frame layout.
4595 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4598 // Exit full screen video.
4600 public void onHideCustomView() {
4601 // Unset the full screen video flag.
4602 displayingFullScreenVideo = false;
4604 // Remove all the views from the full screen video frame layout.
4605 fullScreenVideoFrameLayout.removeAllViews();
4607 // Hide the full screen video frame layout.
4608 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4610 // Enable the sliding drawers.
4611 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4613 // Show the main content relative layout.
4614 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4616 // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4617 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4618 // Hide the app bar if specified.
4623 // Hide the banner ad in the free flavor.
4624 if (BuildConfig.FLAVOR.contentEquals("free")) {
4625 AdHelper.hideAd(findViewById(R.id.adview));
4628 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4629 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4631 /* Hide the system bars.
4632 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4633 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4634 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4635 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4637 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4638 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4639 } else { // Switch to normal viewing mode.
4640 // Remove the `SYSTEM_UI` flags from the root frame layout.
4641 rootFrameLayout.setSystemUiVisibility(0);
4643 // Add the translucent status flag.
4644 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4647 // Reload the ad for the free flavor if not in full screen mode.
4648 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4650 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4656 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4657 // Show the file chooser if the device is running API >= 21.
4658 if (Build.VERSION.SDK_INT >= 21) {
4659 // Store the file path callback.
4660 fileChooserCallback = filePathCallback;
4662 // Create an intent to open a chooser based ont the file chooser parameters.
4663 Intent fileChooserIntent = fileChooserParams.createIntent();
4665 // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4666 startActivityForResult(fileChooserIntent, 0);
4672 nestedScrollWebView.setWebViewClient(new WebViewClient() {
4673 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4674 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4675 @SuppressWarnings("deprecation")
4677 public boolean shouldOverrideUrlLoading(WebView view, String url) {
4678 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
4679 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
4680 formattedUrlString = "";
4682 // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
4683 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
4685 // Check if the user agent has changed.
4686 if (userAgentChanged) {
4687 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
4688 nestedScrollWebView.loadUrl(url, customHeaders);
4690 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4693 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4696 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
4697 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4698 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4700 // Parse the url and set it as the data for the intent.
4701 emailIntent.setData(Uri.parse(url));
4703 // Open the email program in a new task instead of as part of Privacy Browser.
4704 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4707 startActivity(emailIntent);
4709 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4711 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
4712 // Open the dialer and load the phone number, but wait for the user to place the call.
4713 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4715 // Add the phone number to the intent.
4716 dialIntent.setData(Uri.parse(url));
4718 // Open the dialer in a new task instead of as part of Privacy Browser.
4719 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4722 startActivity(dialIntent);
4724 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4726 } else { // Load a system chooser to select an app that can handle the URL.
4727 // Open an app that can handle the URL.
4728 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4730 // Add the URL to the intent.
4731 genericIntent.setData(Uri.parse(url));
4733 // List all apps that can handle the URL instead of just opening the first one.
4734 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4736 // Open the app in a new task instead of as part of Privacy Browser.
4737 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4739 // Start the app or display a snackbar if no app is available to handle the URL.
4741 startActivity(genericIntent);
4742 } catch (ActivityNotFoundException exception) {
4743 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
4746 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4751 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4752 @SuppressWarnings("deprecation")
4754 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4755 // Create an empty web resource response to be used if the resource request is blocked.
4756 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4758 // Reset the whitelist results tracker.
4759 String[] whitelistResultStringArray = null;
4761 // Initialize the third party request tracker.
4762 boolean isThirdPartyRequest = false;
4764 // Initialize the current domain string.
4765 String currentDomain = "";
4767 // Nobody is happy when comparing null strings.
4768 if (!(formattedUrlString == null) && !(url == null)) {
4769 // Get the domain strings to URIs.
4770 Uri currentDomainUri = Uri.parse(formattedUrlString);
4771 Uri requestDomainUri = Uri.parse(url);
4773 // Get the domain host names.
4774 String currentBaseDomain = currentDomainUri.getHost();
4775 String requestBaseDomain = requestDomainUri.getHost();
4777 // Update the current domain variable.
4778 currentDomain = currentBaseDomain;
4780 // Only compare the current base domain and the request base domain if neither is null.
4781 if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
4782 // Determine the current base domain.
4783 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4784 // Remove the first subdomain.
4785 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4788 // Determine the request base domain.
4789 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4790 // Remove the first subdomain.
4791 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4794 // Update the third party request tracker.
4795 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4799 // Get the current WebView page position.
4800 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
4802 // Determine if the WebView is currently displayed.
4803 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
4805 // Block third-party requests if enabled.
4806 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
4807 // Add the result to the resource requests.
4808 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_THIRD_PARTY, url});
4810 // Increment the blocked requests counters.
4811 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4812 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
4814 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4815 if (webViewDisplayed) {
4816 // Updating the UI must be run from the UI thread.
4817 activity.runOnUiThread(() -> {
4818 // Update the menu item titles.
4819 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4820 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4821 blockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
4822 getString(R.string.block_all_third_party_requests));
4826 // Return an empty web resource response.
4827 return emptyWebResourceResponse;
4830 // Check UltraPrivacy if it is enabled.
4831 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)) {
4832 // Check the URL against UltraPrivacy.
4833 String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
4835 // Process the UltraPrivacy results.
4836 if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist.
4837 // Add the result to the resource requests.
4838 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4839 ultraPrivacyResults[5]});
4841 // Increment the blocked requests counters.
4842 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4843 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY);
4845 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4846 if (webViewDisplayed) {
4847 // Updating the UI must be run from the UI thread.
4848 activity.runOnUiThread(() -> {
4849 // Update the menu item titles.
4850 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4851 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4852 ultraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
4856 // The resource request was blocked. Return an empty web resource response.
4857 return emptyWebResourceResponse;
4858 } else if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist.
4859 // Add a whitelist entry to the resource requests array.
4860 nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4861 ultraPrivacyResults[5]});
4863 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
4868 // Check EasyList if it is enabled.
4869 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)) {
4870 // Check the URL against EasyList.
4871 String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
4873 // Process the EasyList results.
4874 if (easyListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist.
4875 // Add the result to the resource requests.
4876 nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
4878 // Increment the blocked requests counters.
4879 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4880 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_LIST);
4882 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4883 if (webViewDisplayed) {
4884 // Updating the UI must be run from the UI thread.
4885 activity.runOnUiThread(() -> {
4886 // Update the menu item titles.
4887 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4888 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4889 easyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
4893 // The resource request was blocked. Return an empty web resource response.
4894 return emptyWebResourceResponse;
4895 } else if (easyListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist.
4896 // Update the whitelist result string array tracker.
4897 whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
4901 // Check EasyPrivacy if it is enabled.
4902 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)) {
4903 // Check the URL against EasyPrivacy.
4904 String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
4906 // Process the EasyPrivacy results.
4907 if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist.
4908 // Add the result to the resource requests.
4909 nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[5],
4910 easyPrivacyResults[5]});
4912 // Increment the blocked requests counters.
4913 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4914 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY);
4916 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4917 if (webViewDisplayed) {
4918 // Updating the UI must be run from the UI thread.
4919 activity.runOnUiThread(() -> {
4920 // Update the menu item titles.
4921 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4922 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4923 easyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
4927 // The resource request was blocked. Return an empty web resource response.
4928 return emptyWebResourceResponse;
4929 } else if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist.
4930 // Update the whitelist result string array tracker.
4931 whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
4935 // Check Fanboy’s Annoyance List if it is enabled.
4936 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
4937 // Check the URL against Fanboy's Annoyance List.
4938 String[] fanboysAnnoyanceListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
4940 // Process the Fanboy's Annoyance List results.
4941 if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist.
4942 // Add the result to the resource requests.
4943 nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4944 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
4946 // Increment the blocked requests counters.
4947 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4948 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
4950 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4951 if (webViewDisplayed) {
4952 // Updating the UI must be run from the UI thread.
4953 activity.runOnUiThread(() -> {
4954 // Update the menu item titles.
4955 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4956 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4957 fanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
4958 getString(R.string.fanboys_annoyance_list));
4962 // The resource request was blocked. Return an empty web resource response.
4963 return emptyWebResourceResponse;
4964 } else if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist.
4965 // Update the whitelist result string array tracker.
4966 whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
4967 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
4969 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
4970 // Check the URL against Fanboy's Annoyance List.
4971 String[] fanboysSocialListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
4973 // Process the Fanboy's Social Blocking List results.
4974 if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist.
4975 // Add the result to the resource requests.
4976 nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
4977 fanboysSocialListResults[4], fanboysSocialListResults[5]});
4979 // Increment the blocked requests counters.
4980 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4981 nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
4983 // Update the titles of the blocklist menu items if the WebView is currently displayed.
4984 if (webViewDisplayed) {
4985 // Updating the UI must be run from the UI thread.
4986 activity.runOnUiThread(() -> {
4987 // Update the menu item titles.
4988 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4989 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4990 fanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
4991 getString(R.string.fanboys_social_blocking_list));
4995 // The resource request was blocked. Return an empty web resource response.
4996 return emptyWebResourceResponse;
4997 } else if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist.
4998 // Update the whitelist result string array tracker.
4999 whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5000 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5004 // Add the request to the log because it hasn't been processed by any of the previous checks.
5005 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5006 nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5007 } else { // The request didn't match any blocklist entry. Log it as a default request.
5008 nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_DEFAULT, url});
5011 // The resource request has not been blocked. `return null` loads the requested resource.
5015 // Handle HTTP authentication requests.
5017 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5018 // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`.
5019 httpAuthHandler = handler;
5021 // Display the HTTP authentication dialog.
5022 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
5023 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5027 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5028 // Reset the list of resource requests.
5029 nestedScrollWebView.clearResourceRequests();
5031 // Reset the requests counters.
5032 nestedScrollWebView.resetRequestsCounters();
5034 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5036 nestedScrollWebView.setVisibility(View.INVISIBLE);
5039 // Hide the keyboard.
5040 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5042 // Check to see if Privacy Browser is waiting on Orbot.
5043 if (!waitingForOrbot) { // Process the URL.
5044 // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
5045 formattedUrlString = url;
5047 // Display the formatted URL text.
5048 urlEditText.setText(formattedUrlString);
5050 // Apply text highlighting to `urlTextBox`.
5053 // Get a URI for the current URL.
5054 Uri currentUri = Uri.parse(formattedUrlString);
5056 // Reset the list of host IP addresses.
5057 nestedScrollWebView.clearCurrentIpAddresses();
5059 // Get the IP addresses for the host.
5060 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5062 // Apply any custom domain settings if the URL was loaded by navigating history.
5063 if (navigatingHistory) {
5064 // Apply the domain settings.
5065 boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5067 // Reset `navigatingHistory`.
5068 navigatingHistory = false;
5070 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5071 if (userAgentChanged) {
5072 loadUrl(formattedUrlString);
5076 // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.)
5077 if (refreshMenuItem != null) {
5079 refreshMenuItem.setTitle(R.string.stop);
5081 // If the icon is displayed in the AppBar, set it according to the theme.
5082 if (displayAdditionalAppBarIcons) {
5084 refreshMenuItem.setIcon(R.drawable.close_dark);
5086 refreshMenuItem.setIcon(R.drawable.close_light);
5093 // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
5095 public void onPageFinished(WebView view, String url) {
5096 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5097 if (!waitingForOrbot) {
5098 // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5099 nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5102 // Flush any cookies to persistent storage. `CookieManager` has become very lazy about flushing cookies in recent versions.
5103 if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) {
5104 cookieManager.flush();
5107 // Update the Refresh menu item if it has been created.
5108 if (refreshMenuItem != null) {
5109 // Reset the Refresh title.
5110 refreshMenuItem.setTitle(R.string.refresh);
5112 // If the icon is displayed in the AppBar, reset it according to the theme.
5113 if (displayAdditionalAppBarIcons) {
5115 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5117 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5123 // Clear the cache and history if Incognito Mode is enabled.
5124 if (incognitoModeEnabled) {
5125 // Clear the cache. `true` includes disk files.
5126 nestedScrollWebView.clearCache(true);
5128 // Clear the back/forward history.
5129 nestedScrollWebView.clearHistory();
5131 // Manually delete cache folders.
5133 // Delete the main cache directory.
5134 privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
5136 // Delete the secondary `Service Worker` cache directory.
5137 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5138 privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5139 } catch (IOException e) {
5140 // Do nothing if an error is thrown.
5144 // Update the URL text box and apply domain settings if not waiting on Orbot.
5145 if (!waitingForOrbot) {
5146 // Check to see if `WebView` has set `url` to be `about:blank`.
5147 if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
5148 // Set `formattedUrlString` to `""`.
5149 formattedUrlString = "";
5151 urlEditText.setText(formattedUrlString);
5153 // Request focus for `urlTextBox`.
5154 urlEditText.requestFocus();
5156 // Display the keyboard.
5157 inputMethodManager.showSoftInput(urlEditText, 0);
5159 // Apply the domain settings. This clears any settings from the previous domain.
5160 applyDomainSettings(nestedScrollWebView, formattedUrlString, true, false);
5161 } else { // `WebView` has loaded a webpage.
5162 // Set the formatted URL string. Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly.
5163 formattedUrlString = nestedScrollWebView.getUrl();
5165 // Only update the URL text box if the user is not typing in it.
5166 if (!urlEditText.hasFocus()) {
5167 // Display the formatted URL text.
5168 urlEditText.setText(formattedUrlString);
5170 // Apply text highlighting to `urlTextBox`.
5175 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5176 if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5177 !nestedScrollWebView.ignorePinnedDomainInformation()) {
5178 CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5183 // Handle SSL Certificate errors.
5185 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5186 // Get the current website SSL certificate.
5187 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5189 // Extract the individual pieces of information from the current website SSL certificate.
5190 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5191 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5192 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5193 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5194 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5195 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5196 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5197 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5199 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5200 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5201 // Get the pinned SSL certificate.
5202 ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5204 // Extract the arrays from the array list.
5205 String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5206 Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5208 // Check if the current SSL certificate matches the pinned certificate.
5209 if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5210 currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5211 currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5212 currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5214 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5217 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5218 // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
5219 sslErrorHandler = handler; // TODO. We need to pass this in instead of using a static variable. Because multiple could be displayed at once from different tabs.
5221 // Display the SSL error `AlertDialog`.
5222 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
5223 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5228 // Check to see if this is the first page.
5229 if (pageNumber == 0) {
5230 // Set this nested scroll WebView as the current WebView.
5231 currentWebView = nestedScrollWebView;
5233 // Apply the app settings from the shared preferences.
5236 // Load the website if not waiting for Orbot to connect.
5237 if (!waitingForOrbot) {
5238 loadUrl(formattedUrlString);