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.content.res.Resources;
40 import android.database.Cursor;
41 import android.graphics.Bitmap;
42 import android.graphics.BitmapFactory;
43 import android.graphics.Typeface;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.net.http.SslCertificate;
48 import android.net.http.SslError;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.preference.PreferenceManager;
54 import android.print.PrintDocumentAdapter;
55 import android.print.PrintManager;
56 import android.text.Editable;
57 import android.text.Spanned;
58 import android.text.TextWatcher;
59 import android.text.style.ForegroundColorSpan;
60 import android.util.Patterns;
61 import android.view.ContextMenu;
62 import android.view.GestureDetector;
63 import android.view.KeyEvent;
64 import android.view.Menu;
65 import android.view.MenuItem;
66 import android.view.MotionEvent;
67 import android.view.View;
68 import android.view.ViewGroup;
69 import android.view.WindowManager;
70 import android.view.inputmethod.InputMethodManager;
71 import android.webkit.CookieManager;
72 import android.webkit.HttpAuthHandler;
73 import android.webkit.SslErrorHandler;
74 import android.webkit.ValueCallback;
75 import android.webkit.WebBackForwardList;
76 import android.webkit.WebChromeClient;
77 import android.webkit.WebResourceResponse;
78 import android.webkit.WebSettings;
79 import android.webkit.WebStorage;
80 import android.webkit.WebView;
81 import android.webkit.WebViewClient;
82 import android.webkit.WebViewDatabase;
83 import android.widget.ArrayAdapter;
84 import android.widget.CursorAdapter;
85 import android.widget.EditText;
86 import android.widget.FrameLayout;
87 import android.widget.ImageView;
88 import android.widget.LinearLayout;
89 import android.widget.ListView;
90 import android.widget.ProgressBar;
91 import android.widget.RadioButton;
92 import android.widget.RelativeLayout;
93 import android.widget.TextView;
95 import androidx.annotation.NonNull;
96 import androidx.appcompat.app.ActionBar;
97 import androidx.appcompat.app.ActionBarDrawerToggle;
98 import androidx.appcompat.app.AppCompatActivity;
99 import androidx.appcompat.widget.Toolbar;
100 import androidx.core.app.ActivityCompat;
101 import androidx.core.content.ContextCompat;
102 import androidx.core.view.GravityCompat;
103 import androidx.drawerlayout.widget.DrawerLayout;
104 import androidx.fragment.app.DialogFragment;
105 import androidx.fragment.app.Fragment;
106 import androidx.fragment.app.FragmentManager;
107 import androidx.fragment.app.FragmentPagerAdapter;
108 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
109 import androidx.viewpager.widget.ViewPager;
111 import com.google.android.material.floatingactionbutton.FloatingActionButton;
112 import com.google.android.material.navigation.NavigationView;
113 import com.google.android.material.snackbar.Snackbar;
114 import com.google.android.material.tabs.TabLayout;
116 import com.stoutner.privacybrowser.BuildConfig;
117 import com.stoutner.privacybrowser.R;
118 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
119 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
121 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
122 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
124 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
125 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
126 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
127 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
128 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
129 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
130 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
131 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
132 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
133 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
134 import com.stoutner.privacybrowser.helpers.AdHelper;
135 import com.stoutner.privacybrowser.helpers.BlockListHelper;
136 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
138 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
139 import com.stoutner.privacybrowser.views.NestedScrollWebView;
141 import java.io.ByteArrayInputStream;
142 import java.io.ByteArrayOutputStream;
144 import java.io.IOException;
145 import java.io.UnsupportedEncodingException;
146 import java.net.MalformedURLException;
148 import java.net.URLDecoder;
149 import java.net.URLEncoder;
150 import java.util.ArrayList;
151 import java.util.Date;
152 import java.util.HashMap;
153 import java.util.HashSet;
154 import java.util.LinkedList;
155 import java.util.List;
156 import java.util.Map;
157 import java.util.Set;
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 // TODO Consider removing
166 // `darkTheme` is public static so it can be accessed from everywhere.
167 public static boolean darkTheme;
169 // TODO Consider removing
170 // `allowScreenshots` is public static so it can be accessed from everywhere. It is also used in `onCreate()`.
171 public static boolean allowScreenshots;
174 // `favoriteIconBitmap` is public static so it can be accessed from `BookmarksActivity`, `BookmarksDatabaseViewActivity`, `CreateBookmarkFolderDialog`,
175 // `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, and `ViewSslCertificateDialog`. It is also used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
176 // `onCreateHomeScreenShortcut()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `applyDomainSettings()`.
177 public static Bitmap favoriteIconBitmap;
180 // `favoriteIconDefaultBitmap` public static so it can be accessed from `PinnedMismatchDialog`. It is also used in `onCreate()` and `applyDomainSettings`.
181 public static Bitmap favoriteIconDefaultBitmap;
183 // TODO Consider removing the formatted URL string.
184 // `formattedUrlString` is public static so it can be accessed from `AddDomainDialog`, `BookmarksActivity`, `DomainSettingsFragment`, and `PinnedMismatchDialog`.
185 // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, `loadUrlFromTextBox()`, and `applyProxyThroughOrbot()`.
186 public static String formattedUrlString;
188 // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedMismatchDialog`, and `ViewSslCertificateDialog`.
189 // It is also used in `onCreate()` and `checkPinnedMismatch()`.
190 public static SslCertificate sslCertificate;
192 // `currentHostIpAddresses` is public static so it can be accessed from `DomainSettingsFragment`, `GetHostIpAddresses()`, and `ViewSslCertificateDialog`.
193 // It is also used in `onCreate()` and `GetHostIpAddresses()`.
194 public static String currentHostIpAddresses;
196 // The getting IP addresses tracker is used in `onCreate() and `GetHostIpAddresses`.
197 public static boolean gettingIpAddresses;
199 // The URL loading tracker is public static so it can be accessed from `GetHostIpAddresses`.
200 // It is also used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, `applyDomainSettings()`, and `GetHostIpAddresses`.
201 public static boolean urlIsLoading;
203 // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
204 public static String orbotStatus;
206 // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`. It is also used in `applyDomainSettings()`.
207 public static String appliedUserAgentString;
209 // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()`
210 public static boolean reloadOnRestart;
212 // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`. It is also used in `onRestart()`.
213 public static boolean loadUrlOnRestart;
215 // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`.
216 public static boolean restartFromBookmarksActivity;
218 // The block list versions are public static so they can be accessed from `AboutTabFragment`. They are also used in `onCreate()`.
219 public static String easyListVersion;
220 public static String easyPrivacyVersion;
221 public static String fanboysAnnoyanceVersion;
222 public static String fanboysSocialVersion;
223 public static String ultraPrivacyVersion;
225 // The request items are public static so they can be accessed by `BlockListHelper`, `RequestsArrayAdapter`, and `ViewRequestsDialog`. They are also used in `onCreate()` and `onPrepareOptionsMenu()`.
226 public static List<String[]> resourceRequests;
227 public static String[] whiteListResultStringArray;
228 private int blockedRequests;
229 private int easyListBlockedRequests;
230 private int easyPrivacyBlockedRequests;
231 private int fanboysAnnoyanceListBlockedRequests;
232 private int fanboysSocialBlockingListBlockedRequests;
233 private int ultraPrivacyBlockedRequests;
234 private int thirdPartyBlockedRequests;
236 public final static int REQUEST_DISPOSITION = 0;
237 public final static int REQUEST_URL = 1;
238 public final static int REQUEST_BLOCKLIST = 2;
239 public final static int REQUEST_SUBLIST = 3;
240 public final static int REQUEST_BLOCKLIST_ENTRIES = 4;
241 public final static int REQUEST_BLOCKLIST_ORIGINAL_ENTRY = 5;
243 public final static int REQUEST_DEFAULT = 0;
244 public final static int REQUEST_ALLOWED = 1;
245 public final static int REQUEST_THIRD_PARTY = 2;
246 public final static int REQUEST_BLOCKED = 3;
248 public final static int MAIN_WHITELIST = 1;
249 public final static int FINAL_WHITELIST = 2;
250 public final static int DOMAIN_WHITELIST = 3;
251 public final static int DOMAIN_INITIAL_WHITELIST = 4;
252 public final static int DOMAIN_FINAL_WHITELIST = 5;
253 public final static int THIRD_PARTY_WHITELIST = 6;
254 public final static int THIRD_PARTY_DOMAIN_WHITELIST = 7;
255 public final static int THIRD_PARTY_DOMAIN_INITIAL_WHITELIST = 8;
257 public final static int MAIN_BLACKLIST = 9;
258 public final static int INITIAL_BLACKLIST = 10;
259 public final static int FINAL_BLACKLIST = 11;
260 public final static int DOMAIN_BLACKLIST = 12;
261 public final static int DOMAIN_INITIAL_BLACKLIST = 13;
262 public final static int DOMAIN_FINAL_BLACKLIST = 14;
263 public final static int DOMAIN_REGULAR_EXPRESSION_BLACKLIST = 15;
264 public final static int THIRD_PARTY_BLACKLIST = 16;
265 public final static int THIRD_PARTY_INITIAL_BLACKLIST = 17;
266 public final static int THIRD_PARTY_DOMAIN_BLACKLIST = 18;
267 public final static int THIRD_PARTY_DOMAIN_INITIAL_BLACKLIST = 19;
268 public final static int THIRD_PARTY_REGULAR_EXPRESSION_BLACKLIST = 20;
269 public final static int THIRD_PARTY_DOMAIN_REGULAR_EXPRESSION_BLACKLIST = 21;
270 public final static int REGULAR_EXPRESSION_BLACKLIST = 22;
272 // `blockAllThirdPartyRequests` is public static so it can be accessed from `RequestsActivity`.
273 // It is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`
274 public static boolean blockAllThirdPartyRequests;
276 // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
277 // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
278 public static String currentBookmarksFolder;
280 // The pinned variables are public static so they can be accessed from `PinnedMismatchDialog`. They are also used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
281 public static String pinnedSslIssuedToCName;
282 public static String pinnedSslIssuedToOName;
283 public static String pinnedSslIssuedToUName;
284 public static String pinnedSslIssuedByCName;
285 public static String pinnedSslIssuedByOName;
286 public static String pinnedSslIssuedByUName;
287 public static Date pinnedSslStartDate;
288 public static Date pinnedSslEndDate;
289 public static String pinnedHostIpAddresses;
291 // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
292 public final static int UNRECOGNIZED_USER_AGENT = -1;
293 public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
294 public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
295 public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
296 public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
297 public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
301 // `pinnedDomainSslCertificate` is used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
302 private static boolean pinnedSslCertificate;
304 // `pinnedIpAddress` is used in `applyDomainSettings()` and `checkPinnedMismatch()`.
305 private static boolean pinnedIpAddresses;
307 // `ignorePinnedDomainInformation` is used in `onSslMismatchProceed()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
308 private static boolean ignorePinnedDomainInformation;
310 // The fragment manager is initialized in `onCreate()` and accessed from the static `checkPinnedMismatch()`.
311 private static FragmentManager fragmentManager;
314 // A handle for the activity is set in `onCreate()` and accessed in `WebViewPagerAdapter`.
315 private Activity activity;
317 // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
318 private boolean navigatingHistory;
320 // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
321 // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
322 private NestedScrollWebView currentWebView;
324 // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
325 private FrameLayout fullScreenVideoFrameLayout;
327 // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`.
328 private CookieManager cookieManager;
330 // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
331 private final Map<String, String> customHeaders = new HashMap<>();
333 // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`.
334 private boolean javaScriptEnabled;
336 // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`.
337 private boolean firstPartyCookiesEnabled;
339 // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
340 private boolean thirdPartyCookiesEnabled;
342 // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
343 private boolean domStorageEnabled;
345 // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. It can be removed once the minimum API >= 26.
346 private boolean saveFormDataEnabled;
348 // `nightMode` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
349 private boolean nightMode;
351 // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`.
352 private String homepage;
354 // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`.
355 private String searchURL;
357 // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`.
358 private Menu mainMenu;
360 // `refreshMenuItem` is used in `onCreate()` and `onCreateOptionsMenu()`.
361 private MenuItem refreshMenuItem;
363 // The WebView pager adapter is used in `onCreate()`, `onResume()`, and `addTab()`.
364 private WebViewPagerAdapter webViewPagerAdapter;
366 // The navigation requests menu item is used in `onCreate()` and accessed from `WebViewPagerAdapter`.
367 private MenuItem navigationRequestsMenuItem;
369 // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
370 BlockListHelper blockListHelper;
372 // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
373 private ArrayList<List<String[]>> easyList;
374 private ArrayList<List<String[]>> easyPrivacy;
375 private ArrayList<List<String[]>> fanboysAnnoyanceList;
376 private ArrayList<List<String[]>> fanboysSocialList;
377 private ArrayList<List<String[]>> ultraPrivacy;
379 // The blocklist menu items are used in `onCreate()`, `onCreateOptionsMenu()`, and `onPrepareOptionsMenu()`.
380 private MenuItem blocklistsMenuItem;
381 private MenuItem easyListMenuItem;
382 private MenuItem easyPrivacyMenuItem;
383 private MenuItem fanboysAnnoyanceListMenuItem;
384 private MenuItem fanboysSocialBlockingListMenuItem;
385 private MenuItem ultraPrivacyMenuItem;
386 private MenuItem blockAllThirdPartyRequestsMenuItem;
388 // The blocklist variables are used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
389 private boolean easyListEnabled;
390 private boolean easyPrivacyEnabled;
391 private boolean fanboysAnnoyanceListEnabled;
392 private boolean fanboysSocialBlockingListEnabled;
393 private boolean ultraPrivacyEnabled;
395 // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
396 private String webViewDefaultUserAgent;
398 // `defaultCustomUserAgentString` is used in `onPrepareOptionsMenu()` and `applyDomainSettings()`.
399 private String defaultCustomUserAgentString;
401 // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
402 private Runtime privacyBrowserRuntime;
404 // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
405 private boolean proxyThroughOrbot;
407 // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
408 private boolean incognitoModeEnabled;
410 // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
411 private boolean fullScreenBrowsingModeEnabled;
413 // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
414 private boolean inFullScreenBrowsingMode;
416 // Hide app bar is used in `onCreate()` and `applyAppSettings()`.
417 private boolean hideAppBar;
419 // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
420 private boolean reapplyDomainSettingsOnRestart;
422 // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
423 private boolean reapplyAppSettingsOnRestart;
425 // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
426 private boolean displayingFullScreenVideo;
428 // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`.
429 private boolean downloadWithExternalApp;
431 // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`.
432 private String currentDomainName;
434 // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
435 private BroadcastReceiver orbotStatusBroadcastReceiver;
437 // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
438 private boolean waitingForOrbot;
440 // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`.
441 private Boolean domainSettingsJavaScriptEnabled;
443 // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`.
444 private String waitingForOrbotHtmlString;
446 // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`.
447 private String privateDataDirectoryString;
449 // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
450 private EditText findOnPageEditText;
452 // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`.
453 private boolean displayAdditionalAppBarIcons;
455 // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
456 private ActionBarDrawerToggle actionBarDrawerToggle;
458 // The color spans are used in `onCreate()` and `highlightUrlText()`.
459 private ForegroundColorSpan redColorSpan;
460 private ForegroundColorSpan initialGrayColorSpan;
461 private ForegroundColorSpan finalGrayColorSpan;
463 // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
464 private int drawerHeaderPaddingLeftAndRight;
465 private int drawerHeaderPaddingTop;
466 private int drawerHeaderPaddingBottom;
468 // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
469 private SslErrorHandler sslErrorHandler;
471 // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`.
472 private static HttpAuthHandler httpAuthHandler;
474 // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
475 private InputMethodManager inputMethodManager;
477 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
478 // and `loadBookmarksFolder()`.
479 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
481 // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
482 private ListView bookmarksListView;
484 // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`.
485 private TextView bookmarksTitleTextView;
487 // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
488 private Cursor bookmarksCursor;
490 // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
491 private CursorAdapter bookmarksCursorAdapter;
493 // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
494 private String oldFolderNameString;
496 // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
497 private ValueCallback<Uri[]> fileChooserCallback;
499 // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
500 private String downloadUrl;
501 private String downloadContentDisposition;
502 private long downloadContentLength;
504 // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
505 private String downloadImageUrl;
507 // The user agent variables are used in `onCreate()` and `applyDomainSettings()`.
508 private ArrayAdapter<CharSequence> userAgentNamesArray;
509 private String[] userAgentDataArray;
511 // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
512 private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
513 private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
516 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. The whole premise of Privacy Browser is built around an understanding of these dangers.
517 // Also, remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
518 @SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
519 // Remove Android Studio's warning about deprecations. The deprecated `getColor()` must be used until API >= 23.
520 @SuppressWarnings("deprecation")
521 protected void onCreate(Bundle savedInstanceState) {
522 // Get a handle for the shared preferences.
523 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
525 // Get the theme and screenshot preferences.
526 darkTheme = sharedPreferences.getBoolean("dark_theme", false);
527 allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
529 // Disable screenshots if not allowed.
530 if (!allowScreenshots) {
531 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
534 // Set the activity theme.
536 setTheme(R.style.PrivacyBrowserDark);
538 setTheme(R.style.PrivacyBrowserLight);
541 // Run the default commands.
542 super.onCreate(savedInstanceState);
544 // Set the content view.
545 setContentView(R.layout.main_framelayout);
547 // Get handles for views, resources, and managers.
549 Resources resources = getResources();
550 fragmentManager = getSupportFragmentManager();
551 inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
552 Toolbar toolbar = findViewById(R.id.toolbar);
554 // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21.
555 setSupportActionBar(toolbar);
556 ActionBar actionBar = getSupportActionBar();
558 // This is needed to get rid of the Android Studio warning that the action bar might be null.
559 assert actionBar != null;
561 // Add the custom `url_app_bar` layout, which shows the favorite icon and the URL text bar.
562 actionBar.setCustomView(R.layout.url_app_bar);
563 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
565 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
566 redColorSpan = new ForegroundColorSpan(resources.getColor(R.color.red_a700));
567 initialGrayColorSpan = new ForegroundColorSpan(resources.getColor(R.color.gray_500));
568 finalGrayColorSpan = new ForegroundColorSpan(resources.getColor(R.color.gray_500));
570 // Get a handle for `urlTextBox`.
571 EditText urlEditText = findViewById(R.id.url_edittext);
573 // Remove the formatting from `urlTextBar` when the user is editing the text.
574 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
575 if (hasFocus) { // The user is editing the URL text box.
576 // Remove the highlighting.
577 urlEditText.getText().removeSpan(redColorSpan);
578 urlEditText.getText().removeSpan(initialGrayColorSpan);
579 urlEditText.getText().removeSpan(finalGrayColorSpan);
580 } else { // The user has stopped editing the URL text box.
581 // Move to the beginning of the string.
582 urlEditText.setSelection(0);
584 // Reapply the highlighting.
589 // Set the go button on the keyboard to load the URL in `urlTextBox`.
590 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
591 // If the event is a key-down event on the `enter` button, load the URL.
592 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
593 // Load the URL into the mainWebView and consume the event.
594 loadUrlFromTextBox();
596 // If the enter key was pressed, consume the event.
599 // If any other key was pressed, do not consume the event.
604 // Set `waitingForOrbotHTMLString`.
605 waitingForOrbotHtmlString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
607 // Initialize `currentDomainName`, `orbotStatus`, and `waitingForOrbot`.
608 currentDomainName = "";
609 orbotStatus = "unknown";
610 waitingForOrbot = false;
612 // Create an Orbot status `BroadcastReceiver`.
613 orbotStatusBroadcastReceiver = new BroadcastReceiver() {
615 public void onReceive(Context context, Intent intent) {
616 // Store the content of the status message in `orbotStatus`.
617 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
619 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
620 if (orbotStatus.equals("ON") && waitingForOrbot) {
621 // Reset `waitingForOrbot`.
622 waitingForOrbot = false;
624 // Load `formattedUrlString
625 loadUrl(formattedUrlString);
630 // Register `orbotStatusBroadcastReceiver` on `this` context.
631 this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
633 // Instantiate the block list helper.
634 blockListHelper = new BlockListHelper();
636 // Initialize the list of resource requests.
637 resourceRequests = new ArrayList<>();
639 // Parse the block lists.
640 easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
641 easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
642 fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
643 fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
644 ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
646 // Store the list versions.
647 easyListVersion = easyList.get(0).get(0)[0];
648 easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
649 fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
650 fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
651 ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
653 // Get handles for views that need to be modified.
654 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
655 final NavigationView navigationView = findViewById(R.id.navigationview);
656 TabLayout tabLayout = findViewById(R.id.tablayout);
657 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
658 ViewPager webViewPager = findViewById(R.id.webviewpager);
659 bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
660 bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
661 FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
662 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
663 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
664 findOnPageEditText = findViewById(R.id.find_on_page_edittext);
665 fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
667 // Listen for touches on the navigation menu.
668 navigationView.setNavigationItemSelectedListener(this);
670 // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
671 final Menu navigationMenu = navigationView.getMenu();
672 final MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
673 final MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
674 final MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
675 final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
676 navigationRequestsMenuItem = navigationMenu.getItem(6);
678 // Initialize the web view pager adapter.
679 webViewPagerAdapter = new WebViewPagerAdapter(fragmentManager);
681 // Set the pager adapter on the web view pager.
682 webViewPager.setAdapter(webViewPagerAdapter);
684 // Store up to 100 tabs in memory.
685 webViewPager.setOffscreenPageLimit(100);
687 // Update the web view pager every time a tab is modified.
688 webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
690 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
695 public void onPageSelected(int position) {
696 // Get the WebView tab fragment.
697 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.webViewFragmentsList.get(position);
699 // Get the fragment view.
700 View fragmentView = webViewTabFragment.getView();
702 // Remove the incorrect lint warning below that the fragment view might be null.
703 assert fragmentView != null;
705 // Store the current WebView.
706 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
708 // Store the current formatted URL string.
709 formattedUrlString = currentWebView.getUrl();
711 // Clear the focus from the URL text box.
712 urlEditText.clearFocus();
714 // Hide the soft keyboard.
715 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
717 // Display the current URL in the URL text box.
718 urlEditText.setText(formattedUrlString);
720 // Highlight the URL text.
723 // Set the background to indicate the domain settings status.
724 if (currentWebView.getDomainSettingsApplied()) {
725 // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
727 urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
729 urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
732 urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
735 // 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.
736 if (tabLayout.getSelectedTabPosition() != position) {
737 // Get a handle for the corresponding tab.
738 TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
740 // Assert that the corresponding tab is not null.
741 assert correspondingTab != null;
743 // Select the corresponding tab.
744 correspondingTab.select();
749 public void onPageScrollStateChanged(int state) {
754 // Display the View SSL Certificate dialog when the currently selected tab is reselected.
755 tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
757 public void onTabSelected(TabLayout.Tab tab) {
758 // Select the same page in the view pager.
759 webViewPager.setCurrentItem(tab.getPosition());
763 public void onTabUnselected(TabLayout.Tab tab) {
768 public void onTabReselected(TabLayout.Tab tab) {
769 // Instantiate the View SSL Certificate dialog.
770 DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog();
772 // Display the View SSL Certificate dialog.
773 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
777 // Add the first tab.
778 webViewPagerAdapter.addPage();
780 // 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.
782 launchBookmarksActivityFab.setImageDrawable(resources.getDrawable(R.drawable.bookmarks_dark));
783 createBookmarkFolderFab.setImageDrawable(resources.getDrawable(R.drawable.create_folder_dark));
784 createBookmarkFab.setImageDrawable(resources.getDrawable(R.drawable.create_bookmark_dark));
785 bookmarksListView.setBackgroundColor(resources.getColor(R.color.gray_850));
787 launchBookmarksActivityFab.setImageDrawable(resources.getDrawable(R.drawable.bookmarks_light));
788 createBookmarkFolderFab.setImageDrawable(resources.getDrawable(R.drawable.create_folder_light));
789 createBookmarkFab.setImageDrawable(resources.getDrawable(R.drawable.create_bookmark_light));
790 bookmarksListView.setBackgroundColor(resources.getColor(R.color.white));
793 // Set the launch bookmarks activity FAB to launch the bookmarks activity.
794 launchBookmarksActivityFab.setOnClickListener(v -> {
795 // Store the current WebView url and title in the bookmarks activity.
796 BookmarksActivity.currentWebViewUrl = currentWebView.getUrl();
797 BookmarksActivity.currentWebViewTitle = currentWebView.getTitle();
799 // Create an intent to launch the bookmarks activity.
800 Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
802 // Include the current folder with the `Intent`.
803 bookmarksIntent.putExtra("Current Folder", currentBookmarksFolder);
806 startActivity(bookmarksIntent);
809 // Set the create new bookmark folder FAB to display an alert dialog.
810 createBookmarkFolderFab.setOnClickListener(v -> {
811 // Show the create bookmark folder dialog and name the instance `@string/create_folder`.
812 DialogFragment createBookmarkFolderDialog = new CreateBookmarkFolderDialog();
813 createBookmarkFolderDialog.show(fragmentManager, resources.getString(R.string.create_folder));
816 // Set the create new bookmark FAB to display an alert dialog.
817 createBookmarkFab.setOnClickListener(view -> {
818 // Instantiate the create bookmark dialog.
819 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), favoriteIconBitmap);
821 // Display the create bookmark dialog.
822 createBookmarkDialog.show(fragmentManager, resources.getString(R.string.create_bookmark));
825 // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
826 findOnPageEditText.addTextChangedListener(new TextWatcher() {
828 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
833 public void onTextChanged(CharSequence s, int start, int before, int count) {
838 public void afterTextChanged(Editable s) {
839 // Search for the text in `mainWebView`.
840 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
844 // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
845 findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
846 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
847 // Hide the soft keyboard.
848 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
850 // Consume the event.
852 } else { // A different key was pressed.
853 // Do not consume the event.
858 // Implement swipe to refresh.
859 swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
861 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
862 swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
864 // Set the swipe to refresh color according to the theme.
866 swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
867 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
869 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
872 // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
873 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
874 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
876 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
877 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
879 // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
880 currentBookmarksFolder = "";
882 // Load the home folder, which is `""` in the database.
883 loadBookmarksFolder();
885 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
886 // Convert the id from long to int to match the format of the bookmarks database.
887 int databaseID = (int) id;
889 // Get the bookmark cursor for this ID and move it to the first row.
890 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
891 bookmarkCursor.moveToFirst();
893 // Act upon the bookmark according to the type.
894 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
895 // Store the new folder name in `currentBookmarksFolder`.
896 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
898 // Load the new folder.
899 loadBookmarksFolder();
900 } else { // The selected bookmark is not a folder.
901 // Load the bookmark URL.
902 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
904 // Close the bookmarks drawer.
905 drawerLayout.closeDrawer(GravityCompat.END);
908 // Close the `Cursor`.
909 bookmarkCursor.close();
912 bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
913 // Convert the database ID from `long` to `int`.
914 int databaseId = (int) id;
916 // Find out if the selected bookmark is a folder.
917 boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
920 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
921 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
923 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
924 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId);
925 editBookmarkFolderDialog.show(fragmentManager, resources.getString(R.string.edit_folder));
927 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
928 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId);
929 editBookmarkDialog.show(fragmentManager, resources.getString(R.string.edit_bookmark));
932 // Consume the event.
936 // Get the status bar pixel size.
937 int statusBarResourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
938 int statusBarPixelSize = resources.getDimensionPixelSize(statusBarResourceId);
940 // Get the resource density.
941 float screenDensity = resources.getDisplayMetrics().density;
943 // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
944 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
945 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
946 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
948 // The drawer listener is used to update the navigation menu.`
949 drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
951 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
955 public void onDrawerOpened(@NonNull View drawerView) {
959 public void onDrawerClosed(@NonNull View drawerView) {
963 public void onDrawerStateChanged(int newState) {
964 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
965 // Get handles for the drawer headers.
966 TextView navigationHeaderTextView = findViewById(R.id.navigationText);
967 TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
969 // 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.
970 if (navigationHeaderTextView != null) {
971 navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
974 // 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.
975 if (bookmarksHeaderTextView != null) {
976 bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
979 // Update the navigation menu items.
980 navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
981 navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
982 navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
983 navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
984 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
986 // Hide the keyboard (if displayed).
987 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
989 // 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.
990 urlEditText.clearFocus();
991 currentWebView.clearFocus();
996 // Create the hamburger icon at the start of the AppBar.
997 actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
999 // Initialize cookieManager.
1000 cookieManager = CookieManager.getInstance();
1002 // 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).
1003 customHeaders.put("X-Requested-With", "");
1005 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
1006 PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
1008 // Get a handle for the `Runtime`.
1009 privacyBrowserRuntime = Runtime.getRuntime();
1011 // Store the application's private data directory.
1012 privateDataDirectoryString = getApplicationInfo().dataDir;
1013 // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
1015 // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
1016 inFullScreenBrowsingMode = false;
1018 // Initialize the privacy settings variables.
1019 javaScriptEnabled = false;
1020 firstPartyCookiesEnabled = false;
1021 thirdPartyCookiesEnabled = false;
1022 domStorageEnabled = false;
1023 saveFormDataEnabled = false; // Form data can be removed once the minimum API >= 26.
1026 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
1027 @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
1029 // Get a handle for the WebView.
1030 WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
1032 // Store the default user agent.
1033 webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
1035 // Destroy the bare WebView.
1036 bareWebView.destroy();
1038 // Initialize the favorite icon bitmap. `ContextCompat` must be used until API >= 21.
1039 Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
1040 BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
1041 assert favoriteIconBitmapDrawable != null;
1042 favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
1044 // If the favorite icon is null, load the default.
1045 if (favoriteIconBitmap == null) {
1046 favoriteIconBitmap = favoriteIconDefaultBitmap;
1049 // Initialize the user agent array adapter and string array.
1050 userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
1051 userAgentDataArray = resources.getStringArray(R.array.user_agent_data);
1053 // Get the intent that started the app.
1054 Intent launchingIntent = getIntent();
1056 // Get the information from the intent.
1057 String launchingIntentAction = launchingIntent.getAction();
1058 Uri launchingIntentUriData = launchingIntent.getData();
1060 // If the intent action is a web search, perform the search.
1061 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
1062 // Create an encoded URL string.
1063 String encodedUrlString;
1065 // Sanitize the search input and convert it to a search.
1067 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
1068 } catch (UnsupportedEncodingException exception) {
1069 encodedUrlString = "";
1072 // Add the base search URL.
1073 formattedUrlString = searchURL + encodedUrlString;
1074 } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
1075 // Set the formatted URL string.
1076 formattedUrlString = launchingIntentUriData.toString();
1081 protected void onNewIntent(Intent intent) {
1082 // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
1085 // Get the information from the intent.
1086 String intentAction = intent.getAction();
1087 Uri intentUriData = intent.getData();
1089 // If the intent action is a web search, perform the search.
1090 if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
1091 // Create an encoded URL string.
1092 String encodedUrlString;
1094 // Sanitize the search input and convert it to a search.
1096 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
1097 } catch (UnsupportedEncodingException exception) {
1098 encodedUrlString = "";
1101 // Add the base search URL.
1102 formattedUrlString = searchURL + encodedUrlString;
1103 } else if (intentUriData != null){ // Check to see if the intent contains a new URL.
1104 // Set the formatted URL string.
1105 formattedUrlString = intentUriData.toString();
1109 loadUrl(formattedUrlString);
1111 // Get a handle for the drawer layout.
1112 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1114 // Close the navigation drawer if it is open.
1115 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
1116 drawerLayout.closeDrawer(GravityCompat.START);
1119 // Close the bookmarks drawer if it is open.
1120 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
1121 drawerLayout.closeDrawer(GravityCompat.END);
1124 // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
1125 currentWebView.requestFocus();
1129 public void onRestart() {
1130 // Run the default commands.
1133 // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
1134 if (proxyThroughOrbot) {
1135 // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
1136 Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
1138 // Send the intent to the Orbot package.
1139 orbotIntent.setPackage("org.torproject.android");
1142 sendBroadcast(orbotIntent);
1145 // Apply the app settings if returning from the Settings activity..
1146 if (reapplyAppSettingsOnRestart) {
1147 // Apply the app settings.
1150 // Reload the webpage if displaying of images has been disabled in the Settings activity.
1151 if (reloadOnRestart) {
1152 // Reload the WebViews.
1153 for (int i = 0; i < webViewPagerAdapter.webViewFragmentsList.size(); i++) {
1154 // Get the WebView tab fragment.
1155 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.webViewFragmentsList.get(i);
1157 // Get the fragment view.
1158 View fragmentView = webViewTabFragment.getView();
1160 // Only reload the WebViews if they exist.
1161 if (fragmentView != null) {
1162 // Get the nested scroll WebView from the tab fragment.
1163 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1165 // TODO this doesn't seem to work if for WebViews that aren't visible.
1166 // Reload the WebView.
1167 nestedScrollWebView.reload();
1171 // Reset `reloadOnRestartBoolean`.
1172 reloadOnRestart = false;
1175 // Reset the return from settings flag.
1176 reapplyAppSettingsOnRestart = false;
1179 // Apply the domain settings if returning from the Domains activity.
1180 if (reapplyDomainSettingsOnRestart) {
1181 // Reapply the domain settings.
1182 applyDomainSettings(formattedUrlString, false, true);
1184 // Reset `reapplyDomainSettingsOnRestart`.
1185 reapplyDomainSettingsOnRestart = false;
1188 // Load the URL on restart to apply changes to night mode.
1189 if (loadUrlOnRestart) {
1190 // Load the current `formattedUrlString`.
1191 loadUrl(formattedUrlString);
1193 // Reset `loadUrlOnRestart.
1194 loadUrlOnRestart = false;
1197 // Update the bookmarks drawer if returning from the Bookmarks activity.
1198 if (restartFromBookmarksActivity) {
1199 // Get a handle for the drawer layout.
1200 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1202 // Close the bookmarks drawer.
1203 drawerLayout.closeDrawer(GravityCompat.END);
1205 // Reload the bookmarks drawer.
1206 loadBookmarksFolder();
1208 // Reset `restartFromBookmarksActivity`.
1209 restartFromBookmarksActivity = false;
1212 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
1213 updatePrivacyIcons(true);
1216 // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
1218 public void onResume() {
1219 // Run the default commands.
1222 for (int i = 0; i < webViewPagerAdapter.webViewFragmentsList.size(); i++) {
1223 // Get the WebView tab fragment.
1224 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.webViewFragmentsList.get(i);
1226 // Get the fragment view.
1227 View fragmentView = webViewTabFragment.getView();
1229 // Only resume the WebViews if they exist (they won't when the app is first created).
1230 if (fragmentView != null) {
1231 // Get the nested scroll WebView from the tab fragment.
1232 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1234 // Pause the nested scroll WebView JavaScript timers.
1235 nestedScrollWebView.resumeTimers();
1237 // Pause the nested scroll WebView.
1238 nestedScrollWebView.onResume();
1242 // Display a message to the user if waiting for Orbot.
1243 if (waitingForOrbot && !orbotStatus.equals("ON")) {
1244 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
1245 currentWebView.getSettings().setUseWideViewPort(false);
1247 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
1248 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
1251 if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
1252 // Get a handle for the root frame layouts.
1253 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1255 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1256 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1258 /* Hide the system bars.
1259 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1260 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1261 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1262 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1264 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1265 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1266 } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
1268 AdHelper.resumeAd(findViewById(R.id.adview));
1273 public void onPause() {
1274 // Run the default commands.
1277 for (int i = 0; i < webViewPagerAdapter.webViewFragmentsList.size(); i++) {
1278 // Get the WebView tab fragment.
1279 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.webViewFragmentsList.get(i);
1281 // Get the fragment view.
1282 View fragmentView = webViewTabFragment.getView();
1284 // Only pause the WebViews if they exist (they won't when the app is first created).
1285 if (fragmentView != null) {
1286 // Get the nested scroll WebView from the tab fragment.
1287 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1289 // Pause the nested scroll WebView.
1290 nestedScrollWebView.onPause();
1292 // Pause the nested scroll WebView JavaScript timers.
1293 nestedScrollWebView.pauseTimers();
1297 // Pause the ad or it will continue to consume resources in the background on the free flavor.
1298 if (BuildConfig.FLAVOR.contentEquals("free")) {
1300 AdHelper.pauseAd(findViewById(R.id.adview));
1305 public void onDestroy() {
1306 // Unregister the Orbot status broadcast receiver.
1307 this.unregisterReceiver(orbotStatusBroadcastReceiver);
1309 // Close the bookmarks cursor and database.
1310 bookmarksCursor.close();
1311 bookmarksDatabaseHelper.close();
1313 // Run the default commands.
1318 public boolean onCreateOptionsMenu(Menu menu) {
1319 // Inflate the menu. This adds items to the action bar if it is present.
1320 getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1322 // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
1325 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1326 updatePrivacyIcons(false);
1328 // Get handles for the menu items.
1329 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1330 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1331 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1332 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1333 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1334 refreshMenuItem = menu.findItem(R.id.refresh);
1335 blocklistsMenuItem = menu.findItem(R.id.blocklists);
1336 easyListMenuItem = menu.findItem(R.id.easylist);
1337 easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1338 fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1339 fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1340 ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1341 blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1342 MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1344 // Only display third-party cookies if API >= 21
1345 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1347 // Only display the form data menu items if the API < 26.
1348 toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1349 clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1351 // Only show Ad Consent if this is the free flavor.
1352 adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1354 // Get the shared preference values.
1355 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1357 // Get the status of the additional AppBar icons.
1358 displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1360 // 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.
1361 if (displayAdditionalAppBarIcons) {
1362 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1363 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1364 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1365 } else { //Do not display the additional icons.
1366 toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1367 toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1368 refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1371 // Replace Refresh with Stop if a URL is already loading.
1374 refreshMenuItem.setTitle(R.string.stop);
1376 // If the icon is displayed in the AppBar, set it according to the theme.
1377 if (displayAdditionalAppBarIcons) {
1379 refreshMenuItem.setIcon(R.drawable.close_dark);
1381 refreshMenuItem.setIcon(R.drawable.close_light);
1390 public boolean onPrepareOptionsMenu(Menu menu) {
1391 // Get a handle for the swipe refresh layout.
1392 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1394 // Get handles for the menu items.
1395 MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1396 MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1397 MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1398 MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1399 MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
1400 MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1401 MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1402 MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1403 MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
1404 MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1405 MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1406 MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1407 MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1408 MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1410 // Set the text for the domain menu item.
1411 if (currentWebView.getDomainSettingsApplied()) {
1412 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1414 addOrEditDomain.setTitle(R.string.add_domain_settings);
1417 // Set the status of the menu item checkboxes.
1418 toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
1419 toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled);
1420 toggleDomStorageMenuItem.setChecked(domStorageEnabled);
1421 toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); // Form data can be removed once the minimum API >= 26.
1422 easyListMenuItem.setChecked(easyListEnabled);
1423 easyPrivacyMenuItem.setChecked(easyPrivacyEnabled);
1424 fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled);
1425 fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled);
1426 ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled);
1427 blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
1428 swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
1429 displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1430 nightModeMenuItem.setChecked(nightMode);
1431 proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1433 // Enable third-party cookies if first-party cookies are enabled.
1434 toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
1436 // Enable DOM Storage if JavaScript is enabled.
1437 toggleDomStorageMenuItem.setEnabled(javaScriptEnabled);
1439 // Enable Clear Cookies if there are any.
1440 clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1442 // Get a count of the number of files in the Local Storage directory.
1443 File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1444 int localStorageDirectoryNumberOfFiles = 0;
1445 if (localStorageDirectory.exists()) {
1446 localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1449 // Get a count of the number of files in the IndexedDB directory.
1450 File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1451 int indexedDBDirectoryNumberOfFiles = 0;
1452 if (indexedDBDirectory.exists()) {
1453 indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1456 // Enable Clear DOM Storage if there is any.
1457 clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1459 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1460 if (Build.VERSION.SDK_INT < 26) {
1461 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
1462 clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
1464 // Disable clear form data because it is not supported on current version of Android.
1465 clearFormDataMenuItem.setEnabled(false);
1468 // Enable Clear Data if any of the submenu items are enabled.
1469 clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1471 // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked.
1472 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
1474 // Initialize the display names for the blocklists with the number of blocked requests.
1475 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + blockedRequests);
1476 easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
1477 easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
1478 fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
1479 fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
1480 ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
1481 blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
1483 // Get the current user agent.
1484 String currentUserAgent = currentWebView.getSettings().getUserAgentString();
1486 // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
1487 if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
1488 menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1489 } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
1490 menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1491 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
1492 menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1493 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
1494 menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1495 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
1496 menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1497 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
1498 menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1499 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
1500 menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1501 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
1502 menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1503 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
1504 menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1505 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
1506 menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1507 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
1508 menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1509 } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
1510 menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1511 } else { // Custom user agent.
1512 menu.findItem(R.id.user_agent_custom).setChecked(true);
1515 // Initialize font size variables.
1516 int fontSize = currentWebView.getSettings().getTextZoom();
1517 String fontSizeTitle;
1518 MenuItem selectedFontSizeMenuItem;
1520 // Prepare the font size title and current size menu item.
1523 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1524 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1528 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1529 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1533 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1534 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1538 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1539 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1543 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1544 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1548 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1549 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1553 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1554 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1558 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1559 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1563 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1564 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1568 // Set the font size title and select the current size menu item.
1569 fontSizeMenuItem.setTitle(fontSizeTitle);
1570 selectedFontSizeMenuItem.setChecked(true);
1572 // Run all the other default commands.
1573 super.onPrepareOptionsMenu(menu);
1575 // Display the menu.
1580 // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1581 @SuppressLint("SetJavaScriptEnabled")
1582 // removeAllCookies is deprecated, but it is required for API < 21.
1583 @SuppressWarnings("deprecation")
1584 public boolean onOptionsItemSelected(MenuItem menuItem) {
1585 // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
1586 if (inFullScreenBrowsingMode) {
1587 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
1588 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1590 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1592 /* Hide the system bars.
1593 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1594 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1595 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1596 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1598 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1599 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1602 // Get the selected menu item ID.
1603 int menuItemId = menuItem.getItemId();
1605 // Run the commands that correlate to the selected menu item.
1606 switch (menuItemId) {
1607 case R.id.toggle_javascript:
1608 // Switch the status of javaScriptEnabled.
1609 javaScriptEnabled = !javaScriptEnabled;
1611 // Apply the new JavaScript status.
1612 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
1614 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1615 updatePrivacyIcons(true);
1617 // Display a `Snackbar`.
1618 if (javaScriptEnabled) { // JavaScrip is enabled.
1619 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1620 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled.
1621 Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1622 } else { // Privacy mode.
1623 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1626 // Reload the current WebView.
1627 currentWebView.reload();
1630 case R.id.add_or_edit_domain:
1631 if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
1632 // Reapply the domain settings on returning to `MainWebViewActivity`.
1633 reapplyDomainSettingsOnRestart = true;
1634 currentDomainName = "";
1636 // Create an intent to launch the domains activity.
1637 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1639 // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
1640 domainsIntent.putExtra("loadDomain", currentWebView.getDomainSettingsDatabaseId());
1641 domainsIntent.putExtra("closeOnBack", true);
1644 startActivity(domainsIntent);
1645 } else { // Add a new domain.
1646 // Apply the new domain settings on returning to `MainWebViewActivity`.
1647 reapplyDomainSettingsOnRestart = true;
1648 currentDomainName = "";
1650 // Get the current domain
1651 Uri currentUri = Uri.parse(formattedUrlString);
1652 String currentDomain = currentUri.getHost();
1654 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1655 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1657 // Create the domain and store the database ID.
1658 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1660 // Create an intent to launch the domains activity.
1661 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1663 // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
1664 domainsIntent.putExtra("loadDomain", newDomainDatabaseId);
1665 domainsIntent.putExtra("closeOnBack", true);
1668 startActivity(domainsIntent);
1672 case R.id.toggle_first_party_cookies:
1673 // Switch the status of firstPartyCookiesEnabled.
1674 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
1676 // Update the menu checkbox.
1677 menuItem.setChecked(firstPartyCookiesEnabled);
1679 // Apply the new cookie status.
1680 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1682 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1683 updatePrivacyIcons(true);
1685 // Display a `Snackbar`.
1686 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
1687 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1688 } else if (javaScriptEnabled) { // JavaScript is still enabled.
1689 Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1690 } else { // Privacy mode.
1691 Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1694 // Reload the current WebView.
1695 currentWebView.reload();
1698 case R.id.toggle_third_party_cookies:
1699 if (Build.VERSION.SDK_INT >= 21) {
1700 // Switch the status of thirdPartyCookiesEnabled.
1701 thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled;
1703 // Update the menu checkbox.
1704 menuItem.setChecked(thirdPartyCookiesEnabled);
1706 // Apply the new cookie status.
1707 cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
1709 // Display a `Snackbar`.
1710 if (thirdPartyCookiesEnabled) {
1711 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1713 Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1716 // Reload the current WebView.
1717 currentWebView.reload();
1718 } // Else do nothing because SDK < 21.
1721 case R.id.toggle_dom_storage:
1722 // Switch the status of domStorageEnabled.
1723 domStorageEnabled = !domStorageEnabled;
1725 // Update the menu checkbox.
1726 menuItem.setChecked(domStorageEnabled);
1728 // Apply the new DOM Storage status.
1729 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
1731 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1732 updatePrivacyIcons(true);
1734 // Display a `Snackbar`.
1735 if (domStorageEnabled) {
1736 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1738 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1741 // Reload the current WebView.
1742 currentWebView.reload();
1745 // Form data can be removed once the minimum API >= 26.
1746 case R.id.toggle_save_form_data:
1747 // Switch the status of saveFormDataEnabled.
1748 saveFormDataEnabled = !saveFormDataEnabled;
1750 // Update the menu checkbox.
1751 menuItem.setChecked(saveFormDataEnabled);
1753 // Apply the new form data status.
1754 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
1756 // Display a `Snackbar`.
1757 if (saveFormDataEnabled) {
1758 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1760 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1763 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
1764 updatePrivacyIcons(true);
1766 // Reload the current WebView.
1767 currentWebView.reload();
1770 case R.id.clear_cookies:
1771 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1772 .setAction(R.string.undo, v -> {
1773 // Do nothing because everything will be handled by `onDismissed()` below.
1775 .addCallback(new Snackbar.Callback() {
1776 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1778 public void onDismissed(Snackbar snackbar, int event) {
1780 // The user pushed the undo button.
1781 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1785 // The snackbar was dismissed without the undo button being pushed.
1787 // `cookieManager.removeAllCookie()` varies by SDK.
1788 if (Build.VERSION.SDK_INT < 21) {
1789 cookieManager.removeAllCookie();
1791 cookieManager.removeAllCookies(null);
1799 case R.id.clear_dom_storage:
1800 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1801 .setAction(R.string.undo, v -> {
1802 // Do nothing because everything will be handled by `onDismissed()` below.
1804 .addCallback(new Snackbar.Callback() {
1805 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1807 public void onDismissed(Snackbar snackbar, int event) {
1809 // The user pushed the undo button.
1810 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1814 // The snackbar was dismissed without the undo button being pushed.
1816 // Delete the DOM Storage.
1817 WebStorage webStorage = WebStorage.getInstance();
1818 webStorage.deleteAllData();
1820 // Initialize a handler to manually delete the DOM storage files and directories.
1821 Handler deleteDomStorageHandler = new Handler();
1823 // Setup a runnable to manually delete the DOM storage files and directories.
1824 Runnable deleteDomStorageRunnable = () -> {
1826 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1827 Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1829 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1830 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1831 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1832 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1833 Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1835 // Wait for the processes to finish.
1836 deleteLocalStorageProcess.waitFor();
1837 deleteIndexProcess.waitFor();
1838 deleteQuotaManagerProcess.waitFor();
1839 deleteQuotaManagerJournalProcess.waitFor();
1840 deleteDatabasesProcess.waitFor();
1841 } catch (Exception exception) {
1842 // Do nothing if an error is thrown.
1846 // Manually delete the DOM storage files after 200 milliseconds.
1847 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1854 // Form data can be remove once the minimum API >= 26.
1855 case R.id.clear_form_data:
1856 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1857 .setAction(R.string.undo, v -> {
1858 // Do nothing because everything will be handled by `onDismissed()` below.
1860 .addCallback(new Snackbar.Callback() {
1861 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1863 public void onDismissed(Snackbar snackbar, int event) {
1865 // The user pushed the undo button.
1866 case Snackbar.Callback.DISMISS_EVENT_ACTION:
1870 // The snackbar was dismissed without the `Undo` button being pushed.
1872 // Delete the form data.
1873 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1874 mainWebViewDatabase.clearFormData();
1882 // Toggle the EasyList status.
1883 easyListEnabled = !easyListEnabled;
1885 // Update the menu checkbox.
1886 menuItem.setChecked(easyListEnabled);
1888 // Reload the current WebView.
1889 currentWebView.reload();
1892 case R.id.easyprivacy:
1893 // Toggle the EasyPrivacy status.
1894 easyPrivacyEnabled = !easyPrivacyEnabled;
1896 // Update the menu checkbox.
1897 menuItem.setChecked(easyPrivacyEnabled);
1899 // Reload the current WebView.
1900 currentWebView.reload();
1903 case R.id.fanboys_annoyance_list:
1904 // Toggle Fanboy's Annoyance List status.
1905 fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled;
1907 // Update the menu checkbox.
1908 menuItem.setChecked(fanboysAnnoyanceListEnabled);
1910 // Update the staus of Fanboy's Social Blocking List.
1911 MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list);
1912 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
1914 // Reload the current WebView.
1915 currentWebView.reload();
1918 case R.id.fanboys_social_blocking_list:
1919 // Toggle Fanboy's Social Blocking List status.
1920 fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled;
1922 // Update the menu checkbox.
1923 menuItem.setChecked(fanboysSocialBlockingListEnabled);
1925 // Reload the current WebView.
1926 currentWebView.reload();
1929 case R.id.ultraprivacy:
1930 // Toggle the UltraPrivacy status.
1931 ultraPrivacyEnabled = !ultraPrivacyEnabled;
1933 // Update the menu checkbox.
1934 menuItem.setChecked(ultraPrivacyEnabled);
1936 // Reload the current WebView.
1937 currentWebView.reload();
1940 case R.id.block_all_third_party_requests:
1941 //Toggle the third-party requests blocker status.
1942 blockAllThirdPartyRequests = !blockAllThirdPartyRequests;
1944 // Update the menu checkbox.
1945 menuItem.setChecked(blockAllThirdPartyRequests);
1947 // Reload the current WebView.
1948 currentWebView.reload();
1951 case R.id.user_agent_privacy_browser:
1952 // Update the user agent.
1953 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1955 // Reload the current WebView.
1956 currentWebView.reload();
1959 case R.id.user_agent_webview_default:
1960 // Update the user agent.
1961 currentWebView.getSettings().setUserAgentString("");
1963 // Reload the current WebView.
1964 currentWebView.reload();
1967 case R.id.user_agent_firefox_on_android:
1968 // Update the user agent.
1969 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1971 // Reload the current WebView.
1972 currentWebView.reload();
1975 case R.id.user_agent_chrome_on_android:
1976 // Update the user agent.
1977 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1979 // Reload the current WebView.
1980 currentWebView.reload();
1983 case R.id.user_agent_safari_on_ios:
1984 // Update the user agent.
1985 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1987 // Reload the current WebView.
1988 currentWebView.reload();
1991 case R.id.user_agent_firefox_on_linux:
1992 // Update the user agent.
1993 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1995 // Reload the current WebView.
1996 currentWebView.reload();
1999 case R.id.user_agent_chromium_on_linux:
2000 // Update the user agent.
2001 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
2003 // Reload the current WebView.
2004 currentWebView.reload();
2007 case R.id.user_agent_firefox_on_windows:
2008 // Update the user agent.
2009 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
2011 // Reload the current WebView.
2012 currentWebView.reload();
2015 case R.id.user_agent_chrome_on_windows:
2016 // Update the user agent.
2017 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
2019 // Reload the current WebView.
2020 currentWebView.reload();
2023 case R.id.user_agent_edge_on_windows:
2024 // Update the user agent.
2025 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
2027 // Reload the current WebView.
2028 currentWebView.reload();
2031 case R.id.user_agent_internet_explorer_on_windows:
2032 // Update the user agent.
2033 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
2035 // Reload the current WebView.
2036 currentWebView.reload();
2039 case R.id.user_agent_safari_on_macos:
2040 // Update the user agent.
2041 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
2043 // Reload the current WebView.
2044 currentWebView.reload();
2047 case R.id.user_agent_custom:
2048 // Update the user agent.
2049 currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
2051 // Reload the current WebView.
2052 currentWebView.reload();
2055 case R.id.font_size_twenty_five_percent:
2056 currentWebView.getSettings().setTextZoom(25);
2059 case R.id.font_size_fifty_percent:
2060 currentWebView.getSettings().setTextZoom(50);
2063 case R.id.font_size_seventy_five_percent:
2064 currentWebView.getSettings().setTextZoom(75);
2067 case R.id.font_size_one_hundred_percent:
2068 currentWebView.getSettings().setTextZoom(100);
2071 case R.id.font_size_one_hundred_twenty_five_percent:
2072 currentWebView.getSettings().setTextZoom(125);
2075 case R.id.font_size_one_hundred_fifty_percent:
2076 currentWebView.getSettings().setTextZoom(150);
2079 case R.id.font_size_one_hundred_seventy_five_percent:
2080 currentWebView.getSettings().setTextZoom(175);
2083 case R.id.font_size_two_hundred_percent:
2084 currentWebView.getSettings().setTextZoom(200);
2087 case R.id.swipe_to_refresh:
2088 // Get a handle for the swipe refresh layout.
2089 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
2091 // Toggle swipe to refresh.
2092 swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
2095 case R.id.display_images:
2096 if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
2097 // Disable loading of images.
2098 currentWebView.getSettings().setLoadsImagesAutomatically(false);
2100 // Reload the website to remove existing images.
2101 currentWebView.reload();
2102 } else { // Images are not currently loaded automatically.
2103 // Enable loading of images. Missing images will be loaded without the need for a reload.
2104 currentWebView.getSettings().setLoadsImagesAutomatically(true);
2108 case R.id.night_mode:
2109 // Toggle night mode.
2110 nightMode = !nightMode;
2112 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
2113 if (nightMode) { // Night mode is enabled. Enable JavaScript.
2114 // Update the global variable.
2115 javaScriptEnabled = true;
2116 } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
2117 // Get the JavaScript preference that was stored the last time domain settings were loaded.
2118 javaScriptEnabled = domainSettingsJavaScriptEnabled;
2119 } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
2120 // Get a handle for the shared preference.
2121 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2123 // Get the JavaScript preference.
2124 javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
2127 // Apply the JavaScript setting to the WebView.
2128 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
2130 // Update the privacy icons.
2131 updatePrivacyIcons(false);
2133 // Reload the website.
2134 currentWebView.reload();
2137 case R.id.find_on_page:
2138 // Get a handle for the views.
2139 Toolbar toolbar = findViewById(R.id.toolbar);
2140 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2142 // Hide the toolbar.
2143 toolbar.setVisibility(View.GONE);
2145 // Show the find on page linear layout.
2146 findOnPageLinearLayout.setVisibility(View.VISIBLE);
2148 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
2149 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
2150 findOnPageEditText.postDelayed(() -> {
2151 // Set the focus on `findOnPageEditText`.
2152 findOnPageEditText.requestFocus();
2154 // Display the keyboard. `0` sets no input flags.
2155 inputMethodManager.showSoftInput(findOnPageEditText, 0);
2159 case R.id.view_source:
2160 // Launch the View Source activity.
2161 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
2162 startActivity(viewSourceIntent);
2165 case R.id.share_url:
2166 // Setup the share string.
2167 String shareString = currentWebView.getTitle() + " – " + formattedUrlString;
2169 // Create the share intent.
2170 Intent shareIntent = new Intent(Intent.ACTION_SEND);
2171 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2172 shareIntent.setType("text/plain");
2175 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2179 // Get a `PrintManager` instance.
2180 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2182 // Create a print document adapter form the current WebView.
2183 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2185 // Remove the lint error below that `printManager` might be `null`.
2186 assert printManager != null;
2188 // Print the document. The print attributes are `null`.
2189 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2192 case R.id.open_with_app:
2193 openWithApp(formattedUrlString);
2196 case R.id.open_with_browser:
2197 openWithBrowser(formattedUrlString);
2200 case R.id.add_to_homescreen:
2201 // Instantiate the create home screen shortcut dialog.
2202 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, favoriteIconBitmap);
2204 // Show the create home screen shortcut dialog.
2205 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2208 case R.id.proxy_through_orbot:
2209 // Toggle the proxy through Orbot variable.
2210 proxyThroughOrbot = !proxyThroughOrbot;
2212 // Apply the proxy through Orbot settings.
2213 applyProxyThroughOrbot(true);
2217 if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
2218 // Reload the current WebView.
2219 currentWebView.reload();
2220 } else { // The stop button was pushed.
2221 // Stop the loading of the WebView.
2222 currentWebView.stopLoading();
2226 case R.id.ad_consent:
2227 // Display the ad consent dialog.
2228 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2229 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2233 // Don't consume the event.
2234 return super.onOptionsItemSelected(menuItem);
2238 // removeAllCookies is deprecated, but it is required for API < 21.
2239 @SuppressWarnings("deprecation")
2241 public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2242 // Get the menu item ID.
2243 int menuItemId = menuItem.getItemId();
2245 // Run the commands that correspond to the selected menu item.
2246 switch (menuItemId) {
2247 case R.id.close_tab:
2248 // Get a handle for the tab layout.
2249 TabLayout tabLayout = findViewById(R.id.tablayout);
2251 // Get the current tab number.
2252 int currentTabNumber = tabLayout.getSelectedTabPosition();
2254 // Delete the tab and page.
2255 webViewPagerAdapter.deletePage(currentTabNumber);
2258 case R.id.clear_and_exit:
2259 // Close the bookmarks cursor and database.
2260 bookmarksCursor.close();
2261 bookmarksDatabaseHelper.close();
2263 // Get a handle for the shared preferences.
2264 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2266 // Get the status of the clear everything preference.
2267 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2270 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2271 // The command to remove cookies changed slightly in API 21.
2272 if (Build.VERSION.SDK_INT >= 21) {
2273 cookieManager.removeAllCookies(null);
2275 cookieManager.removeAllCookie();
2278 // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2280 // Two commands must be used because `Runtime.exec()` does not like `*`.
2281 Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2282 Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2284 // Wait until the processes have finished.
2285 deleteCookiesProcess.waitFor();
2286 deleteCookiesJournalProcess.waitFor();
2287 } catch (Exception exception) {
2288 // Do nothing if an error is thrown.
2292 // Clear DOM storage.
2293 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2294 // Ask `WebStorage` to clear the DOM storage.
2295 WebStorage webStorage = WebStorage.getInstance();
2296 webStorage.deleteAllData();
2298 // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2300 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2301 Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2303 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2304 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2305 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2306 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2307 Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2309 // Wait until the processes have finished.
2310 deleteLocalStorageProcess.waitFor();
2311 deleteIndexProcess.waitFor();
2312 deleteQuotaManagerProcess.waitFor();
2313 deleteQuotaManagerJournalProcess.waitFor();
2314 deleteDatabaseProcess.waitFor();
2315 } catch (Exception exception) {
2316 // Do nothing if an error is thrown.
2320 // Clear form data if the API < 26.
2321 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2322 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2323 webViewDatabase.clearFormData();
2325 // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2327 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2328 Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2329 Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2331 // Wait until the processes have finished.
2332 deleteWebDataProcess.waitFor();
2333 deleteWebDataJournalProcess.waitFor();
2334 } catch (Exception exception) {
2335 // Do nothing if an error is thrown.
2340 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2343 currentWebView.clearCache(true);
2345 // Manually delete the cache directories.
2347 // Delete the main cache directory.
2348 Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2350 // Delete the secondary `Service Worker` cache directory.
2351 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2352 Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2354 // Wait until the processes have finished.
2355 deleteCacheProcess.waitFor();
2356 deleteServiceWorkerProcess.waitFor();
2357 } catch (Exception exception) {
2358 // Do nothing if an error is thrown.
2362 // Clear SSL certificate preferences.
2364 currentWebView.clearSslPreferences();
2366 // Clear the back/forward history.
2368 currentWebView.clearHistory();
2370 // Clear `formattedUrlString`.
2371 formattedUrlString = null;
2373 // Clear `customHeaders`.
2374 customHeaders.clear();
2376 // Destroy the internal state of `mainWebView`.
2378 currentWebView.destroy();
2380 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2381 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2382 if (clearEverything) {
2384 // Delete the folder.
2385 Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2387 // Wait until the process has finished.
2388 deleteAppWebviewProcess.waitFor();
2389 } catch (Exception exception) {
2390 // Do nothing if an error is thrown.
2394 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2395 if (Build.VERSION.SDK_INT >= 21) {
2396 finishAndRemoveTask();
2401 // Remove the terminated program from RAM. The status code is `0`.
2410 if (currentWebView.canGoBack()) {
2411 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2412 formattedUrlString = "";
2414 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2415 navigatingHistory = true;
2417 // Load the previous website in the history.
2418 currentWebView.goBack();
2423 if (currentWebView.canGoForward()) {
2424 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2425 formattedUrlString = "";
2427 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2428 navigatingHistory = true;
2430 // Load the next website in the history.
2431 currentWebView.goForward();
2436 // Get the `WebBackForwardList`.
2437 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2439 // Show the URL history dialog and name this instance `R.string.history`.
2440 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2441 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2445 // Launch the requests activity.
2446 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2447 startActivity(requestsIntent);
2450 case R.id.downloads:
2451 // Launch the system Download Manager.
2452 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2454 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2455 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2457 startActivity(downloadManagerIntent);
2461 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2462 reapplyDomainSettingsOnRestart = true;
2463 currentDomainName = "";
2465 // Launch the domains activity.
2466 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2467 startActivity(domainsIntent);
2471 // Set the flag to reapply app settings on restart when returning from Settings.
2472 reapplyAppSettingsOnRestart = true;
2474 // Set the flag to reapply the domain settings on restart when returning from Settings.
2475 reapplyDomainSettingsOnRestart = true;
2476 currentDomainName = "";
2478 // Launch the settings activity.
2479 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2480 startActivity(settingsIntent);
2483 case R.id.import_export:
2484 // Launch the import/export activity.
2485 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2486 startActivity(importExportIntent);
2490 // Launch the logcat activity.
2491 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2492 startActivity(logcatIntent);
2496 // Launch `GuideActivity`.
2497 Intent guideIntent = new Intent(this, GuideActivity.class);
2498 startActivity(guideIntent);
2502 // Launch `AboutActivity`.
2503 Intent aboutIntent = new Intent(this, AboutActivity.class);
2504 startActivity(aboutIntent);
2508 // Get a handle for the drawer layout.
2509 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2511 // Close the navigation drawer.
2512 drawerLayout.closeDrawer(GravityCompat.START);
2517 public void onPostCreate(Bundle savedInstanceState) {
2518 // Run the default commands.
2519 super.onPostCreate(savedInstanceState);
2521 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2522 actionBarDrawerToggle.syncState();
2526 public void onConfigurationChanged(Configuration newConfig) {
2527 // Run the default commands.
2528 super.onConfigurationChanged(newConfig);
2530 // Get the status bar pixel size.
2531 int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2532 int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2534 // Get the resource density.
2535 float screenDensity = getResources().getDisplayMetrics().density;
2537 // Recalculate the drawer header padding.
2538 drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2539 drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2540 drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2542 // Reload the ad for the free flavor if not in full screen mode.
2543 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2544 // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2545 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2548 // `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:
2549 // https://code.google.com/p/android/issues/detail?id=20493#c8
2550 // ActivityCompat.invalidateOptionsMenu(this);
2554 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2555 // Store the `HitTestResult`.
2556 final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2559 final String imageUrl;
2560 final String linkUrl;
2562 // Get a handle for the the clipboard and fragment managers.
2563 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2564 FragmentManager fragmentManager = getSupportFragmentManager();
2566 // Remove the lint errors below that `clipboardManager` might be `null`.
2567 assert clipboardManager != null;
2569 switch (hitTestResult.getType()) {
2570 // `SRC_ANCHOR_TYPE` is a link.
2571 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2572 // Get the target URL.
2573 linkUrl = hitTestResult.getExtra();
2575 // Set the target URL as the title of the `ContextMenu`.
2576 menu.setHeaderTitle(linkUrl);
2578 // Add a Load URL entry.
2579 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
2584 // Add a Copy URL entry.
2585 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2586 // Save the link URL in a `ClipData`.
2587 ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2589 // Set the `ClipData` as the clipboard's primary clip.
2590 clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2594 // Add a Download URL entry.
2595 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2596 // Check if the download should be processed by an external app.
2597 if (downloadWithExternalApp) { // Download with an external app.
2598 openUrlWithExternalApp(linkUrl);
2599 } else { // Download with Android's download manager.
2600 // Check to see if the storage permission has already been granted.
2601 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2602 // Store the variables for future use by `onRequestPermissionsResult()`.
2603 downloadUrl = linkUrl;
2604 downloadContentDisposition = "none";
2605 downloadContentLength = -1;
2607 // Show a dialog if the user has previously denied the permission.
2608 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2609 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2610 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2612 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
2613 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2614 } else { // Show the permission request directly.
2615 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
2616 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2618 } else { // The storage permission has already been granted.
2619 // Get a handle for the download file alert dialog.
2620 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2622 // Show the download file alert dialog.
2623 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2629 // Add an Open with App entry.
2630 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2631 openWithApp(linkUrl);
2635 // Add an Open with Browser entry.
2636 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2637 openWithBrowser(linkUrl);
2641 // Add a Cancel entry, which by default closes the context menu.
2642 menu.add(R.string.cancel);
2645 case WebView.HitTestResult.EMAIL_TYPE:
2646 // Get the target URL.
2647 linkUrl = hitTestResult.getExtra();
2649 // Set the target URL as the title of the `ContextMenu`.
2650 menu.setHeaderTitle(linkUrl);
2652 // Add a Write Email entry.
2653 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2654 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2655 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2657 // Parse the url and set it as the data for the `Intent`.
2658 emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2660 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2661 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2664 startActivity(emailIntent);
2668 // Add a Copy Email Address entry.
2669 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2670 // Save the email address in a `ClipData`.
2671 ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2673 // Set the `ClipData` as the clipboard's primary clip.
2674 clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2678 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2679 menu.add(R.string.cancel);
2682 // `IMAGE_TYPE` is an image.
2683 case WebView.HitTestResult.IMAGE_TYPE:
2684 // Get the image URL.
2685 imageUrl = hitTestResult.getExtra();
2687 // Set the image URL as the title of the `ContextMenu`.
2688 menu.setHeaderTitle(imageUrl);
2690 // Add a View Image entry.
2691 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2696 // Add a Download Image entry.
2697 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2698 // Check if the download should be processed by an external app.
2699 if (downloadWithExternalApp) { // Download with an external app.
2700 openUrlWithExternalApp(imageUrl);
2701 } else { // Download with Android's download manager.
2702 // Check to see if the storage permission has already been granted.
2703 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2704 // Store the image URL for use by `onRequestPermissionResult()`.
2705 downloadImageUrl = imageUrl;
2707 // Show a dialog if the user has previously denied the permission.
2708 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2709 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2710 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2712 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2713 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2714 } else { // Show the permission request directly.
2715 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2716 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2718 } else { // The storage permission has already been granted.
2719 // Get a handle for the download image alert dialog.
2720 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2722 // Show the download image alert dialog.
2723 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2729 // Add a Copy URL entry.
2730 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2731 // Save the image URL in a `ClipData`.
2732 ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2734 // Set the `ClipData` as the clipboard's primary clip.
2735 clipboardManager.setPrimaryClip(srcImageTypeClipData);
2739 // Add an Open with App entry.
2740 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2741 openWithApp(imageUrl);
2745 // Add an Open with Browser entry.
2746 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2747 openWithBrowser(imageUrl);
2751 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2752 menu.add(R.string.cancel);
2756 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2757 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2758 // Get the image URL.
2759 imageUrl = hitTestResult.getExtra();
2761 // Set the image URL as the title of the `ContextMenu`.
2762 menu.setHeaderTitle(imageUrl);
2764 // Add a `View Image` entry.
2765 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2770 // Add a `Download Image` entry.
2771 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2772 // Check if the download should be processed by an external app.
2773 if (downloadWithExternalApp) { // Download with an external app.
2774 openUrlWithExternalApp(imageUrl);
2775 } else { // Download with Android's download manager.
2776 // Check to see if the storage permission has already been granted.
2777 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
2778 // Store the image URL for use by `onRequestPermissionResult()`.
2779 downloadImageUrl = imageUrl;
2781 // Show a dialog if the user has previously denied the permission.
2782 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
2783 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2784 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2786 // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
2787 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2788 } else { // Show the permission request directly.
2789 // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
2790 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2792 } else { // The storage permission has already been granted.
2793 // Get a handle for the download image alert dialog.
2794 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2796 // Show the download image alert dialog.
2797 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2803 // Add a `Copy URL` entry.
2804 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2805 // Save the image URL in a `ClipData`.
2806 ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2808 // Set the `ClipData` as the clipboard's primary clip.
2809 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2813 // Add an Open with App entry.
2814 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2815 openWithApp(imageUrl);
2819 // Add an Open with Browser entry.
2820 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2821 openWithBrowser(imageUrl);
2825 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2826 menu.add(R.string.cancel);
2832 public void onCreateBookmark(DialogFragment dialogFragment) {
2833 // Get the views from the dialog fragment.
2834 EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2835 EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2837 // Extract the strings from the edit texts.
2838 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2839 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2841 // Get a copy of the favorite icon bitmap.
2842 Bitmap favoriteIcon = favoriteIconBitmap;
2844 // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2845 if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2846 favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2849 // Create a favorite icon byte array output stream.
2850 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2852 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2853 favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2855 // Convert the favorite icon byte array stream to a byte array.
2856 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2858 // Display the new bookmark below the current items in the (0 indexed) list.
2859 int newBookmarkDisplayOrder = bookmarksListView.getCount();
2861 // Create the bookmark.
2862 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2864 // Update the bookmarks cursor with the current contents of this folder.
2865 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2867 // Update the list view.
2868 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2870 // Scroll to the new bookmark.
2871 bookmarksListView.setSelection(newBookmarkDisplayOrder);
2875 public void onCreateBookmarkFolder(DialogFragment dialogFragment) {
2876 // Get handles for the views in the dialog fragment.
2877 EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2878 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2879 ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2881 // Get new folder name string.
2882 String folderNameString = createFolderNameEditText.getText().toString();
2884 // Create a folder icon bitmap.
2885 Bitmap folderIconBitmap;
2887 // Set the folder icon bitmap according to the dialog.
2888 if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
2889 // Get the default folder icon drawable.
2890 Drawable folderIconDrawable = folderIconImageView.getDrawable();
2892 // Convert the folder icon drawable to a bitmap drawable.
2893 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2895 // Convert the folder icon bitmap drawable to a bitmap.
2896 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2897 } else { // Use the WebView favorite icon.
2898 // Get a copy of the favorite icon bitmap.
2899 folderIconBitmap = favoriteIconBitmap;
2901 // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2902 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2903 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2907 // Create a folder icon byte array output stream.
2908 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2910 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2911 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2913 // Convert the folder icon byte array stream to a byte array.
2914 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2916 // Move all the bookmarks down one in the display order.
2917 for (int i = 0; i < bookmarksListView.getCount(); i++) {
2918 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2919 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2922 // Create the folder, which will be placed at the top of the `ListView`.
2923 bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2925 // Update the bookmarks cursor with the current contents of this folder.
2926 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2928 // Update the `ListView`.
2929 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2931 // Scroll to the new folder.
2932 bookmarksListView.setSelection(0);
2936 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
2937 // Get handles for the views from `dialogFragment`.
2938 EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2939 EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2940 RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2942 // Store the bookmark strings.
2943 String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2944 String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2946 // Update the bookmark.
2947 if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
2948 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2949 } else { // Update the bookmark using the `WebView` favorite icon.
2950 // Get a copy of the favorite icon bitmap.
2951 Bitmap favoriteIcon = favoriteIconBitmap;
2953 // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
2954 if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2955 favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2958 // Create a favorite icon byte array output stream.
2959 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2961 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2962 favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2964 // Convert the favorite icon byte array stream to a byte array.
2965 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2967 // Update the bookmark and the favorite icon.
2968 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2971 // Update the bookmarks cursor with the current contents of this folder.
2972 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2974 // Update the list view.
2975 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2979 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId) {
2980 // Get handles for the views from `dialogFragment`.
2981 EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2982 RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2983 RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2984 ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2986 // Get the new folder name.
2987 String newFolderNameString = editFolderNameEditText.getText().toString();
2989 // Check if the favorite icon has changed.
2990 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
2991 // Update the name in the database.
2992 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2993 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
2994 // Create the new folder icon Bitmap.
2995 Bitmap folderIconBitmap;
2997 // Populate the new folder icon bitmap.
2998 if (defaultFolderIconRadioButton.isChecked()) {
2999 // Get the default folder icon drawable.
3000 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
3002 // Convert the folder icon drawable to a bitmap drawable.
3003 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3005 // Convert the folder icon bitmap drawable to a bitmap.
3006 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3007 } else { // Use the `WebView` favorite icon.
3008 // Get a copy of the favorite icon bitmap.
3009 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
3011 // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
3012 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
3013 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
3017 // Create a folder icon byte array output stream.
3018 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
3020 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
3021 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
3023 // Convert the folder icon byte array stream to a byte array.
3024 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
3026 // Update the folder icon in the database.
3027 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
3028 } else { // The folder icon and the name have changed.
3029 // Get the new folder icon `Bitmap`.
3030 Bitmap folderIconBitmap;
3031 if (defaultFolderIconRadioButton.isChecked()) {
3032 // Get the default folder icon drawable.
3033 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
3035 // Convert the folder icon drawable to a bitmap drawable.
3036 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3038 // Convert the folder icon bitmap drawable to a bitmap.
3039 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3040 } else { // Use the `WebView` favorite icon.
3041 // Get a copy of the favorite icon bitmap.
3042 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
3044 // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
3045 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
3046 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
3050 // Create a folder icon byte array output stream.
3051 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
3053 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
3054 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
3056 // Convert the folder icon byte array stream to a byte array.
3057 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
3059 // Update the folder name and icon in the database.
3060 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
3063 // Update the bookmarks cursor with the current contents of this folder.
3064 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3066 // Update the `ListView`.
3067 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3071 public void onCloseDownloadLocationPermissionDialog(int downloadType) {
3072 switch (downloadType) {
3073 case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
3074 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
3075 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3078 case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
3079 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
3080 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3086 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
3087 // Get a handle for the fragment manager.
3088 FragmentManager fragmentManager = getSupportFragmentManager();
3090 switch (requestCode) {
3091 case DOWNLOAD_FILE_REQUEST_CODE:
3092 // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
3093 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
3095 // On API 23, displaying the fragment must be delayed or the app will crash.
3096 if (Build.VERSION.SDK_INT == 23) {
3097 new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3099 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
3102 // Reset the download variables.
3104 downloadContentDisposition = "";
3105 downloadContentLength = 0;
3108 case DOWNLOAD_IMAGE_REQUEST_CODE:
3109 // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
3110 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
3112 // On API 23, displaying the fragment must be delayed or the app will crash.
3113 if (Build.VERSION.SDK_INT == 23) {
3114 new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3116 downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
3119 // Reset the image URL variable.
3120 downloadImageUrl = "";
3126 public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
3127 // Download the image if it has an HTTP or HTTPS URI.
3128 if (imageUrl.startsWith("http")) {
3129 // Get a handle for the system `DOWNLOAD_SERVICE`.
3130 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3132 // Parse `imageUrl`.
3133 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3135 // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
3136 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3137 if (firstPartyCookiesEnabled) {
3138 // Get the cookies for `imageUrl`.
3139 String cookies = cookieManager.getCookie(imageUrl);
3141 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3142 downloadRequest.addRequestHeader("Cookie", cookies);
3145 // Get the file name from the dialog fragment.
3146 EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3147 String imageName = downloadImageNameEditText.getText().toString();
3149 // Specify the download location.
3150 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3151 // Download to the public download directory.
3152 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3153 } else { // External write permission denied.
3154 // Download to the app's external download directory.
3155 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3158 // Allow `MediaScanner` to index the download if it is a media file.
3159 downloadRequest.allowScanningByMediaScanner();
3161 // Add the URL as the description for the download.
3162 downloadRequest.setDescription(imageUrl);
3164 // Show the download notification after the download is completed.
3165 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3167 // Remove the lint warning below that `downloadManager` might be `null`.
3168 assert downloadManager != null;
3170 // Initiate the download.
3171 downloadManager.enqueue(downloadRequest);
3172 } else { // The image is not an HTTP or HTTPS URI.
3173 Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3178 public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3179 // Download the file if it has an HTTP or HTTPS URI.
3180 if (downloadUrl.startsWith("http")) {
3181 // Get a handle for the system `DOWNLOAD_SERVICE`.
3182 DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3184 // Parse `downloadUrl`.
3185 DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3187 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
3188 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3189 if (firstPartyCookiesEnabled) {
3190 // Get the cookies for `downloadUrl`.
3191 String cookies = cookieManager.getCookie(downloadUrl);
3193 // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
3194 downloadRequest.addRequestHeader("Cookie", cookies);
3197 // Get the file name from the dialog fragment.
3198 EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3199 String fileName = downloadFileNameEditText.getText().toString();
3201 // Specify the download location.
3202 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
3203 // Download to the public download directory.
3204 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3205 } else { // External write permission denied.
3206 // Download to the app's external download directory.
3207 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3210 // Allow `MediaScanner` to index the download if it is a media file.
3211 downloadRequest.allowScanningByMediaScanner();
3213 // Add the URL as the description for the download.
3214 downloadRequest.setDescription(downloadUrl);
3216 // Show the download notification after the download is completed.
3217 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3219 // Remove the lint warning below that `downloadManager` might be `null`.
3220 assert downloadManager != null;
3222 // Initiate the download.
3223 downloadManager.enqueue(downloadRequest);
3224 } else { // The download is not an HTTP or HTTPS URI.
3225 Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3230 public void onHttpAuthenticationCancel() {
3231 // Cancel the `HttpAuthHandler`.
3232 httpAuthHandler.cancel();
3236 public void onHttpAuthenticationProceed(DialogFragment dialogFragment) {
3237 // Get handles for the `EditTexts`.
3238 EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3239 EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3241 // Proceed with the HTTP authentication.
3242 httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3246 public void onSslErrorCancel() {
3247 sslErrorHandler.cancel();
3251 public void onSslErrorProceed() {
3252 sslErrorHandler.proceed();
3256 public void onPinnedMismatchBack() {
3257 if (currentWebView.canGoBack()) { // There is a back page in the history.
3258 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3259 formattedUrlString = "";
3261 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3262 navigatingHistory = true;
3265 currentWebView.goBack();
3266 } else { // There are no pages to go back to.
3267 // Load a blank page
3273 public void onPinnedMismatchProceed() {
3274 // Do not check the pinned information for this domain again until the domain changes.
3275 ignorePinnedDomainInformation = true;
3279 public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3280 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3281 formattedUrlString = "";
3283 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3284 navigatingHistory = true;
3286 // Load the history entry.
3287 currentWebView.goBackOrForward(moveBackOrForwardSteps);
3291 public void onClearHistory() {
3292 // Clear the history.
3293 currentWebView.clearHistory();
3296 // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3298 public void onBackPressed() {
3299 // Get a handle for the drawer layout.
3300 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3302 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
3303 // Close the navigation drawer.
3304 drawerLayout.closeDrawer(GravityCompat.START);
3305 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
3306 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
3307 // close the bookmarks drawer.
3308 drawerLayout.closeDrawer(GravityCompat.END);
3309 } else { // A subfolder is displayed.
3310 // Place the former parent folder in `currentFolder`.
3311 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3313 // Load the new folder.
3314 loadBookmarksFolder();
3317 } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
3318 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3319 formattedUrlString = "";
3321 // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3322 navigatingHistory = true;
3325 currentWebView.goBack();
3326 } else { // There isn't anything to do in Privacy Browser.
3327 // Pass `onBackPressed()` to the system.
3328 super.onBackPressed();
3332 // 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.
3334 public void onActivityResult(int requestCode, int resultCode, Intent data) {
3335 // File uploads only work on API >= 21.
3336 if (Build.VERSION.SDK_INT >= 21) {
3337 // Pass the file to the WebView.
3338 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3342 private void loadUrlFromTextBox() {
3343 // Get a handle for the URL edit text.
3344 EditText urlEditText = findViewById(R.id.url_edittext);
3346 // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
3347 String unformattedUrlString = urlEditText.getText().toString().trim();
3349 // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
3350 if (unformattedUrlString.startsWith("content://")) {
3351 // Load the entire content URL.
3352 formattedUrlString = unformattedUrlString;
3353 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3354 || unformattedUrlString.startsWith("file://")) {
3355 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
3356 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3357 unformattedUrlString = "https://" + unformattedUrlString;
3360 // Initialize `unformattedUrl`.
3361 URL unformattedUrl = null;
3363 // 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.
3365 unformattedUrl = new URL(unformattedUrlString);
3366 } catch (MalformedURLException e) {
3367 e.printStackTrace();
3370 // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3371 String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3372 String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3373 String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3374 String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3375 String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3378 Uri.Builder formattedUri = new Uri.Builder();
3379 formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3381 // Decode `formattedUri` as a `String` in `UTF-8`.
3383 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
3384 } catch (UnsupportedEncodingException exception) {
3385 // Load a blank string.
3386 formattedUrlString = "";
3388 } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
3389 // Load a blank string.
3390 formattedUrlString = "";
3391 } else { // Search for the contents of the URL box.
3392 // Create an encoded URL String.
3393 String encodedUrlString;
3395 // Sanitize the search input.
3397 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3398 } catch (UnsupportedEncodingException exception) {
3399 encodedUrlString = "";
3402 // Add the base search URL.
3403 formattedUrlString = searchURL + encodedUrlString;
3406 // 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.
3407 urlEditText.clearFocus();
3410 loadUrl(formattedUrlString);
3413 private void loadUrl(String url) {// Apply any custom domain settings.
3414 // Set the URL as the formatted URL string so that checking third-party requests works correctly.
3415 formattedUrlString = url;
3417 // Apply the domain settings.
3418 applyDomainSettings(url, true, false);
3420 // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
3421 urlIsLoading = !url.equals("");
3424 currentWebView.loadUrl(url, customHeaders);
3427 public void findPreviousOnPage(View view) {
3428 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
3429 currentWebView.findNext(false);
3432 public void findNextOnPage(View view) {
3433 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3434 currentWebView.findNext(true);
3437 public void closeFindOnPage(View view) {
3438 // Get a handle for the views.
3439 Toolbar toolbar = findViewById(R.id.toolbar);
3440 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3442 // Delete the contents of `find_on_page_edittext`.
3443 findOnPageEditText.setText(null);
3445 // Clear the highlighted phrases.
3446 currentWebView.clearMatches();
3448 // Hide the find on page linear layout.
3449 findOnPageLinearLayout.setVisibility(View.GONE);
3451 // Show the toolbar.
3452 toolbar.setVisibility(View.VISIBLE);
3454 // Hide the keyboard.
3455 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3458 private void applyAppSettings() {
3459 // Get a handle for the shared preferences.
3460 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3462 // Store the values from the shared preferences in variables.
3463 incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3464 boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3465 proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3466 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3467 hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3468 downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
3470 // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
3471 FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3472 ActionBar actionBar = getSupportActionBar();
3474 // Remove the incorrect lint warnings below that the action bar might be null.
3475 assert actionBar != null;
3477 // Apply the proxy through Orbot settings.
3478 applyProxyThroughOrbot(false);
3480 // Set Do Not Track status.
3481 if (doNotTrackEnabled) {
3482 customHeaders.put("DNT", "1");
3484 customHeaders.remove("DNT");
3487 // TODO this also needs to be set when creating a new tab.
3488 // Set the app bar scrolling for each WebView.
3489 for (int i = 0; i < webViewPagerAdapter.webViewFragmentsList.size(); i++) {
3490 // Get the WebView tab fragment.
3491 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.webViewFragmentsList.get(i);
3493 // Get the fragment view.
3494 View fragmentView = webViewTabFragment.getView();
3496 // Only modify the WebViews if they exist.
3497 if (fragmentView != null) {
3498 // Get the nested scroll WebView from the tab fragment.
3499 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3501 // Set the app bar scrolling.
3502 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
3506 // Update the full screen browsing mode settings.
3507 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3508 // Update the visibility of the app bar, which might have changed in the settings.
3515 // Hide the banner ad in the free flavor.
3516 if (BuildConfig.FLAVOR.contentEquals("free")) {
3517 AdHelper.hideAd(findViewById(R.id.adview));
3520 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
3521 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3523 /* Hide the system bars.
3524 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3525 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3526 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3527 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3529 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3530 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3531 } else { // Privacy Browser is not in full screen browsing mode.
3532 // 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.
3533 inFullScreenBrowsingMode = false;
3535 // Show the app bar.
3538 // Show the banner ad in the free flavor.
3539 if (BuildConfig.FLAVOR.contentEquals("free")) {
3540 // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
3541 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), fragmentManager, getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3544 // Remove the `SYSTEM_UI` flags from the root frame layout.
3545 rootFrameLayout.setSystemUiVisibility(0);
3547 // Add the translucent status flag.
3548 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3553 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3554 // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3555 @SuppressWarnings("deprecation")
3556 private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
3557 // Get a handle for the URL edit text.
3558 EditText urlEditText = findViewById(R.id.url_edittext);
3560 // Get the current user agent.
3561 String initialUserAgent = currentWebView.getSettings().getUserAgentString();
3563 // Initialize a variable to track if the user agent changes.
3564 boolean userAgentChanged = false;
3566 // Parse the URL into a URI.
3567 Uri uri = Uri.parse(url);
3569 // Extract the domain from `uri`.
3570 String hostName = uri.getHost();
3572 // Initialize `loadingNewDomainName`.
3573 boolean loadingNewDomainName;
3575 // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
3576 // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
3577 //noinspection SimplifiableIfStatement
3578 if ((hostName == null) || (currentDomainName == null)) {
3579 loadingNewDomainName = true;
3580 } else { // Determine if `hostName` equals `currentDomainName`.
3581 loadingNewDomainName = !hostName.equals(currentDomainName);
3584 // Strings don't like to be null.
3585 if (hostName == null) {
3589 // 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.
3590 if (loadingNewDomainName) {
3591 // Set the new `hostname` as the `currentDomainName`.
3592 currentDomainName = hostName;
3594 // Reset the ignoring of pinned domain information.
3595 ignorePinnedDomainInformation = false;
3597 // Reset the favorite icon if specified.
3598 if (resetFavoriteIcon) {
3599 // Store the favorite icon bitmap.
3600 favoriteIconBitmap = favoriteIconDefaultBitmap;
3602 // Get a handle for the tab layout.
3603 TabLayout tabLayout = findViewById(R.id.tablayout);
3605 // Get the current tab.
3606 TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition());
3608 // Remove the warning below that the current tab might be null.
3609 assert currentTab != null;
3611 // Get the current tab custom view.
3612 View currentTabCustomView = currentTab.getCustomView();
3614 // Remove the warning below that the current tab custom view might be null.
3615 assert currentTabCustomView != null;
3617 // Get the current tab favorite icon image view.
3618 ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
3620 // Set the default favorite icon as the favorite icon for this tab.
3621 currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
3624 // Get a handle for the swipe refresh layout.
3625 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3627 // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3628 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3630 // Get a full cursor from `domainsDatabaseHelper`.
3631 Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3633 // Initialize `domainSettingsSet`.
3634 Set<String> domainSettingsSet = new HashSet<>();
3636 // Get the domain name column index.
3637 int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3639 // Populate `domainSettingsSet`.
3640 for (int i = 0; i < domainNameCursor.getCount(); i++) {
3641 // Move `domainsCursor` to the current row.
3642 domainNameCursor.moveToPosition(i);
3644 // Store the domain name in `domainSettingsSet`.
3645 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3648 // Close `domainNameCursor.
3649 domainNameCursor.close();
3651 // Initialize the domain name in database variable.
3652 String domainNameInDatabase = null;
3654 // Check the hostname against the domain settings set.
3655 if (domainSettingsSet.contains(hostName)) { // The hostname is contained in the domain settings set.
3656 // Record the domain name in the database.
3657 domainNameInDatabase = hostName;
3659 // Set the domain settings applied tracker to true.
3660 currentWebView.setDomainSettingsApplied(true);
3661 } else { // The hostname is not contained in the domain settings set.
3662 // Set the domain settings applied tracker to false.
3663 currentWebView.setDomainSettingsApplied(false);
3666 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3667 while (!currentWebView.getDomainSettingsApplied() && hostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3668 if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`.
3669 // Set the domain settings applied tracker to true.
3670 currentWebView.setDomainSettingsApplied(true);
3672 // Store the applied domain names as it appears in the database.
3673 domainNameInDatabase = "*." + hostName;
3676 // Strip out the lowest subdomain of of the host name.
3677 hostName = hostName.substring(hostName.indexOf(".") + 1);
3681 // Get a handle for the shared preference.
3682 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3684 // Store the general preference information.
3685 String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3686 String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3687 defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
3688 boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3689 nightMode = sharedPreferences.getBoolean("night_mode", false);
3690 boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3692 if (currentWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
3693 // Get a cursor for the current host and move it to the first position.
3694 Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3695 currentHostDomainSettingsCursor.moveToFirst();
3697 // Get the settings from the cursor.
3698 currentWebView.setDomainSettingsDatabaseId(currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3699 javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3700 firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3701 thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3702 domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3703 // Form data can be removed once the minimum API >= 26.
3704 saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3705 easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3706 easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3707 fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3708 fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3709 ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3710 blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3711 String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3712 int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3713 int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3714 int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3715 int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3716 pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3717 pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3718 pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3719 pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3720 pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3721 pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3722 pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3723 pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3724 pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3726 // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
3727 switch (nightModeInt) {
3728 case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3732 case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3737 // Store the domain JavaScript status. This is used by the options menu night mode toggle.
3738 domainSettingsJavaScriptEnabled = javaScriptEnabled;
3740 // Enable JavaScript if night mode is enabled.
3742 javaScriptEnabled = true;
3745 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
3746 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3747 pinnedSslStartDate = null;
3749 pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3752 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
3753 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3754 pinnedSslEndDate = null;
3756 pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3759 // Close `currentHostDomainSettingsCursor`.
3760 currentHostDomainSettingsCursor.close();
3762 // Apply the domain settings.
3763 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
3764 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
3765 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
3767 // Apply the form data setting if the API < 26.
3768 if (Build.VERSION.SDK_INT < 26) {
3769 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3772 // Apply the font size.
3773 if (fontSize == 0) { // Apply the default font size.
3774 currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3775 } else { // Apply the specified font size.
3776 currentWebView.getSettings().setTextZoom(fontSize);
3779 // Set third-party cookies status if API >= 21.
3780 if (Build.VERSION.SDK_INT >= 21) {
3781 cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
3784 // 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.
3785 // <https://redmine.stoutner.com/issues/160>
3786 if (!urlIsLoading) {
3787 // Set the user agent.
3788 if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
3789 // Get the array position of the default user agent name.
3790 int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3792 // Set the user agent according to the system default.
3793 switch (defaultUserAgentArrayPosition) {
3794 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3795 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3796 currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
3799 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3800 // Set the user agent to `""`, which uses the default value.
3801 currentWebView.getSettings().setUserAgentString("");
3804 case SETTINGS_CUSTOM_USER_AGENT:
3805 // Set the custom user agent.
3806 currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
3810 // Get the user agent string from the user agent data array
3811 currentWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3813 } else { // Set the user agent according to the stored name.
3814 // Get the array position of the user agent name.
3815 int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3817 switch (userAgentArrayPosition) {
3818 case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
3819 currentWebView.getSettings().setUserAgentString(userAgentName);
3822 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3823 // Set the user agent to `""`, which uses the default value.
3824 currentWebView.getSettings().setUserAgentString("");
3828 // Get the user agent string from the user agent data array.
3829 currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3833 // Store the applied user agent string, which is used in the View Source activity.
3834 appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
3836 // Update the user agent change tracker.
3837 userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
3840 // Set swipe to refresh.
3841 switch (swipeToRefreshInt) {
3842 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3843 // Set swipe to refresh according to the default.
3844 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3847 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3848 // Enable swipe to refresh.
3849 swipeRefreshLayout.setEnabled(true);
3852 case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3853 // Disable swipe to refresh.
3854 swipeRefreshLayout.setEnabled(false);
3857 // Set the loading of webpage images.
3858 switch (displayWebpageImagesInt) {
3859 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3860 currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3863 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3864 currentWebView.getSettings().setLoadsImagesAutomatically(true);
3867 case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3868 currentWebView.getSettings().setLoadsImagesAutomatically(false);
3872 // Set a green background on URL edit text to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3874 urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3876 urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3878 } else { // The new URL does not have custom domain settings. Load the defaults.
3879 // Store the values from `sharedPreferences` in variables.
3880 javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3881 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
3882 thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3883 domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
3884 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
3885 easyListEnabled = sharedPreferences.getBoolean("easylist", true);
3886 easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
3887 fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
3888 fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
3889 ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
3890 blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
3892 // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
3894 javaScriptEnabled = true;
3897 // Apply the default settings.
3898 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
3899 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
3900 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
3901 currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3902 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3904 // Apply the form data setting if the API < 26.
3905 if (Build.VERSION.SDK_INT < 26) {
3906 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3909 // Reset the pinned variables.
3910 currentWebView.setDomainSettingsDatabaseId(-1);
3911 pinnedSslCertificate = false;
3912 pinnedSslIssuedToCName = "";
3913 pinnedSslIssuedToOName = "";
3914 pinnedSslIssuedToUName = "";
3915 pinnedSslIssuedByCName = "";
3916 pinnedSslIssuedByOName = "";
3917 pinnedSslIssuedByUName = "";
3918 pinnedSslStartDate = null;
3919 pinnedSslEndDate = null;
3920 pinnedIpAddresses = false;
3921 pinnedHostIpAddresses = "";
3923 // Set third-party cookies status if API >= 21.
3924 if (Build.VERSION.SDK_INT >= 21) {
3925 cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
3928 // 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.
3929 // <https://redmine.stoutner.com/issues/160>
3930 if (!urlIsLoading) {
3931 // Get the array position of the user agent name.
3932 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3934 // Set the user agent.
3935 switch (userAgentArrayPosition) {
3936 case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
3937 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3938 currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
3941 case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3942 // Set the user agent to `""`, which uses the default value.
3943 currentWebView.getSettings().setUserAgentString("");
3946 case SETTINGS_CUSTOM_USER_AGENT:
3947 // Set the custom user agent.
3948 currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
3952 // Get the user agent string from the user agent data array
3953 currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3956 // Store the applied user agent string, which is used in the View Source activity.
3957 appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
3959 // Update the user agent change tracker.
3960 userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
3963 // Set the loading of webpage images.
3964 currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3966 // Set a transparent background on URL edit text. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3967 urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
3970 // Close the domains database helper.
3971 domainsDatabaseHelper.close();
3973 // Update the privacy icons, but only if `mainMenu` has already been populated.
3974 if (mainMenu != null) {
3975 updatePrivacyIcons(true);
3979 // Reload the website if returning from the Domains activity.
3980 if (reloadWebsite) {
3981 currentWebView.reload();
3984 // Return the user agent changed status.
3985 return userAgentChanged;
3988 private void applyProxyThroughOrbot(boolean reloadWebsite) {
3989 // Get a handle for the shared preferences.
3990 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3992 // Get the search preferences.
3993 String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
3994 String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
3995 String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3996 String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3997 String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3998 String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4000 // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
4001 ActionBar actionBar = getSupportActionBar();
4003 // Remove the incorrect lint warning later that the action bar might be null.
4004 assert actionBar != null;
4006 // Set the homepage, search, and proxy options.
4007 if (proxyThroughOrbot) { // Set the Tor options.
4008 // Set `torHomepageString` as `homepage`.
4009 homepage = torHomepageString;
4011 // If formattedUrlString is null assign the homepage to it.
4012 if (formattedUrlString == null) {
4013 formattedUrlString = homepage;
4016 // Set the search URL.
4017 if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
4018 searchURL = torSearchCustomUrlString;
4019 } else { // Use the string from the pre-built list.
4020 searchURL = torSearchString;
4023 // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
4024 OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4026 // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context.
4028 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
4030 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
4033 // Check to see if Orbot is ready.
4034 if (!orbotStatus.equals("ON")) { // Orbot is not ready.
4035 // Set `waitingForOrbot`.
4036 waitingForOrbot = true;
4038 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4039 currentWebView.getSettings().setUseWideViewPort(false);
4041 // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
4042 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
4043 } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
4044 // Reload the website.
4045 currentWebView.reload();
4047 } else { // Set the non-Tor options.
4048 // Set `homepageString` as `homepage`.
4049 homepage = homepageString;
4051 // If formattedUrlString is null assign the homepage to it.
4052 if (formattedUrlString == null) {
4053 formattedUrlString = homepage;
4056 // Set the search URL.
4057 if (searchString.equals("Custom URL")) { // Get the custom URL string.
4058 searchURL = searchCustomUrlString;
4059 } else { // Use the string from the pre-built list.
4060 searchURL = searchString;
4063 // Reset the proxy to default. The host is `""` and the port is `"0"`.
4064 OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4066 // Set the default `appBar` background. `this` refers to the context.
4068 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
4070 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
4073 // Reset `waitingForOrbot.
4074 waitingForOrbot = false;
4076 // Reload the WebViews if requested.
4077 if (reloadWebsite) {
4078 // Reload the WebViews.
4079 for (int i = 0; i < webViewPagerAdapter.webViewFragmentsList.size(); i++) {
4080 // Get the WebView tab fragment.
4081 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.webViewFragmentsList.get(i);
4083 // Get the fragment view.
4084 View fragmentView = webViewTabFragment.getView();
4086 // Only reload the WebViews if they exist.
4087 if (fragmentView != null) {
4088 // Get the nested scroll WebView from the tab fragment.
4089 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4091 // Reload the WebView.
4092 nestedScrollWebView.reload();
4099 private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4100 // Get handles for the menu items.
4101 MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript);
4102 MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies);
4103 MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage);
4104 MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh);
4106 // Update the privacy icon.
4107 if (javaScriptEnabled) { // JavaScript is enabled.
4108 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4109 } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled.
4110 privacyMenuItem.setIcon(R.drawable.warning);
4111 } else { // All the dangerous features are disabled.
4112 privacyMenuItem.setIcon(R.drawable.privacy_mode);
4115 // Update the first-party cookies icon.
4116 if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
4117 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4118 } else { // First-party cookies are disabled.
4120 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4122 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4126 // Update the DOM storage icon.
4127 if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled.
4128 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4129 } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled.
4131 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4133 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4135 } else { // JavaScript is disabled, so DOM storage is ghosted.
4137 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4139 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4143 // Update the refresh icon.
4145 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4147 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4150 // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4151 if (runInvalidateOptionsMenu) {
4152 invalidateOptionsMenu();
4156 private void openUrlWithExternalApp(String url) {
4157 // Create a download intent. Not specifying the action type will display the maximum number of options.
4158 Intent downloadIntent = new Intent();
4160 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4161 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4163 // Flag the intent to open in a new task.
4164 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4166 // Show the chooser.
4167 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4170 private void highlightUrlText() {
4171 // Get a handle for the URL edit text.
4172 EditText urlEditText = findViewById(R.id.url_edittext);
4174 // Only highlight the URL text if the box is not currently selected.
4175 if (!urlEditText.hasFocus()) {
4176 // Get the URL string.
4177 String urlString = urlEditText.getText().toString();
4179 // Highlight the URL according to the protocol.
4180 if (urlString.startsWith("file://")) { // This is a file URL.
4181 // De-emphasize only the protocol.
4182 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4183 } else if (urlString.startsWith("content://")) {
4184 // De-emphasize only the protocol.
4185 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4186 } else { // This is a web URL.
4187 // Get the index of the `/` immediately after the domain name.
4188 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4190 // Create a base URL string.
4193 // Get the base URL.
4194 if (endOfDomainName > 0) { // There is at least one character after the base URL.
4195 // Get the base URL.
4196 baseUrl = urlString.substring(0, endOfDomainName);
4197 } else { // There are no characters after the base URL.
4198 // Set the base URL to be the entire URL string.
4199 baseUrl = urlString;
4202 // Get the index of the last `.` in the domain.
4203 int lastDotIndex = baseUrl.lastIndexOf(".");
4205 // Get the index of the penultimate `.` in the domain.
4206 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4208 // Markup the beginning of the URL.
4209 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
4210 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4212 // De-emphasize subdomains.
4213 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4214 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4216 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
4217 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
4218 // De-emphasize the protocol and the additional subdomains.
4219 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4220 } else { // There is only one subdomain in the domain name.
4221 // De-emphasize only the protocol.
4222 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4226 // De-emphasize the text after the domain name.
4227 if (endOfDomainName > 0) {
4228 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4234 private void loadBookmarksFolder() {
4235 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4236 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4238 // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
4239 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4241 public View newView(Context context, Cursor cursor, ViewGroup parent) {
4242 // Inflate the individual item layout. `false` does not attach it to the root.
4243 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4247 public void bindView(View view, Context context, Cursor cursor) {
4248 // Get handles for the views.
4249 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4250 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4252 // Get the favorite icon byte array from the cursor.
4253 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4255 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4256 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4258 // Display the bitmap in `bookmarkFavoriteIcon`.
4259 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4261 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4262 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4263 bookmarkNameTextView.setText(bookmarkNameString);
4265 // Make the font bold for folders.
4266 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4267 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4268 } else { // Reset the font to default for normal bookmarks.
4269 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4274 // Populate the `ListView` with the adapter.
4275 bookmarksListView.setAdapter(bookmarksCursorAdapter);
4277 // Set the bookmarks drawer title.
4278 if (currentBookmarksFolder.isEmpty()) {
4279 bookmarksTitleTextView.setText(R.string.bookmarks);
4281 bookmarksTitleTextView.setText(currentBookmarksFolder);
4285 private void openWithApp(String url) {
4286 // Create the open with intent with `ACTION_VIEW`.
4287 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4289 // Set the URI but not the MIME type. This should open all available apps.
4290 openWithAppIntent.setData(Uri.parse(url));
4292 // Flag the intent to open in a new task.
4293 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4295 // Show the chooser.
4296 startActivity(openWithAppIntent);
4299 private void openWithBrowser(String url) {
4300 // Create the open with intent with `ACTION_VIEW`.
4301 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4303 // Set the URI and the MIME type. `"text/html"` should load browser options.
4304 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4306 // Flag the intent to open in a new task.
4307 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4309 // Show the chooser.
4310 startActivity(openWithBrowserIntent);
4313 public static void checkPinnedMismatch(int domainSettingsDatabaseId) {
4314 if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) {
4315 // Initialize the current SSL certificate variables.
4316 String currentWebsiteIssuedToCName = "";
4317 String currentWebsiteIssuedToOName = "";
4318 String currentWebsiteIssuedToUName = "";
4319 String currentWebsiteIssuedByCName = "";
4320 String currentWebsiteIssuedByOName = "";
4321 String currentWebsiteIssuedByUName = "";
4322 Date currentWebsiteSslStartDate = null;
4323 Date currentWebsiteSslEndDate = null;
4326 // Extract the individual pieces of information from the current website SSL certificate if it is not null.
4327 if (sslCertificate != null) {
4328 currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName();
4329 currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName();
4330 currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName();
4331 currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName();
4332 currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName();
4333 currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName();
4334 currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate();
4335 currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate();
4338 // Initialize string variables to store the SSL certificate dates. Strings are needed to compare the values below, which doesn't work with `Dates` if they are `null`.
4339 String currentWebsiteSslStartDateString = "";
4340 String currentWebsiteSslEndDateString = "";
4341 String pinnedSslStartDateString = "";
4342 String pinnedSslEndDateString = "";
4344 // Convert the `Dates` to `Strings` if they are not `null`.
4345 if (currentWebsiteSslStartDate != null) {
4346 currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
4349 if (currentWebsiteSslEndDate != null) {
4350 currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
4353 if (pinnedSslStartDate != null) {
4354 pinnedSslStartDateString = pinnedSslStartDate.toString();
4357 if (pinnedSslEndDate != null) {
4358 pinnedSslEndDateString = pinnedSslEndDate.toString();
4361 // Check to see if the pinned information matches the current information.
4362 if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) ||
4363 !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) ||
4364 !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) ||
4365 !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
4366 !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
4368 // Get a handle for the pinned mismatch alert dialog.
4369 DialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(domainSettingsDatabaseId, pinnedSslCertificate, pinnedIpAddresses);
4371 // Show the pinned mismatch alert dialog.
4372 pinnedMismatchDialogFragment.show(fragmentManager, "Pinned Mismatch");
4377 private class WebViewPagerAdapter extends FragmentPagerAdapter {
4378 // The WebView fragments list contains all the WebViews.
4379 private LinkedList<WebViewTabFragment> webViewFragmentsList = new LinkedList<>();
4381 // Define the constructor.
4382 private WebViewPagerAdapter(FragmentManager fragmentManager){
4383 // Run the default commands.
4384 super(fragmentManager);
4388 public int getCount() {
4389 // Return the number of pages.
4390 return webViewFragmentsList.size();
4394 public int getItemPosition(@NonNull Object object) {
4395 //noinspection SuspiciousMethodCalls
4396 if (webViewFragmentsList.contains(object)) {
4397 // The tab has not been deleted.
4398 return POSITION_UNCHANGED;
4400 // The tab has been deleted.
4401 return POSITION_NONE;
4406 public Fragment getItem(int pageNumber) {
4407 // Get a WebView for a particular page. Page numbers are 0 indexed.
4408 return webViewFragmentsList.get(pageNumber);
4411 private void addPage() {
4412 // Add a new page. The pages and tabs are 0 indexed, so the size of the current list equals the number of the next page.
4413 webViewFragmentsList.add(WebViewTabFragment.createTab(webViewFragmentsList.size()));
4415 // Update the view pager.
4416 notifyDataSetChanged();
4419 private void deletePage(int pageNumber) {
4420 // Get a handle for the tab layout.
4421 TabLayout tabLayout = findViewById(R.id.tablayout);
4423 // TODO always move to the next tab if possible.
4424 // Select a tab that is not being deleted.
4425 if (pageNumber == 0) { // The first tab is being deleted.
4426 // Get a handle for the second tab. The tabs are 0 indexed.
4427 TabLayout.Tab secondTab = tabLayout.getTabAt(1);
4429 // Remove the incorrect lint warning below that the second tab might be null.
4430 assert secondTab != null;
4432 // Select the second tab.
4434 } else { // The first tab is not being deleted.
4435 // Get a handle for the previous tab.
4436 TabLayout.Tab previousTab = tabLayout.getTabAt(pageNumber - 1);
4438 // Remove the incorrect lint warning below tha the previous tab might be null.
4439 assert previousTab != null;
4441 // Select the previous tab.
4442 previousTab.select();
4446 webViewFragmentsList.remove(pageNumber);
4449 tabLayout.removeTabAt(pageNumber);
4451 // Update the view pager.
4452 notifyDataSetChanged();
4456 public void addTab(View view) {
4457 // Add the new WebView page.
4458 webViewPagerAdapter.addPage();
4460 // Get a handle for the tab layout.
4461 TabLayout tabLayout = findViewById(R.id.tablayout);
4463 // Get a handle for the new tab. The tabs are 0 indexed.
4464 TabLayout.Tab newTab = tabLayout.getTabAt(tabLayout.getTabCount() - 1);
4466 // Remove the incorrect warning below that the new tab might be null.
4467 assert newTab != null;
4469 // Move the tab layout to the new tab.
4474 public void initializeWebView(int tabNumber, ProgressBar progressBar, NestedScrollWebView nestedScrollWebView) {
4475 // Get handles for the activity views.
4476 final FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4477 final DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4478 final RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4479 final ActionBar actionBar = getSupportActionBar();
4480 EditText urlEditText = findViewById(R.id.url_edittext);
4481 final TabLayout tabLayout = findViewById(R.id.tablayout);
4482 final SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4484 // Remove the incorrect lint warnings below that the some of the views might be null.
4485 assert actionBar != null;
4487 // TODO. Still doesn't work right.
4488 // Create the tab if it doesn't already exist.
4490 TabLayout.Tab tab = tabLayout.getTabAt(tabNumber);
4494 tab.getCustomView();
4495 } catch (Exception exception) {
4496 tabLayout.addTab(tabLayout.newTab());
4499 // Get the current tab.
4500 TabLayout.Tab currentTab = tabLayout.getTabAt(tabNumber);
4502 // Remove the lint warning below that the current tab might be null.
4503 assert currentTab != null;
4505 // Set a custom view on the current tab.
4506 currentTab.setCustomView(R.layout.custom_tab_view);
4508 // Get the custom view from the tab.
4509 View currentTabView = currentTab.getCustomView();
4511 // Remove the incorrect warning below that the current tab view might be null.
4512 assert currentTabView != null;
4514 // Get the current views from the tab.
4515 ImageView tabFavoriteIconImageView = currentTabView.findViewById(R.id.favorite_icon_imageview);
4516 TextView tabTitleTextView = currentTabView.findViewById(R.id.title_textview);
4518 // Get a handle for the shared preferences.
4519 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4521 // Get the relevant preferences.
4522 boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4524 // Allow pinch to zoom.
4525 nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4527 // Hide zoom controls.
4528 nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4530 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4531 if (Build.VERSION.SDK_INT >= 21) {
4532 nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4535 // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
4536 nestedScrollWebView.getSettings().setUseWideViewPort(true);
4538 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4539 nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4541 // Explicitly disable geolocation.
4542 nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4544 // Create a double-tap gesture detector to toggle full-screen mode.
4545 GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4546 // Override `onDoubleTap()`. All other events are handled using the default settings.
4548 public boolean onDoubleTap(MotionEvent event) {
4549 if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4550 // Toggle the full screen browsing mode tracker.
4551 inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4553 // Toggle the full screen browsing mode.
4554 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4555 // Hide the app bar if specified.
4560 // Hide the banner ad in the free flavor.
4561 if (BuildConfig.FLAVOR.contentEquals("free")) {
4562 AdHelper.hideAd(findViewById(R.id.adview));
4565 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4566 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4568 /* Hide the system bars.
4569 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4570 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4571 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4572 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4574 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4575 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4576 } else { // Switch to normal viewing mode.
4577 // Show the app bar.
4580 // Show the banner ad in the free flavor.
4581 if (BuildConfig.FLAVOR.contentEquals("free")) {
4583 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4586 // Remove the `SYSTEM_UI` flags from the root frame layout.
4587 rootFrameLayout.setSystemUiVisibility(0);
4589 // Add the translucent status flag.
4590 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4593 // Consume the double-tap.
4595 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4601 // Pass all touch events on the WebView through the double-tap gesture detector.
4602 nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4603 // Call `performClick()` on the view, which is required for accessibility.
4604 view.performClick();
4606 // Send the event to the gesture detector.
4607 return doubleTapGestureDetector.onTouchEvent(event);
4610 // Register the WebView for a context menu. This is used to see link targets and download images.
4611 registerForContextMenu(nestedScrollWebView);
4613 // Allow the downloading of files.
4614 nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4615 // Check if the download should be processed by an external app.
4616 if (downloadWithExternalApp) { // Download with an external app.
4617 // Create a download intent. Not specifying the action type will display the maximum number of options.
4618 Intent downloadIntent = new Intent();
4620 // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
4621 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4623 // Flag the intent to open in a new task.
4624 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4626 // Show the chooser.
4627 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4628 } else { // Download with Android's download manager.
4629 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4630 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
4631 // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4633 // Store the variables for future use by `onRequestPermissionsResult()`.
4635 downloadContentDisposition = contentDisposition;
4636 downloadContentLength = contentLength;
4638 // Show a dialog if the user has previously denied the permission.
4639 if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
4640 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4641 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4643 // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
4644 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
4645 } else { // Show the permission request directly.
4646 // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
4647 ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4649 } else { // The storage permission has already been granted.
4650 // Get a handle for the download file alert dialog.
4651 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4653 // Show the download file alert dialog.
4654 downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
4659 // Update the find on page count.
4660 nestedScrollWebView.setFindListener(new WebView.FindListener() {
4661 // Get a handle for `findOnPageCountTextView`.
4662 final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4665 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4666 if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
4667 // Set `findOnPageCountTextView` to `0/0`.
4668 findOnPageCountTextView.setText(R.string.zero_of_zero);
4669 } else if (isDoneCounting) { // There are matches.
4670 // `activeMatchOrdinal` is zero-based.
4671 int activeMatch = activeMatchOrdinal + 1;
4673 // Build the match string.
4674 String matchString = activeMatch + "/" + numberOfMatches;
4676 // Set `findOnPageCountTextView`.
4677 findOnPageCountTextView.setText(matchString);
4682 // Set the web chrome client.
4683 nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4684 // Update the progress bar when a page is loading.
4686 public void onProgressChanged(WebView view, int progress) {
4687 // Inject the night mode CSS if night mode is enabled.
4689 // `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
4690 // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
4691 // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
4692 // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4693 nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4694 "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4695 "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4696 // Initialize a handler to display `mainWebView`.
4697 Handler displayWebViewHandler = new Handler();
4699 // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4700 Runnable displayWebViewRunnable = () -> {
4701 // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
4702 if (progressBar.getVisibility() == View.GONE) {
4703 nestedScrollWebView.setVisibility(View.VISIBLE);
4707 // Displaying of `mainWebView` after 500 milliseconds.
4708 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4712 // Update the progress bar.
4713 progressBar.setProgress(progress);
4715 // Set the visibility of the progress bar.
4716 if (progress < 100) {
4717 // Show the progress bar.
4718 progressBar.setVisibility(View.VISIBLE);
4720 // Hide the progress bar.
4721 progressBar.setVisibility(View.GONE);
4723 // Display `mainWebView` if night mode is disabled.
4724 // 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
4725 // currently enabled.
4727 nestedScrollWebView.setVisibility(View.VISIBLE);
4730 //Stop the swipe to refresh indicator if it is running
4731 swipeRefreshLayout.setRefreshing(false);
4735 // Set the favorite icon when it changes.
4737 public void onReceivedIcon(WebView view, Bitmap icon) {
4738 // Only update the favorite icon if the website has finished loading.
4739 if (progressBar.getVisibility() == View.GONE) {
4740 // Save a copy of the favorite icon.
4741 // TODO. We need to save and access the icons for each tab.
4742 favoriteIconBitmap = icon;
4744 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4748 // Save a copy of the title when it changes.
4750 public void onReceivedTitle(WebView view, String title) {
4751 // Set the title as the tab text.
4752 tabTitleTextView.setText(title);
4755 // Enter full screen video.
4757 public void onShowCustomView(View video, CustomViewCallback callback) {
4758 // Set the full screen video flag.
4759 displayingFullScreenVideo = true;
4761 // Pause the ad if this is the free flavor.
4762 if (BuildConfig.FLAVOR.contentEquals("free")) {
4763 // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4764 AdHelper.pauseAd(findViewById(R.id.adview));
4767 // Hide the keyboard.
4768 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4770 // Hide the main content relative layout.
4771 mainContentRelativeLayout.setVisibility(View.GONE);
4773 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4774 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4776 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4777 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4779 /* Hide the system bars.
4780 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4781 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4782 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4783 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4785 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4786 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4788 // Disable the sliding drawers.
4789 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4791 // Add the video view to the full screen video frame layout.
4792 fullScreenVideoFrameLayout.addView(video);
4794 // Show the full screen video frame layout.
4795 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4798 // Exit full screen video.
4800 public void onHideCustomView() {
4801 // Unset the full screen video flag.
4802 displayingFullScreenVideo = false;
4804 // Remove all the views from the full screen video frame layout.
4805 fullScreenVideoFrameLayout.removeAllViews();
4807 // Hide the full screen video frame layout.
4808 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4810 // Enable the sliding drawers.
4811 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4813 // Show the main content relative layout.
4814 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4816 // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4817 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4818 // Hide the app bar if specified.
4823 // Hide the banner ad in the free flavor.
4824 if (BuildConfig.FLAVOR.contentEquals("free")) {
4825 AdHelper.hideAd(findViewById(R.id.adview));
4828 // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
4829 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4831 /* Hide the system bars.
4832 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4833 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4834 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4835 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4837 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4838 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4839 } else { // Switch to normal viewing mode.
4840 // Remove the `SYSTEM_UI` flags from the root frame layout.
4841 rootFrameLayout.setSystemUiVisibility(0);
4843 // Add the translucent status flag.
4844 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4847 // Reload the ad for the free flavor if not in full screen mode.
4848 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4850 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4856 public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4857 // Show the file chooser if the device is running API >= 21.
4858 if (Build.VERSION.SDK_INT >= 21) {
4859 // Store the file path callback.
4860 fileChooserCallback = filePathCallback;
4862 // Create an intent to open a chooser based ont the file chooser parameters.
4863 Intent fileChooserIntent = fileChooserParams.createIntent();
4865 // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4866 startActivityForResult(fileChooserIntent, 0);
4872 nestedScrollWebView.setWebViewClient(new WebViewClient() {
4873 // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4874 // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4875 @SuppressWarnings("deprecation")
4877 public boolean shouldOverrideUrlLoading(WebView view, String url) {
4878 if (url.startsWith("http")) { // Load the URL in Privacy Browser.
4879 // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
4880 formattedUrlString = "";
4882 // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
4883 boolean userAgentChanged = applyDomainSettings(url, true, false);
4885 // Check if the user agent has changed.
4886 if (userAgentChanged) {
4887 // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
4888 nestedScrollWebView.loadUrl(url, customHeaders);
4890 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4893 // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4896 } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
4897 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4898 Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4900 // Parse the url and set it as the data for the intent.
4901 emailIntent.setData(Uri.parse(url));
4903 // Open the email program in a new task instead of as part of Privacy Browser.
4904 emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4907 startActivity(emailIntent);
4909 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4911 } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
4912 // Open the dialer and load the phone number, but wait for the user to place the call.
4913 Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4915 // Add the phone number to the intent.
4916 dialIntent.setData(Uri.parse(url));
4918 // Open the dialer in a new task instead of as part of Privacy Browser.
4919 dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4922 startActivity(dialIntent);
4924 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4926 } else { // Load a system chooser to select an app that can handle the URL.
4927 // Open an app that can handle the URL.
4928 Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4930 // Add the URL to the intent.
4931 genericIntent.setData(Uri.parse(url));
4933 // List all apps that can handle the URL instead of just opening the first one.
4934 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4936 // Open the app in a new task instead of as part of Privacy Browser.
4937 genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4939 // Start the app or display a snackbar if no app is available to handle the URL.
4941 startActivity(genericIntent);
4942 } catch (ActivityNotFoundException exception) {
4943 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
4946 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4951 // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4952 @SuppressWarnings("deprecation")
4954 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4955 // Create an empty web resource response to be used if the resource request is blocked.
4956 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4958 // Reset the whitelist results tracker.
4959 whiteListResultStringArray = null;
4961 // Initialize the third party request tracker.
4962 boolean isThirdPartyRequest = false;
4964 // Initialize the current domain string.
4965 String currentDomain = "";
4967 // Nobody is happy when comparing null strings.
4968 if (!(formattedUrlString == null) && !(url == null)) {
4969 // Get the domain strings to URIs.
4970 Uri currentDomainUri = Uri.parse(formattedUrlString);
4971 Uri requestDomainUri = Uri.parse(url);
4973 // Get the domain host names.
4974 String currentBaseDomain = currentDomainUri.getHost();
4975 String requestBaseDomain = requestDomainUri.getHost();
4977 // Update the current domain variable.
4978 currentDomain = currentBaseDomain;
4980 // Only compare the current base domain and the request base domain if neither is null.
4981 if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
4982 // Determine the current base domain.
4983 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4984 // Remove the first subdomain.
4985 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4988 // Determine the request base domain.
4989 while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
4990 // Remove the first subdomain.
4991 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4994 // Update the third party request tracker.
4995 isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4999 // Block third-party requests if enabled.
5000 if (isThirdPartyRequest && blockAllThirdPartyRequests) {
5001 // Increment the blocked requests counters.
5003 thirdPartyBlockedRequests++;
5005 // Update the titles of the blocklist menu items. This must be run from the UI thread.
5006 activity.runOnUiThread(() -> {
5007 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5008 blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5009 blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
5012 // Add the request to the log.
5013 resourceRequests.add(new String[]{String.valueOf(REQUEST_THIRD_PARTY), url});
5015 // Return an empty web resource response.
5016 return emptyWebResourceResponse;
5019 // Check UltraPrivacy if it is enabled.
5020 if (ultraPrivacyEnabled) {
5021 if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, ultraPrivacy)) {
5022 // Increment the blocked requests counters.
5024 ultraPrivacyBlockedRequests++;
5026 // Update the titles of the blocklist menu items. This must be run from the UI thread.
5027 activity.runOnUiThread(() -> {
5028 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5029 blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5030 ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
5033 // The resource request was blocked. Return an empty web resource response.
5034 return emptyWebResourceResponse;
5037 // If the whitelist result is not null, the request has been allowed by UltraPrivacy.
5038 if (whiteListResultStringArray != null) {
5039 // Add a whitelist entry to the resource requests array.
5040 resourceRequests.add(whiteListResultStringArray);
5042 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5047 // Check EasyList if it is enabled.
5048 if (easyListEnabled) {
5049 if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyList)) {
5050 // Increment the blocked requests counters.
5052 easyListBlockedRequests++;
5054 // Update the titles of the blocklist menu items. This must be run from the UI thread.
5055 activity.runOnUiThread(() -> {
5056 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5057 blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5058 easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
5061 // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
5062 whiteListResultStringArray = null;
5064 // The resource request was blocked. Return an empty web resource response.
5065 return emptyWebResourceResponse;
5069 // Check EasyPrivacy if it is enabled.
5070 if (easyPrivacyEnabled) {
5071 if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyPrivacy)) {
5072 // Increment the blocked requests counters.
5074 easyPrivacyBlockedRequests++;
5076 // Update the titles of the blocklist menu items. This must be run from the UI thread.
5077 activity.runOnUiThread(() -> {
5078 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5079 blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5080 easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
5083 // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
5084 whiteListResultStringArray = null;
5086 // The resource request was blocked. Return an empty web resource response.
5087 return emptyWebResourceResponse;
5091 // Check Fanboy’s Annoyance List if it is enabled.
5092 if (fanboysAnnoyanceListEnabled) {
5093 if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList)) {
5094 // Increment the blocked requests counters.
5096 fanboysAnnoyanceListBlockedRequests++;
5098 // Update the titles of the blocklist menu items. This must be run from the UI thread.
5099 activity.runOnUiThread(() -> {
5100 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5101 blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5102 fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
5105 // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
5106 whiteListResultStringArray = null;
5108 // The resource request was blocked. Return an empty web resource response.
5109 return emptyWebResourceResponse;
5111 } else if (fanboysSocialBlockingListEnabled) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5112 if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysSocialList)) {
5113 // Increment the blocked requests counters.
5115 fanboysSocialBlockingListBlockedRequests++;
5117 // Update the titles of the blocklist menu items. This must be run from the UI thread.
5118 activity.runOnUiThread(() -> {
5119 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5120 blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
5121 fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
5124 // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
5125 whiteListResultStringArray = null;
5127 // The resource request was blocked. Return an empty web resource response.
5128 return emptyWebResourceResponse;
5132 // Add the request to the log because it hasn't been processed by any of the previous checks.
5133 if (whiteListResultStringArray != null) { // The request was processed by a whitelist.
5134 resourceRequests.add(whiteListResultStringArray);
5135 } else { // The request didn't match any blocklist entry. Log it as a default request.
5136 resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url});
5139 // The resource request has not been blocked. `return null` loads the requested resource.
5143 // Handle HTTP authentication requests.
5145 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5146 // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`.
5147 httpAuthHandler = handler;
5149 // Display the HTTP authentication dialog.
5150 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
5151 httpAuthenticationDialogFragment.show(fragmentManager, getString(R.string.http_authentication));
5154 // Update the URL in urlTextBox when the page starts to load.
5156 public void onPageStarted(WebView view, String url, Bitmap favicon) {
5157 // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
5158 // This is also used to determine when to check for pinned mismatches.
5159 urlIsLoading = true;
5161 // Reset the list of host IP addresses.
5162 currentHostIpAddresses = "";
5164 // Reset the list of resource requests.
5165 resourceRequests.clear();
5167 // Initialize the counters for requests blocked by each blocklist.
5168 blockedRequests = 0;
5169 easyListBlockedRequests = 0;
5170 easyPrivacyBlockedRequests = 0;
5171 fanboysAnnoyanceListBlockedRequests = 0;
5172 fanboysSocialBlockingListBlockedRequests = 0;
5173 ultraPrivacyBlockedRequests = 0;
5174 thirdPartyBlockedRequests = 0;
5176 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5178 nestedScrollWebView.setVisibility(View.INVISIBLE);
5181 // Hide the keyboard.
5182 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5184 // Check to see if Privacy Browser is waiting on Orbot.
5185 if (!waitingForOrbot) { // Process the URL.
5186 // 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.
5187 formattedUrlString = url;
5189 // Display the formatted URL text.
5190 urlEditText.setText(formattedUrlString);
5192 // Apply text highlighting to `urlTextBox`.
5195 // Get a URI for the current URL.
5196 Uri currentUri = Uri.parse(formattedUrlString);
5198 // Get the IP addresses for the host.
5199 new GetHostIpAddresses(activity, currentWebView.getDomainSettingsDatabaseId()).execute(currentUri.getHost());
5201 // Apply any custom domain settings if the URL was loaded by navigating history.
5202 if (navigatingHistory) {
5203 // Apply the domain settings.
5204 boolean userAgentChanged = applyDomainSettings(url, true, false);
5206 // Reset `navigatingHistory`.
5207 navigatingHistory = false;
5209 // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5210 if (userAgentChanged) {
5211 loadUrl(formattedUrlString);
5215 // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.)
5216 if (refreshMenuItem != null) {
5218 refreshMenuItem.setTitle(R.string.stop);
5220 // If the icon is displayed in the AppBar, set it according to the theme.
5221 if (displayAdditionalAppBarIcons) {
5223 refreshMenuItem.setIcon(R.drawable.close_dark);
5225 refreshMenuItem.setIcon(R.drawable.close_light);
5232 // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
5234 public void onPageFinished(WebView view, String url) {
5235 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5236 if (!waitingForOrbot) {
5237 // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5238 nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5241 // Flush any cookies to persistent storage. `CookieManager` has become very lazy about flushing cookies in recent versions.
5242 if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) {
5243 cookieManager.flush();
5246 // Update the Refresh menu item if it has been created.
5247 if (refreshMenuItem != null) {
5248 // Reset the Refresh title.
5249 refreshMenuItem.setTitle(R.string.refresh);
5251 // If the icon is displayed in the AppBar, reset it according to the theme.
5252 if (displayAdditionalAppBarIcons) {
5254 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5256 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5262 // Clear the cache and history if Incognito Mode is enabled.
5263 if (incognitoModeEnabled) {
5264 // Clear the cache. `true` includes disk files.
5265 nestedScrollWebView.clearCache(true);
5267 // Clear the back/forward history.
5268 nestedScrollWebView.clearHistory();
5270 // Manually delete cache folders.
5272 // Delete the main cache directory.
5273 privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
5275 // Delete the secondary `Service Worker` cache directory.
5276 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5277 privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5278 } catch (IOException e) {
5279 // Do nothing if an error is thrown.
5283 // Update the URL text box and apply domain settings if not waiting on Orbot.
5284 if (!waitingForOrbot) {
5285 // Check to see if `WebView` has set `url` to be `about:blank`.
5286 if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
5287 // Set `formattedUrlString` to `""`.
5288 formattedUrlString = "";
5290 urlEditText.setText(formattedUrlString);
5292 // Request focus for `urlTextBox`.
5293 urlEditText.requestFocus();
5295 // Display the keyboard.
5296 inputMethodManager.showSoftInput(urlEditText, 0);
5298 // Apply the domain settings. This clears any settings from the previous domain.
5299 applyDomainSettings(formattedUrlString, true, false);
5300 } else { // `WebView` has loaded a webpage.
5301 // 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.
5302 formattedUrlString = nestedScrollWebView.getUrl();
5304 // Only update the URL text box if the user is not typing in it.
5305 if (!urlEditText.hasFocus()) {
5306 // Display the formatted URL text.
5307 urlEditText.setText(formattedUrlString);
5309 // Apply text highlighting to `urlTextBox`.
5314 // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`.
5315 sslCertificate = nestedScrollWebView.getCertificate();
5317 // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5318 if (!gettingIpAddresses) {
5319 checkPinnedMismatch(currentWebView.getDomainSettingsDatabaseId());
5323 // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. It is also used to determine when to check for pinned mismatches.
5324 urlIsLoading = false;
5327 // Handle SSL Certificate errors.
5329 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5330 // Get the current website SSL certificate.
5331 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5333 // Extract the individual pieces of information from the current website SSL certificate.
5334 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5335 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5336 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5337 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5338 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5339 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5340 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5341 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5343 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5344 if (pinnedSslCertificate &&
5345 currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) &&
5346 currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) &&
5347 currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) &&
5348 currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) {
5350 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5352 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5353 // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
5354 sslErrorHandler = handler;
5356 // Display the SSL error `AlertDialog`.
5357 DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
5358 sslCertificateErrorDialogFragment.show(fragmentManager, getString(R.string.ssl_certificate_error));
5363 // Check to see if this is the first tab.
5364 if (tabNumber == 0) {
5365 // Set this nested scroll WebView as the current WebView.
5366 currentWebView = nestedScrollWebView;
5368 // Apply the app settings from the shared preferences.
5371 // Load the website if not waiting for Orbot to connect.
5372 if (!waitingForOrbot) {
5373 loadUrl(formattedUrlString);