+ // Create an Orbot status broadcast receiver.
+ orbotStatusBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Store the content of the status message in `orbotStatus`.
+ orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
+
+ // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
+ if ((orbotStatus != null) && orbotStatus.equals(ProxyHelper.ORBOT_STATUS_ON) && waitingForProxy) {
+ // Reset the waiting for proxy status.
+ waitingForProxy = false;
+
+ // Get a list of the current fragments.
+ List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
+
+ // Check each fragment to see if it is a waiting for proxy dialog. Sometimes more than one is displayed.
+ for (int i = 0; i < fragmentList.size(); i++) {
+ // Get the fragment tag.
+ String fragmentTag = fragmentList.get(i).getTag();
+
+ // Check to see if it is the waiting for proxy dialog.
+ if ((fragmentTag!= null) && fragmentTag.equals(getString(R.string.waiting_for_proxy_dialog))) {
+ // Dismiss the waiting for proxy dialog.
+ ((DialogFragment) fragmentList.get(i)).dismiss();
+ }
+ }
+
+ // Reload existing URLs and load any URLs that are waiting for the proxy.
+ for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+ // Get the WebView tab fragment.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+ // Get the fragment view.
+ View fragmentView = webViewTabFragment.getView();
+
+ // Only process the WebViews if they exist.
+ if (fragmentView != null) {
+ // Get the nested scroll WebView from the tab fragment.
+ NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Get the waiting for proxy URL string.
+ String waitingForProxyUrlString = nestedScrollWebView.getWaitingForProxyUrlString();
+
+ // Load the pending URL if it exists.
+ if (!waitingForProxyUrlString.isEmpty()) { // A URL is waiting to be loaded.
+ // Load the URL.
+ loadUrl(nestedScrollWebView, waitingForProxyUrlString);
+
+ // Reset the waiting for proxy URL string.
+ nestedScrollWebView.setWaitingForProxyUrlString("");
+ } else { // No URL is waiting to be loaded.
+ // Reload the existing URL.
+ nestedScrollWebView.reload();
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // Register the Orbot status broadcast receiver on `this` context.
+ this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
+
+ // Get handles for views that need to be modified.
+ LinearLayout bookmarksHeaderLinearLayout = findViewById(R.id.bookmarks_header_linearlayout);
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+ FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
+ FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
+ FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
+ EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
+
+ // Update the web view pager every time a tab is modified.
+ webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ // Close the find on page bar if it is open.
+ closeFindOnPage(null);
+
+ // Set the current WebView.
+ setCurrentWebView(position);
+
+ // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled by creating a new tab.
+ if (tabLayout.getSelectedTabPosition() != position) {
+ // Wait until the new tab has been created.
+ tabLayout.post(() -> {
+ // Get a handle for the tab.
+ TabLayout.Tab tab = tabLayout.getTabAt(position);
+
+ // Assert that the tab is not null.
+ assert tab != null;
+
+ // Select the tab.
+ tab.select();
+ });
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // Do nothing.
+ }
+ });
+
+ // Display the View SSL Certificate dialog when the currently selected tab is reselected.
+ tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ // Select the same page in the view pager.
+ webViewPager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ // Instantiate the View SSL Certificate dialog.
+ DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteIcon());
+
+ // Display the View SSL Certificate dialog.
+ viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
+ }
+ });
+
+ // Set a touch listener on the bookmarks header linear layout so that touches don't pass through to the button underneath.
+ bookmarksHeaderLinearLayout.setOnTouchListener((view, motionEvent) -> {
+ // Consume the touch.
+ return true;
+ });
+
+ // Set the launch bookmarks activity FAB to launch the bookmarks activity.
+ launchBookmarksActivityFab.setOnClickListener(v -> {
+ // Get a copy of the favorite icon bitmap.
+ Bitmap favoriteIconBitmap = currentWebView.getFavoriteIcon();
+
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
+
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
+
+ // Create an intent to launch the bookmarks activity.
+ Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
+
+ // Add the extra information to the intent.
+ bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
+ bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
+ bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
+ bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
+
+ // Make it so.
+ startActivity(bookmarksIntent);
+ });
+
+ // Set the create new bookmark folder FAB to display an alert dialog.
+ createBookmarkFolderFab.setOnClickListener(v -> {
+ // Create a create bookmark folder dialog.
+ DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteIcon());
+
+ // Show the create bookmark folder dialog.
+ createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
+ });
+
+ // Set the create new bookmark FAB to display an alert dialog.
+ createBookmarkFab.setOnClickListener(view -> {
+ // Instantiate the create bookmark dialog.
+ DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteIcon());
+
+ // Display the create bookmark dialog.
+ createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
+ });
+
+ // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
+ findOnPageEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing.
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null.
+ if (currentWebView != null) {
+ currentWebView.findAllAsync(findOnPageEditText.getText().toString());
+ }
+ }
+ });
+
+ // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
+ findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
+ // Hide the soft keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+
+ // Consume the event.
+ return true;
+ } else { // A different key was pressed.
+ // Do not consume the event.
+ return false;
+ }
+ });
+
+ // Implement swipe to refresh.
+ swipeRefreshLayout.setOnRefreshListener(() -> {
+ // Reload the website.
+ currentWebView.reload();
+ });
+
+ // Store the default progress view offsets for use later in `initializeWebView()`.
+ defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
+ defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
+
+ // Set the refresh color scheme according to the theme.
+ swipeRefreshLayout.setColorSchemeResources(R.color.blue_text);
+
+ // Initialize a color background typed value.
+ TypedValue colorBackgroundTypedValue = new TypedValue();
+
+ // Get the color background from the theme.
+ getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
+
+ // Get the color background int from the typed value.
+ int colorBackgroundInt = colorBackgroundTypedValue.data;
+
+ // Set the swipe refresh background color.
+ swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
+
+ // The drawer titles identify the drawer layouts in accessibility mode.
+ drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
+ drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
+
+ // Load the bookmarks folder.
+ loadBookmarksFolder();
+
+ // Handle clicks on bookmarks.
+ bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
+ // Convert the id from long to int to match the format of the bookmarks database.
+ int databaseId = (int) id;
+
+ // Get the bookmark cursor for this ID.
+ Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
+
+ // Move the bookmark cursor to the first row.
+ bookmarkCursor.moveToFirst();
+
+ // Act upon the bookmark according to the type.
+ if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
+ // Store the new folder name in `currentBookmarksFolder`.
+ currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
+
+ // Load the new folder.
+ loadBookmarksFolder();
+ } else { // The selected bookmark is not a folder.
+ // Load the bookmark URL.
+ loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)));
+
+ // Close the bookmarks drawer if it is not pinned.
+ if (!bookmarksDrawerPinned)
+ drawerLayout.closeDrawer(GravityCompat.END);
+ }
+
+ // Close the cursor.
+ bookmarkCursor.close();
+ });
+
+ // Handle long-presses on bookmarks.
+ bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
+ // Convert the database ID from `long` to `int`.
+ int databaseId = (int) id;
+
+ // Find out if the selected bookmark is a folder.
+ boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
+
+ // Check to see if the bookmark is a folder.
+ if (isFolder) { // The bookmark is a folder.
+ // Get a cursor of all the bookmarks in the folder.
+ Cursor bookmarksCursor = bookmarksDatabaseHelper.getFolderBookmarks(databaseId);
+
+ // Move to the first entry in the cursor.
+ bookmarksCursor.moveToFirst();
+
+ // Open each bookmark
+ for (int i = 0; i < bookmarksCursor.getCount(); i++) {
+ // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned.
+ addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), (!bookmarksDrawerPinned && (i == 0)));
+
+ // Move to the next bookmark.
+ bookmarksCursor.moveToNext();
+ }
+
+ // Close the cursor.
+ bookmarksCursor.close();
+ } else { // The bookmark is not a folder.
+ // Get the bookmark cursor for this ID.
+ Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
+
+ // Move the bookmark cursor to the first row.
+ bookmarkCursor.moveToFirst();
+
+ // Load the bookmark in a new tab and move to the tab if the drawer is not pinned.
+ addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned);
+
+ // Close the cursor.
+ bookmarkCursor.close();
+ }
+
+ // Close the bookmarks drawer if it is not pinned.
+ if (!bookmarksDrawerPinned)
+ drawerLayout.closeDrawer(GravityCompat.END);
+
+ // Consume the event.
+ return true;
+ });
+
+ // The drawer listener is used to update the navigation menu.
+ drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
+ @Override
+ public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
+ }
+
+ @Override
+ public void onDrawerOpened(@NonNull View drawerView) {
+ }
+
+ @Override
+ public void onDrawerClosed(@NonNull View drawerView) {
+ // Reset the drawer icon when the drawer is closed. Otherwise, it is an arrow if the drawer is open when the app is restarted.
+ actionBarDrawerToggle.syncState();
+ }
+
+ @Override
+ public void onDrawerStateChanged(int newState) {
+ if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
+ // Update the navigation menu items if the WebView is not null.
+ if (currentWebView != null) {
+ navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
+ navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
+ navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
+
+ // Hide the keyboard (if displayed).
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+ }
+
+ // Clear the focus from from the URL text box. This removes any text selection markers and context menus, which otherwise draw above the open drawers.
+ urlEditText.clearFocus();
+
+ // Clear the focus from from the WebView if it is not null, which can happen if a user opens a drawer while the browser is being resumed.
+ if (currentWebView != null) {
+ // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
+ currentWebView.clearFocus();
+ }
+ }
+ }
+ });
+
+ // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
+ @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
+
+ // Get a handle for the WebView.
+ WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
+
+ // Store the default user agent.
+ webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
+
+ // Destroy the bare WebView.
+ bareWebView.destroy();
+ }
+
+ private void applyAppSettings() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Store the values from the shared preferences in variables.
+ incognitoModeEnabled = sharedPreferences.getBoolean(getString(R.string.incognito_mode_key), false);
+ sanitizeTrackingQueries = sharedPreferences.getBoolean(getString(R.string.tracking_queries_key), true);
+ sanitizeAmpRedirects = sharedPreferences.getBoolean(getString(R.string.amp_redirects_key), true);
+ proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value));
+ fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false);
+ hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true);
+ downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false);
+ scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true);
+
+ // Apply the saved proxy mode if the app has been restarted.
+ if (savedProxyMode != null) {
+ // Apply the saved proxy mode.
+ proxyMode = savedProxyMode;
+
+ // Reset the saved proxy mode.
+ savedProxyMode = null;
+ }
+
+ // Get the search string.
+ String searchString = sharedPreferences.getString(getString(R.string.search_key), getString(R.string.search_default_value));
+
+ // Set the search string.
+ if (searchString.equals(getString(R.string.custom_url_item)))
+ searchURL = sharedPreferences.getString(getString(R.string.search_custom_url_key), getString(R.string.search_custom_url_default_value));
+ else
+ searchURL = searchString;
+
+ // Apply the proxy.
+ applyProxy(false);
+
+ // Adjust the layout and scrolling parameters according to the position of the app bar.
+ if (bottomAppBar) { // The app bar is on the bottom.
+ // Adjust the UI.
+ if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
+ // Reset the WebView padding to fill the available space.
+ swipeRefreshLayout.setPadding(0, 0, 0, 0);
+ } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
+ // Move the WebView above the app bar layout.
+ swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
+
+ // Show the app bar if it is scrolled off the screen.
+ if (appBarLayout.getTranslationY() != 0) {
+ // Animate the bottom app bar onto the screen.
+ objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
+
+ // Make it so.
+ objectAnimator.start();
+ }
+ }
+ } else { // The app bar is on the top.
+ // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
+ CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
+ AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
+ AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams();
+ AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams();
+
+ // Add the scrolling behavior to the layout parameters.
+ if (scrollAppBar) {
+ // Enable scrolling of the app bar.
+ swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
+ toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
+ findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
+ tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
+ } else {
+ // Disable scrolling of the app bar.
+ swipeRefreshLayoutParams.setBehavior(null);
+ toolbarLayoutParams.setScrollFlags(0);
+ findOnPageLayoutParams.setScrollFlags(0);
+ tabsLayoutParams.setScrollFlags(0);
+
+ // Expand the app bar if it is currently collapsed.
+ appBarLayout.setExpanded(true);
+ }
+
+ // Set the app bar scrolling for each WebView.
+ for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+ // Get the WebView tab fragment.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+ // Get the fragment view.
+ View fragmentView = webViewTabFragment.getView();
+
+ // Only modify the WebViews if they exist.
+ if (fragmentView != null) {
+ // Get the nested scroll WebView from the tab fragment.
+ NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Set the app bar scrolling.
+ nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
+ }
+ }
+ }
+
+ // Update the full screen browsing mode settings.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Update the visibility of the app bar, which might have changed in the settings.
+ if (hideAppBar) {
+ // Hide the tab linear layout.
+ tabsLinearLayout.setVisibility(View.GONE);
+
+ // Hide the action bar.
+ actionBar.hide();
+ } else {
+ // Show the tab linear layout.
+ tabsLinearLayout.setVisibility(View.VISIBLE);
+
+ // Show the action bar.
+ actionBar.show();
+ }
+
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ } else { // Privacy Browser is not in full screen browsing mode.
+ // 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.
+ inFullScreenBrowsingMode = false;
+
+ // Show the tab linear layout.
+ tabsLinearLayout.setVisibility(View.VISIBLE);
+
+ // Show the action bar.
+ actionBar.show();
+
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+ }
+ }
+
+ @Override
+ public void navigateHistory(@NonNull String url, int steps) {
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView, url, false, false, false);
+
+ // Load the history entry.
+ currentWebView.goBackOrForward(steps);
+ }
+
+ @Override
+ public void pinnedErrorGoBack() {
+ // Get the current web back forward list.
+ WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
+
+ // Get the previous entry URL.
+ String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
+
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView, previousUrl, false, false, false);
+
+ // Go back.
+ currentWebView.goBack();
+ }
+
+ // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
+ @SuppressLint("SetJavaScriptEnabled")
+ private void applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite, boolean loadUrl) {
+ // Store the current URL.
+ nestedScrollWebView.setCurrentUrl(url);
+
+ // Parse the URL into a URI.
+ Uri uri = Uri.parse(url);
+
+ // Extract the domain from `uri`.
+ String newHostName = uri.getHost();
+
+ // Strings don't like to be null.
+ if (newHostName == null) {
+ newHostName = "";
+ }
+
+ // Apply the domain settings if a new domain is being loaded or if the new domain is blank. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
+ if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
+ // Set the new host name as the current domain name.
+ nestedScrollWebView.setCurrentDomainName(newHostName);
+
+ // Reset the ignoring of pinned domain information.
+ nestedScrollWebView.setIgnorePinnedDomainInformation(false);
+
+ // Clear any pinned SSL certificate or IP addresses.
+ nestedScrollWebView.clearPinnedSslCertificate();
+ nestedScrollWebView.setPinnedIpAddresses("");
+
+ // Reset the favorite icon if specified.
+ if (resetTab) {
+ // Initialize the favorite icon.
+ nestedScrollWebView.initializeFavoriteIcon();
+
+ // Get the current page position.
+ int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
+
+ // Get the corresponding tab.
+ TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
+
+ // Update the tab if it isn't null, which sometimes happens when restarting from the background.
+ if (tab != null) {
+ // Get the tab custom view.
+ View tabCustomView = tab.getCustomView();
+
+ // Remove the warning below that the tab custom view might be null.
+ assert tabCustomView != null;
+
+ // Get the tab views.
+ ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
+ TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
+
+ // Set the default favorite icon as the favorite icon for this tab.
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true));
+
+ // Set the loading title text.
+ tabTitleTextView.setText(R.string.loading);
+ }
+ }
+
+ // Get a full domain name cursor.
+ Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
+
+ // Initialize `domainSettingsSet`.
+ Set<String> domainSettingsSet = new HashSet<>();
+
+ // Get the domain name column index.
+ int domainNameColumnIndex = domainNameCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME);
+
+ // Populate the domain settings set.
+ for (int i = 0; i < domainNameCursor.getCount(); i++) {
+ // Move the domains cursor to the current row.
+ domainNameCursor.moveToPosition(i);
+
+ // Store the domain name in the domain settings set.
+ domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
+ }
+
+ // Close the domain name cursor.
+ domainNameCursor.close();
+
+ // Initialize the domain name in database variable.
+ String domainNameInDatabase = null;
+
+ // Check the hostname against the domain settings set.
+ if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
+ // Record the domain name in the database.
+ domainNameInDatabase = newHostName;
+
+ // Set the domain settings applied tracker to true.
+ nestedScrollWebView.setDomainSettingsApplied(true);
+ } else { // The hostname is not contained in the domain settings set.
+ // Set the domain settings applied tracker to false.
+ nestedScrollWebView.setDomainSettingsApplied(false);
+ }
+
+ // Check all the subdomains of the host name against wildcard domains in the domain cursor.
+ while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the hostname.
+ if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`.
+ // Set the domain settings applied tracker to true.
+ nestedScrollWebView.setDomainSettingsApplied(true);
+
+ // Store the applied domain names as it appears in the database.
+ domainNameInDatabase = "*." + newHostName;
+ }
+
+ // Strip out the lowest subdomain of of the host name.
+ newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
+ }
+
+
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Store the general preference information.
+ String defaultFontSizeString = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value));
+ String defaultUserAgentName = sharedPreferences.getString(getString(R.string.user_agent_key), getString(R.string.user_agent_default_value));
+ boolean defaultSwipeToRefresh = sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true);
+ String webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value));
+ boolean wideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true);
+ boolean displayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true);
+
+ // Get the WebView theme entry values string array.
+ String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Initialize the user agent array adapter and string array.
+ ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
+ String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
+
+ if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings.
+ // Remove the incorrect lint warning below that the domain name in database might be null.
+ assert domainNameInDatabase != null;
+
+ // Get a cursor for the current host.
+ Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
+
+ // Move to the first position.
+ currentDomainSettingsCursor.moveToFirst();
+
+ // Get the settings from the cursor.
+ nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID)));
+ nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
+ nestedScrollWebView.setAcceptCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)) == 1);
+ nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
+ // Form data can be removed once the minimum API >= 26.
+ boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
+ nestedScrollWebView.setEasyListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
+ nestedScrollWebView.setEasyPrivacyEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
+ nestedScrollWebView.setFanboysAnnoyanceListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
+ DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
+ nestedScrollWebView.setFanboysSocialBlockingListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
+ DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
+ nestedScrollWebView.setUltraListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)) == 1);
+ nestedScrollWebView.setUltraPrivacyEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
+ nestedScrollWebView.setBlockAllThirdPartyRequests(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
+ DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
+ String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT));
+ int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE));
+ int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
+ int webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME));
+ int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT));
+ int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES));
+ boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
+ String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
+ String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
+ String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
+ String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
+ String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
+ String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
+ Date pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)));
+ Date pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)));
+ boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
+ String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES));
+
+ // Close the current host domain settings cursor.
+ currentDomainSettingsCursor.close();
+
+ // If there is a pinned SSL certificate, store it in the WebView.
+ if (pinnedSslCertificate) {
+ nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
+ pinnedSslStartDate, pinnedSslEndDate);
+ }
+
+ // If there is a pinned IP address, store it in the WebView.
+ if (pinnedIpAddresses) {
+ nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
+ }
+
+ // Apply the cookie domain settings.
+ cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
+
+ // Apply the form data setting if the API < 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
+ }
+
+ // Apply the font size.
+ try { // Try the specified font size to see if it is valid.
+ if (fontSize == 0) { // Apply the default font size.
+ // Try to set the font size from the value in the app settings.
+ nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
+ } else { // Apply the font size from domain settings.
+ nestedScrollWebView.getSettings().setTextZoom(fontSize);
+ }
+ } catch (Exception exception) { // The specified font size is invalid
+ // Set the font size to be 100%
+ nestedScrollWebView.getSettings().setTextZoom(100);
+ }
+
+ // Set the user agent.
+ if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
+ // Get the array position of the default user agent name.
+ int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
+
+ // Set the user agent according to the system default.
+ switch (defaultUserAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
+ // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
+ nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
+ break;
+
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ nestedScrollWebView.getSettings().setUserAgentString("");
+ break;
+
+ case SETTINGS_CUSTOM_USER_AGENT:
+ // Set the default custom user agent.
+ nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value)));
+ break;
+
+ default:
+ // Get the user agent string from the user agent data array
+ nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
+ }
+ } else { // Set the user agent according to the stored name.
+ // Get the array position of the user agent name.
+ int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
+
+ switch (userAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
+ nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
+ break;
+
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ nestedScrollWebView.getSettings().setUserAgentString("");
+ break;
+
+ default:
+ // Get the user agent string from the user agent data array.
+ nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
+ }
+ }
+
+ // Set swipe to refresh.
+ switch (swipeToRefreshInt) {
+ case DomainsDatabaseHelper.SYSTEM_DEFAULT:
+ // Store the swipe to refresh status in the nested scroll WebView.
+ nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
+
+ // Update the swipe refresh layout.
+ if (defaultSwipeToRefresh) { // Swipe to refresh is enabled.
+ // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
+ if (currentWebView != null) {
+ // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
+ swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+ }
+ } else { // Swipe to refresh is disabled.
+ // Disable the swipe refresh layout.
+ swipeRefreshLayout.setEnabled(false);
+ }
+ break;
+
+ case DomainsDatabaseHelper.ENABLED:
+ // Store the swipe to refresh status in the nested scroll WebView.
+ nestedScrollWebView.setSwipeToRefresh(true);
+
+
+ // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
+ if (currentWebView != null) {
+ // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
+ swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+ }
+ break;
+
+ case DomainsDatabaseHelper.DISABLED:
+ // Store the swipe to refresh status in the nested scroll WebView.
+ nestedScrollWebView.setSwipeToRefresh(false);
+
+ // Disable swipe to refresh.
+ swipeRefreshLayout.setEnabled(false);
+ break;
+ }
+
+ // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
+ if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
+ // Set the WebView theme.
+ switch (webViewThemeInt) {
+ case DomainsDatabaseHelper.SYSTEM_DEFAULT:
+ // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
+ if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected.
+ // Turn off algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
+ } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected.
+ // Turn on algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
+ } else { // The system default theme is selected.
+ // Get the current system theme status.
+ int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
+ // Set the algorithmic darkening according to the current system theme status.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES));
+ }
+ break;
+
+ case DomainsDatabaseHelper.LIGHT_THEME:
+ // Turn off algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
+ break;
+
+ case DomainsDatabaseHelper.DARK_THEME:
+ // Turn on algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
+ break;
+ }
+ }
+
+ // Set the viewport.
+ switch (wideViewportInt) {
+ case DomainsDatabaseHelper.SYSTEM_DEFAULT:
+ nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
+ break;
+
+ case DomainsDatabaseHelper.ENABLED:
+ nestedScrollWebView.getSettings().setUseWideViewPort(true);
+ break;
+
+ case DomainsDatabaseHelper.DISABLED:
+ nestedScrollWebView.getSettings().setUseWideViewPort(false);
+ break;
+ }
+
+ // Set the loading of webpage images.
+ switch (displayWebpageImagesInt) {
+ case DomainsDatabaseHelper.SYSTEM_DEFAULT:
+ nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
+ break;
+
+ case DomainsDatabaseHelper.ENABLED:
+ nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
+ break;
+
+ case DomainsDatabaseHelper.DISABLED:
+ nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
+ break;
+ }
+
+ // Set a background on the URL relative layout to indicate that custom domain settings are being used.
+ urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.domain_settings_url_background, null));
+ } else { // The new URL does not have custom domain settings. Load the defaults.
+ // Store the values from the shared preferences.
+ nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean(getString(R.string.javascript_key), false));
+ nestedScrollWebView.setAcceptCookies(sharedPreferences.getBoolean(getString(R.string.cookies_key), false));
+ nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false));
+ boolean saveFormData = sharedPreferences.getBoolean(getString(R.string.save_form_data_key), false); // Form data can be removed once the minimum API >= 26.
+ nestedScrollWebView.setEasyListEnabled(sharedPreferences.getBoolean(getString(R.string.easylist_key), true));
+ nestedScrollWebView.setEasyPrivacyEnabled(sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true));
+ nestedScrollWebView.setFanboysAnnoyanceListEnabled(sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true));
+ nestedScrollWebView.setFanboysSocialBlockingListEnabled(sharedPreferences.getBoolean(getString(R.string.fanboys_social_blocking_list_key), true));
+ nestedScrollWebView.setUltraListEnabled(sharedPreferences.getBoolean(getString(R.string.ultralist_key), true));
+ nestedScrollWebView.setUltraPrivacyEnabled(sharedPreferences.getBoolean(getString(R.string.ultraprivacy_key), true));
+ nestedScrollWebView.setBlockAllThirdPartyRequests(sharedPreferences.getBoolean(getString(R.string.block_all_third_party_requests_key), false));
+
+ // Apply the default cookie setting.
+ cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
+
+ // Apply the default font size setting.
+ try {
+ // Try to set the font size from the value in the app settings.
+ nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
+ } catch (Exception exception) {
+ // If the app settings value is invalid, set the font size to 100%.
+ nestedScrollWebView.getSettings().setTextZoom(100);
+ }
+
+ // Apply the form data setting if the API < 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
+ }
+
+ // Store the swipe to refresh status in the nested scroll WebView.
+ nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
+
+ // Update the swipe refresh layout.
+ if (defaultSwipeToRefresh) { // Swipe to refresh is enabled.
+ // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
+ if (currentWebView != null) {
+ // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
+ swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+ }
+ } else { // Swipe to refresh is disabled.
+ // Disable the swipe refresh layout.
+ swipeRefreshLayout.setEnabled(false);
+ }
+
+ // Reset the pinned variables.
+ nestedScrollWebView.setDomainSettingsDatabaseId(-1);
+
+ // Get the array position of the user agent name.
+ int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
+
+ // Set the user agent.
+ switch (userAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
+ // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
+ nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
+ break;
+
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ nestedScrollWebView.getSettings().setUserAgentString("");
+ break;
+
+ case SETTINGS_CUSTOM_USER_AGENT:
+ // Set the default custom user agent.
+ nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value)));
+ break;
+
+ default:
+ // Get the user agent string from the user agent data array
+ nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
+ }
+
+ // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
+ if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
+ // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
+ if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // the light theme is selected.
+ // Turn off algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
+ } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected.
+ // Turn on algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
+ } else { // The system default theme is selected.
+ // Get the current system theme status.
+ int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
+ // Set the algorithmic darkening according to the current system theme status.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), currentThemeStatus == Configuration.UI_MODE_NIGHT_YES);
+ }
+ }
+
+ // Set the viewport.
+ nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
+
+ // Set the loading of webpage images.
+ nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
+
+ // Set a transparent background on the URL relative layout.
+ urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
+ }
+
+ // Close the domains database helper.
+ domainsDatabaseHelper.close();
+
+ // Update the privacy icons.
+ updatePrivacyIcons(true);
+ }
+
+ // Reload the website if returning from the Domains activity.
+ if (reloadWebsite) {
+ nestedScrollWebView.reload();
+ }
+
+ // Load the URL if directed. This makes sure that the domain settings are properly loaded before the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
+ if (loadUrl) {
+ nestedScrollWebView.loadUrl(url);
+ }
+ }
+
+ private void applyProxy(boolean reloadWebViews) {
+ // Set the proxy according to the mode.
+ proxyHelper.setProxy(getApplicationContext(), appBarLayout, proxyMode);
+
+ // Reset the waiting for proxy tracker.
+ waitingForProxy = false;
+
+ // Get the current theme status.
+ int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
+ // Update the user interface and reload the WebViews if requested.
+ switch (proxyMode) {
+ case ProxyHelper.NONE:
+ // Initialize a color background typed value.
+ TypedValue colorBackgroundTypedValue = new TypedValue();
+
+ // Get the color background from the theme.
+ getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
+
+ // Get the color background int from the typed value.
+ int colorBackgroundInt = colorBackgroundTypedValue.data;
+
+ // Set the default app bar layout background.
+ appBarLayout.setBackgroundColor(colorBackgroundInt);
+ break;
+
+ case ProxyHelper.TOR:
+ // Set the app bar background to indicate proxying through Orbot is enabled.
+ if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+ appBarLayout.setBackgroundResource(R.color.blue_50);
+ } else {
+ appBarLayout.setBackgroundResource(R.color.dark_blue_30);
+ }
+
+ // Check to see if Orbot is installed.
+ try {
+ // Get the package manager.
+ PackageManager packageManager = getPackageManager();
+
+ // Check to see if Orbot is in the list. This will throw an error and drop to the catch section if it isn't installed.
+ packageManager.getPackageInfo("org.torproject.android", 0);
+
+ // Check to see if the proxy is ready.
+ if (!orbotStatus.equals(ProxyHelper.ORBOT_STATUS_ON)) { // Orbot is not ready.
+ // Set the waiting for proxy status.
+ waitingForProxy = true;
+
+ // Show the waiting for proxy dialog if it isn't already displayed.
+ if (getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
+ // Get a handle for the waiting for proxy alert dialog.
+ DialogFragment waitingForProxyDialogFragment = new WaitingForProxyDialog();
+
+ // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
+ try {
+ // Show the waiting for proxy alert dialog.
+ waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog));
+ } catch (Exception waitingForTorException) {
+ // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
+ pendingDialogsArrayList.add(new PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)));
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException exception) { // Orbot is not installed.
+ // Show the Orbot not installed dialog if it is not already displayed.
+ if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
+ // Get a handle for the Orbot not installed alert dialog.
+ DialogFragment orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
+
+ // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
+ try {
+ // Display the Orbot not installed alert dialog.
+ orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
+ } catch (Exception orbotNotInstalledException) {
+ // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
+ pendingDialogsArrayList.add(new PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
+ }
+ }
+ }
+ break;
+
+ case ProxyHelper.I2P:
+ // Set the app bar background to indicate proxying through Orbot is enabled.
+ if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+ appBarLayout.setBackgroundResource(R.color.blue_50);
+ } else {
+ appBarLayout.setBackgroundResource(R.color.dark_blue_30);
+ }
+ // Get the package manager.
+ PackageManager packageManager = getPackageManager();
+
+ // Check to see if I2P is installed.
+ try {
+ // Check to see if the F-Droid flavor is installed. This will throw an error and drop to the catch section if it isn't installed.
+ packageManager.getPackageInfo("net.i2p.android.router", 0);
+ } catch (PackageManager.NameNotFoundException fdroidException) { // The F-Droid flavor is not installed.
+ try {
+ // Check to see if the Google Play flavor is installed. This will throw an error and drop to the catch section if it isn't installed.
+ packageManager.getPackageInfo("net.i2p.android", 0);
+ } catch (PackageManager.NameNotFoundException googlePlayException) { // The Google Play flavor is not installed.
+ // Sow the I2P not installed dialog if it is not already displayed.
+ if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
+ // Get a handle for the waiting for proxy alert dialog.
+ DialogFragment i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
+
+ // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
+ try {
+ // Display the I2P not installed alert dialog.
+ i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
+ } catch (Exception i2pNotInstalledException) {
+ // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
+ pendingDialogsArrayList.add(new PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
+ }
+ }
+ }
+ }
+ break;
+
+ case ProxyHelper.CUSTOM:
+ // Set the app bar background to indicate proxying through Orbot is enabled.
+ if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+ appBarLayout.setBackgroundResource(R.color.blue_50);
+ } else {
+ appBarLayout.setBackgroundResource(R.color.dark_blue_30);
+ }
+ break;
+ }
+
+ // Reload the WebViews if requested and not waiting for the proxy.
+ if (reloadWebViews && !waitingForProxy) {
+ // Reload the WebViews.
+ for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+ // Get the WebView tab fragment.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+ // Get the fragment view.
+ View fragmentView = webViewTabFragment.getView();
+
+ // Only reload the WebViews if they exist.
+ if (fragmentView != null) {
+ // Get the nested scroll WebView from the tab fragment.
+ NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Reload the WebView.
+ nestedScrollWebView.reload();
+ }
+ }
+ }
+ }
+
+ private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
+ // Only update the privacy icons if the options menu and the current WebView have already been populated.
+ if ((optionsMenu != null) && (currentWebView != null)) {
+ // Update the privacy icon.
+ if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
+ optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled);
+ } else if (currentWebView.getAcceptCookies()) { // JavaScript is disabled but cookies are enabled.
+ optionsPrivacyMenuItem.setIcon(R.drawable.warning);
+ } else { // All the dangerous features are disabled.
+ optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode);
+ }
+
+ // Update the cookies icon.
+ if (currentWebView.getAcceptCookies()) {
+ optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
+ } else {
+ optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled);
+ }
+
+ // Update the refresh icon.
+ if (optionsRefreshMenuItem.getTitle() == getString(R.string.refresh)) { // The refresh icon is displayed.
+ // Set the icon. Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
+ optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled);
+ } else { // The stop icon is displayed.
+ // Set the icon. Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
+ optionsRefreshMenuItem.setIcon(R.drawable.close_blue);
+ }
+
+ // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
+ if (runInvalidateOptionsMenu) {
+ invalidateOptionsMenu();
+ }
+ }
+ }
+
+ private void loadBookmarksFolder() {
+ // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Populate the bookmarks cursor adapter.
+ bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ // Inflate the individual item layout.
+ return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ // Get handles for the views.
+ ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
+ TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
+
+ // Get the favorite icon byte array from the cursor.
+ byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON));
+
+ // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
+ Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
+
+ // Display the bitmap in `bookmarkFavoriteIcon`.
+ bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
+
+ // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
+ String bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
+ bookmarkNameTextView.setText(bookmarkNameString);
+
+ // Make the font bold for folders.
+ if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
+ bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
+ } else { // Reset the font to default for normal bookmarks.
+ bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
+ }
+ }
+ };
+
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Populate the list view with the adapter.
+ bookmarksListView.setAdapter(bookmarksCursorAdapter);
+
+ // Get a handle for the bookmarks title text view.
+ TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
+
+ // Set the bookmarks drawer title.
+ if (currentBookmarksFolder.isEmpty()) {
+ bookmarksTitleTextView.setText(R.string.bookmarks);
+ } else {
+ bookmarksTitleTextView.setText(currentBookmarksFolder);
+ }
+ }
+
+ private void openWithApp(String url) {
+ // Create an open with app intent with `ACTION_VIEW`.
+ Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Set the URI but not the MIME type. This should open all available apps.
+ openWithAppIntent.setData(Uri.parse(url));
+
+ // Flag the intent to open in a new task.
+ openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Try the intent.
+ try {
+ // Show the chooser.
+ startActivity(openWithAppIntent);
+ } catch (ActivityNotFoundException exception) { // There are no apps available to open the URL.
+ // Show a snackbar with the error.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ private void openWithBrowser(String url) {
+ // Create an open with browser intent with `ACTION_VIEW`.
+ Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Set the URI and the MIME type. `"text/html"` should load browser options.
+ openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
+
+ // Flag the intent to open in a new task.
+ openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Try the intent.
+ try {
+ // Show the chooser.
+ startActivity(openWithBrowserIntent);
+ } catch (ActivityNotFoundException exception) { // There are no browsers available to open the URL.
+ // Show a snackbar with the error.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ private String sanitizeUrl(String url) {
+ // Sanitize tracking queries.
+ if (sanitizeTrackingQueries)
+ url = SanitizeUrlHelper.sanitizeTrackingQueries(url);
+
+ // Sanitize AMP redirects.
+ if (sanitizeAmpRedirects)
+ url = SanitizeUrlHelper.sanitizeAmpRedirects(url);
+
+ // Return the sanitized URL.
+ return url;
+ }
+
+ public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
+ // Store the blocklists.
+ easyList = combinedBlocklists.get(0);
+ easyPrivacy = combinedBlocklists.get(1);
+ fanboysAnnoyanceList = combinedBlocklists.get(2);
+ fanboysSocialList = combinedBlocklists.get(3);
+ ultraList = combinedBlocklists.get(4);
+ ultraPrivacy = combinedBlocklists.get(5);
+
+ // Check to see if the activity has been restarted with a saved state.
+ if ((savedStateArrayList == null) || (savedStateArrayList.size() == 0)) { // The activity has not been restarted or it was restarted on start to force the night theme.
+ // Add the first tab.
+ addNewTab("", true);
+ } else { // The activity has been restarted.
+ // Restore each tab. Once the minimum API >= 24, a `forEach()` command can be used.
+ for (int i = 0; i < savedStateArrayList.size(); i++) {
+ // Add a new tab.
+ tabLayout.addTab(tabLayout.newTab());
+
+ // Get the new tab.
+ TabLayout.Tab newTab = tabLayout.getTabAt(i);
+
+ // Remove the lint warning below that the current tab might be null.
+ assert newTab != null;
+
+ // Set a custom view on the new tab.
+ newTab.setCustomView(R.layout.tab_custom_view);
+
+ // Add the new page.
+ webViewPagerAdapter.restorePage(savedStateArrayList.get(i), savedNestedScrollWebViewStateArrayList.get(i));
+ }
+
+ // Reset the saved state variables.
+ savedStateArrayList = null;
+ savedNestedScrollWebViewStateArrayList = null;
+
+ // Restore the selected tab position.
+ if (savedTabPosition == 0) { // The first tab is selected.
+ // Set the first page as the current WebView.
+ setCurrentWebView(0);
+ } else { // the first tab is not selected.
+ // Move to the selected tab.
+ webViewPager.setCurrentItem(savedTabPosition);
+ }
+
+ // Get the intent that started the app.
+ Intent intent = getIntent();
+
+ // Reset the intent. This prevents a duplicate tab from being created on restart.
+ setIntent(new Intent());
+
+ // Get the information from the intent.
+ String intentAction = intent.getAction();
+ Uri intentUriData = intent.getData();
+ String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
+
+ // Determine if this is a web search.
+ boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
+
+ // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null.
+ if (intentUriData != null || intentStringExtra != null || isWebSearch) {
+ // Get the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Create a URL string.
+ String url;
+
+ // If the intent action is a web search, perform the search.
+ if (isWebSearch) { // The intent is a web search.
+ // Create an encoded URL string.
+ String encodedUrlString;
+
+ // Sanitize the search input and convert it to a search.
+ try {
+ encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
+
+ // Add the base search URL.
+ url = searchURL + encodedUrlString;
+ } else if (intentUriData != null) { // The intent contains a URL formatted as a URI.
+ // Set the intent data as the URL.
+ url = intentUriData.toString();
+ } else { // The intent contains a string, which might be a URL.
+ // Set the intent string as the URL.
+ url = intentStringExtra;
+ }
+
+ // Add a new tab if specified in the preferences.
+ if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) { // Load the URL in a new tab.
+ // Set the loading new intent flag.
+ loadingNewIntent = true;
+
+ // Add a new tab.
+ addNewTab(url, true);
+ } else { // Load the URL in the current tab.
+ // Make it so.
+ loadUrl(currentWebView, url);
+ }
+ }
+ }
+ }
+
+ public void addTab(View view) {
+ // Add a new tab with a blank URL.
+ addNewTab("", true);
+ }
+
+ private void addNewTab(String url, boolean moveToTab) {
+ // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
+ urlEditText.clearFocus();
+
+ // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
+ int newTabNumber = tabLayout.getTabCount();
+
+ // Add a new tab.
+ tabLayout.addTab(tabLayout.newTab());
+
+ // Get the new tab.
+ TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
+
+ // Remove the lint warning below that the current tab might be null.
+ assert newTab != null;
+
+ // Set a custom view on the new tab.
+ newTab.setCustomView(R.layout.tab_custom_view);
+
+ // Add the new WebView page.
+ webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab);
+
+ // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
+ if (bottomAppBar && moveToTab && (appBarLayout.getTranslationY() != 0)) {
+ // Animate the bottom app bar onto the screen.
+ objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
+
+ // Make it so.
+ objectAnimator.start();
+ }
+ }
+
+ public void closeTab(View view) {
+ // Run the command according to the number of tabs.
+ if (tabLayout.getTabCount() > 1) { // There is more than one tab open.
+ // Close the current tab.
+ closeCurrentTab();
+ } else { // There is only one tab open.
+ clearAndExit();
+ }
+ }
+
+ private void closeCurrentTab() {
+ // Get the current tab number.
+ int currentTabNumber = tabLayout.getSelectedTabPosition();
+
+ // Delete the current tab.
+ tabLayout.removeTabAt(currentTabNumber);
+
+ // Delete the current page. If the selected page number did not change during the delete (because the newly selected tab has has same number as the previously deleted tab), it will return true,
+ // meaning that the current WebView must be reset. Otherwise it will happen automatically as the selected tab number changes.
+ if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
+ setCurrentWebView(currentTabNumber);
+ }
+ }
+
+ private void exitFullScreenVideo() {
+ // Re-enable the screen timeout.
+ fullScreenVideoFrameLayout.setKeepScreenOn(false);
+
+ // Unset the full screen video flag.
+ displayingFullScreenVideo = false;
+
+ // Remove all the views from the full screen video frame layout.
+ fullScreenVideoFrameLayout.removeAllViews();
+
+ // Hide the full screen video frame layout.
+ fullScreenVideoFrameLayout.setVisibility(View.GONE);
+
+ // Enable the sliding drawers.
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
+
+ // Show the coordinator layout.
+ coordinatorLayout.setVisibility(View.VISIBLE);
+
+ // Apply the appropriate full screen mode flags.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ // Hide the tab linear layout.
+ tabsLinearLayout.setVisibility(View.GONE);
+
+ // Hide the action bar.
+ actionBar.hide();
+ }
+
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ } else { // Switch to normal viewing mode.
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+ }
+ }
+
+ private void clearAndExit() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Close the bookmarks cursor and database.
+ bookmarksCursor.close();
+ bookmarksDatabaseHelper.close();
+
+ // Get the status of the clear everything preference.
+ boolean clearEverything = sharedPreferences.getBoolean(getString(R.string.clear_everything_key), true);
+
+ // Get a handle for the runtime.
+ Runtime runtime = Runtime.getRuntime();