+ public void onPostCreate(Bundle savedInstanceState) {
+ // Run the default commands.
+ super.onPostCreate(savedInstanceState);
+
+ // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
+ actionBarDrawerToggle.syncState();
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ // Run the default commands.
+ super.onConfigurationChanged(newConfig);
+
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+
+ // `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:
+ // https://code.google.com/p/android/issues/detail?id=20493#c8
+ // ActivityCompat.invalidateOptionsMenu(this);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ // Get the hit test result.
+ final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
+
+ // Define the URL strings.
+ final String imageUrl;
+ final String linkUrl;
+
+ // Get handles for the system managers.
+ final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+
+ // Remove the lint errors below that the clipboard manager might be null.
+ assert clipboardManager != null;
+
+ // Process the link according to the type.
+ switch (hitTestResult.getType()) {
+ // `SRC_ANCHOR_TYPE` is a link.
+ case WebView.HitTestResult.SRC_ANCHOR_TYPE:
+ // Get the target URL.
+ linkUrl = hitTestResult.getExtra();
+
+ // Set the target URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(linkUrl);
+
+ // Add an Open in New Tab entry.
+ menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the link URL in a new tab and move to it.
+ addNewTab(linkUrl, true);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open in Background entry.
+ menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the link URL in a new tab but do not move to it.
+ addNewTab(linkUrl, false);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the link URL in a `ClipData`.
+ ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
+
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Save URL entry.
+ menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired.
+ new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+ currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an empty Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+
+ // `IMAGE_TYPE` is an image.
+ case WebView.HitTestResult.IMAGE_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
+
+ // Remove the incorrect lint warning below that the image URL might be null.
+ assert imageUrl != null;
+
+ // Set the context menu title.
+ if (imageUrl.startsWith("data:")) { // The image data is contained in within the URL, making it exceedingly long.
+ // Truncate the image URL before making it the title.
+ menu.setHeaderTitle(imageUrl.substring(0, 100));
+ } else { // The image URL does not contain the full image data.
+ // Set the image URL as the title of the context menu.
+ menu.setHeaderTitle(imageUrl);
+ }
+
+ // Add an Open in New Tab entry.
+ menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the image in a new tab.
+ addNewTab(imageUrl, true);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the image URL with an external app.
+ openWithApp(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the image URL with an external browser.
+ openWithBrowser(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a View Image entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
+ // Load the image in the current tab.
+ loadUrl(currentWebView, imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Save Image entry.
+ menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired.
+ new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+ currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the image URL in a clip data.
+ ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
+
+ // Set the clip data as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(imageTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an empty Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+
+ // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
+ case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
+
+ // Instantiate a handler.
+ Handler handler = new Handler();
+
+ // Get a message from the handler.
+ Message message = handler.obtainMessage();
+
+ // Request the image details from the last touched node be returned in the message.
+ currentWebView.requestFocusNodeHref(message);
+
+ // Get the link URL from the message data.
+ linkUrl = message.getData().getString("url");
+
+ // Set the link URL as the title of the context menu.
+ menu.setHeaderTitle(linkUrl);
+
+ // Add an Open in New Tab entry.
+ menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the link URL in a new tab and move to it.
+ addNewTab(linkUrl, true);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open in Background entry.
+ menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
+ // Lod the link URL in a new tab but do not move to it.
+ addNewTab(linkUrl, false);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open Image in New Tab entry.
+ menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the image in a new tab and move to it.
+ addNewTab(imageUrl, true);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the link URL with an external app.
+ openWithApp(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the link URL with an external browser.
+ openWithBrowser(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a View Image entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // View the image in the current tab.
+ loadUrl(currentWebView, imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Save Image entry.
+ menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired.
+ new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+ currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the link URL in a clip data.
+ ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
+
+ // Set the clip data as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Save URL entry.
+ menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired.
+ new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+ currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an empty Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+
+ case WebView.HitTestResult.EMAIL_TYPE:
+ // Get the target URL.
+ linkUrl = hitTestResult.getExtra();
+
+ // Set the target URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(linkUrl);
+
+ // Add a Write Email entry.
+ menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
+ // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
+ Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
+
+ // Parse the url and set it as the data for the `Intent`.
+ emailIntent.setData(Uri.parse("mailto:" + linkUrl));
+
+ // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
+ emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ try {
+ // Make it so.
+ startActivity(emailIntent);
+ } catch (ActivityNotFoundException exception) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy Email Address entry.
+ menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
+ // Save the email address in a `ClipData`.
+ ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
+
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcEmailTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an empty Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+ }
+ }
+
+ @Override
+ public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Get the dialog.
+ Dialog dialog = dialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert dialog != null;
+
+ // Get the views from the dialog fragment.
+ EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
+ EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
+
+ // Extract the strings from the edit texts.
+ String bookmarkNameString = createBookmarkNameEditText.getText().toString();
+ String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
+
+ // 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();
+
+ // Display the new bookmark below the current items in the (0 indexed) list.
+ int newBookmarkDisplayOrder = bookmarksListView.getCount();
+
+ // Create the bookmark.
+ bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the list view.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+
+ // Scroll to the new bookmark.
+ bookmarksListView.setSelection(newBookmarkDisplayOrder);
+ }
+
+ @Override
+ public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Get the dialog.
+ Dialog dialog = dialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert dialog != null;
+
+ // Get handles for the views in the dialog fragment.
+ EditText createFolderNameEditText = dialog.findViewById(R.id.create_folder_name_edittext);
+ RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.create_folder_default_icon_radiobutton);
+ ImageView folderIconImageView = dialog.findViewById(R.id.create_folder_default_icon);
+
+ // Get new folder name string.
+ String folderNameString = createFolderNameEditText.getText().toString();
+
+ // Create a folder icon bitmap.
+ Bitmap folderIconBitmap;
+
+ // Set the folder icon bitmap according to the dialog.
+ if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = folderIconImageView.getDrawable();
+
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the WebView favorite icon.
+ // Copy the favorite icon bitmap to the folder icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+ }
+
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
+
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
+
+ // Move all the bookmarks down one in the display order.
+ for (int i = 0; i < bookmarksListView.getCount(); i++) {
+ int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
+ bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
+ }
+
+ // Create the folder, which will be placed at the top of the `ListView`.
+ bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the `ListView`.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+
+ // Scroll to the new folder.
+ bookmarksListView.setSelection(0);
+ }
+
+ @Override
+ public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
+ // Remove the incorrect lint warning below that the dialog fragment might be null.
+ assert dialogFragment != null;
+
+ // Get the dialog.
+ Dialog dialog = dialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert dialog != null;
+
+ // Get handles for the views from the dialog.
+ EditText editFolderNameEditText = dialog.findViewById(R.id.edit_folder_name_edittext);
+ RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_current_icon_radiobutton);
+ RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_default_icon_radiobutton);
+ ImageView defaultFolderIconImageView = dialog.findViewById(R.id.edit_folder_default_icon_imageview);
+
+ // Get the new folder name.
+ String newFolderNameString = editFolderNameEditText.getText().toString();
+
+ // Check if the favorite icon has changed.
+ if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
+ // Update the name in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
+ } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
+ // Create the new folder icon Bitmap.
+ Bitmap folderIconBitmap;
+
+ // Populate the new folder icon bitmap.
+ if (defaultFolderIconRadioButton.isChecked()) {
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
+
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the `WebView` favorite icon.
+ // Copy the favorite icon bitmap to the folder icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+ }
+
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
+
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
+
+ // Update the folder icon in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
+ } else { // The folder icon and the name have changed.
+ // Get the new folder icon `Bitmap`.
+ Bitmap folderIconBitmap;
+ if (defaultFolderIconRadioButton.isChecked()) {
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
+
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the `WebView` favorite icon.
+ // Copy the favorite icon bitmap to the folder icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+ }
+
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
+
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
+
+ // Update the folder name and icon in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
+ }
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the `ListView`.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ }
+
+ // Override `onBackPressed()` to handle the navigation drawer and and the WebViews.
+ @Override
+ public void onBackPressed() {
+ // Check the different options for processing `back`.
+ if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
+ // Close the navigation drawer.
+ drawerLayout.closeDrawer(GravityCompat.START);
+ } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
+ // close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ } else if (displayingFullScreenVideo) { // A full screen video is shown.
+ // Get a handle for the layouts.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+ FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
+
+ // 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 main content relative layout.
+ mainContentRelativeLayout.setVisibility(View.VISIBLE);
+
+ // Apply the appropriate full screen mode flags.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
+
+ /* 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);
+
+ // Reload the website if the app bar is hidden. Otherwise, there is some bug in Android that causes the WebView to be entirely black.
+ if (hideAppBar) {
+ // Reload the WebView.
+ currentWebView.reload();
+ }
+ } else { // Switch to normal viewing mode.
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+ }
+
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+ } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
+ // 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();
+ } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs.
+ // Close the current tab.
+ closeCurrentTab();
+ } else { // There isn't anything to do in Privacy Browser.
+ // Close Privacy Browser. `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
+ if (Build.VERSION.SDK_INT >= 21) {
+ finishAndRemoveTask();
+ } else {
+ finish();
+ }
+
+ // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted.
+ System.exit(0);
+ }
+ }
+
+ // Process the results of a file browse.
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
+ // Run the default commands.
+ super.onActivityResult(requestCode, resultCode, returnedIntent);
+
+ // Run the commands that correlate to the specified request code.
+ switch (requestCode) {
+ case BROWSE_FILE_UPLOAD_REQUEST_CODE:
+ // File uploads only work on API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Pass the file to the WebView.
+ fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
+ }
+ break;
+
+ case BROWSE_SAVE_WEBPAGE_REQUEST_CODE:
+ // Don't do anything if the user pressed back from the file picker.
+ if (resultCode == Activity.RESULT_OK) {
+ // Get a handle for the save dialog fragment.
+ DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
+
+ // Only update the file name if the dialog still exists.
+ if (saveWebpageDialogFragment != null) {
+ // Get a handle for the save webpage dialog.
+ Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert saveWebpageDialog != null;
+
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
+ TextView fileExistsWarningTextView = saveWebpageDialog.findViewById(R.id.file_exists_warning_textview);
+
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
+
+ // Get the file path if it isn't null.
+ if (returnedIntent.getData() != null) {
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
+
+ // Move the cursor to the end of the file name edit text.
+ fileNameEditText.setSelection(fileNamePath.length());
+
+ // Hide the file exists warning.
+ fileExistsWarningTextView.setVisibility(View.GONE);
+ }
+ }
+ }
+ break;
+
+ case BROWSE_OPEN_REQUEST_CODE:
+ // Don't do anything if the user pressed back from the file picker.
+ if (resultCode == Activity.RESULT_OK) {
+ // Get a handle for the open dialog fragment.
+ DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
+
+ // Only update the file name if the dialog still exists.
+ if (openDialogFragment != null) {
+ // Get a handle for the open dialog.
+ Dialog openDialog = openDialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert openDialog != null;
+
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
+
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
+
+ // Get the file path if it isn't null.
+ if (returnedIntent.getData() != null) {
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
+
+ // Move the cursor to the end of the file name edit text.
+ fileNameEditText.setSelection(fileNamePath.length());
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ private void loadUrlFromTextBox() {
+ // Get a handle for the URL edit text.
+ EditText urlEditText = findViewById(R.id.url_edittext);
+
+ // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
+ String unformattedUrlString = urlEditText.getText().toString().trim();
+
+ // Initialize the formatted URL string.
+ String url = "";
+
+ // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
+ if (unformattedUrlString.startsWith("content://")) { // This is a Content URL.
+ // Load the entire content URL.
+ url = unformattedUrlString;
+ } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
+ unformattedUrlString.startsWith("file://")) { // This is a standard URL.
+ // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
+ if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
+ unformattedUrlString = "https://" + unformattedUrlString;
+ }
+
+ // Initialize `unformattedUrl`.
+ URL unformattedUrl = null;
+
+ // 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.
+ try {
+ unformattedUrl = new URL(unformattedUrlString);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+
+ // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
+ String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
+ String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
+ String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
+ String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
+ String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
+
+ // Build the URI.
+ Uri.Builder uri = new Uri.Builder();
+ uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
+
+ // Decode the URI as a UTF-8 string in.
+ try {
+ url = URLDecoder.decode(uri.build().toString(), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ // Do nothing. The formatted URL string will remain blank.
+ }
+ } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string.
+ // Create an encoded URL String.
+ String encodedUrlString;
+
+ // Sanitize the search input.
+ try {
+ encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
+
+ // Add the base search URL.
+ url = searchURL + encodedUrlString;
+ }
+
+ // 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.
+ urlEditText.clearFocus();
+
+ // Make it so.
+ loadUrl(currentWebView, url);
+ }
+
+ private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
+ // Sanitize the URL.
+ url = sanitizeUrl(url);
+
+ // Apply the domain settings and load the URL.
+ applyDomainSettings(nestedScrollWebView, url, true, false, true);
+ }
+
+ public void findPreviousOnPage(View view) {
+ // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
+ currentWebView.findNext(false);
+ }
+
+ public void findNextOnPage(View view) {
+ // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
+ currentWebView.findNext(true);
+ }
+
+ public void closeFindOnPage(View view) {
+ // Get a handle for the views.
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+ EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
+
+ // Delete the contents of `find_on_page_edittext`.
+ findOnPageEditText.setText(null);
+
+ // Clear the highlighted phrases if the WebView is not null.
+ if (currentWebView != null) {
+ currentWebView.clearMatches();
+ }
+
+ // Hide the find on page linear layout.
+ findOnPageLinearLayout.setVisibility(View.GONE);
+
+ // Show the toolbar.
+ toolbar.setVisibility(View.VISIBLE);
+
+ // Get a handle for the input method manager.
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // Remove the lint warning below that the input method manager might be null.
+ assert inputMethodManager != null;
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
+ }
+
+ @Override
+ public void onApplyNewFontSize(DialogFragment dialogFragment) {
+ // Remove the incorrect lint warning below that the dialog fragment might be null.
+ assert dialogFragment != null;
+
+ // Get the dialog.
+ Dialog dialog = dialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below tha the dialog might be null.
+ assert dialog != null;
+
+ // Get a handle for the font size edit text.
+ EditText fontSizeEditText = dialog.findViewById(R.id.font_size_edittext);
+
+ // Initialize the new font size variable with the current font size.
+ int newFontSize = currentWebView.getSettings().getTextZoom();
+
+ // Get the font size from the edit text.
+ try {
+ newFontSize = Integer.parseInt(fontSizeEditText.getText().toString());
+ } catch (Exception exception) {
+ // If the edit text does not contain a valid font size do nothing.
+ }
+
+ // Apply the new font size.
+ currentWebView.getSettings().setTextZoom(newFontSize);
+ }
+
+ @Override
+ public void onOpen(DialogFragment dialogFragment) {
+ // Get the dialog.
+ Dialog dialog = dialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert dialog != null;
+
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
+
+ // Get the file path string.
+ openFilePath = fileNameEditText.getText().toString();
+
+ // Apply the domain settings. This resets the favorite icon and removes any domain settings.
+ applyDomainSettings(currentWebView, "file://" + openFilePath, true, false, false);
+
+ // Check to see if the storage permission is needed.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
+ // Open the file.
+ currentWebView.loadUrl("file://" + openFilePath);
+ } else { // The storage permission has not been granted.
+ // Get the external private directory file.
+ File externalPrivateDirectoryFile = getExternalFilesDir(null);
+
+ // Remove the incorrect lint error below that the file might be null.
+ assert externalPrivateDirectoryFile != null;
+
+ // Get the external private directory string.
+ String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
+
+ // Check to see if the file path is in the external private directory.
+ if (openFilePath.startsWith(externalPrivateDirectory)) { // the file path is in the external private directory.
+ // Open the file.
+ currentWebView.loadUrl("file://" + openFilePath);
+ } else { // The file path is in a public directory.
+ // Check if the user has previously denied the storage permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the storage permission alert dialog.
+ DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.OPEN);
+
+ // Show the storage permission alert dialog. The permission will be requested the the dialog is closed.
+ storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
+ } else { // Show the permission request directly.
+ // Request the write external storage permission. The file will be opened when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.OPEN);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) {
+ // Get the dialog.
+ Dialog dialog = dialogFragment.getDialog();
+
+ // Remove the incorrect lint warning below that the dialog might be null.
+ assert dialog != null;
+
+ // Get a handle for the edit texts.
+ EditText urlEditText = dialog.findViewById(R.id.url_edittext);
+ EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
+
+ // Store the URL.
+ if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
+ // Save the original URL.
+ saveWebpageUrl = originalUrlString;
+ } else {
+ // Get the URL from the edit text, which may have been modified.
+ saveWebpageUrl = urlEditText.getText().toString();
+ }
+
+ // Get the file path from the edit text.
+ saveWebpageFilePath = fileNameEditText.getText().toString();
+
+ // Check to see if the storage permission is needed.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
+ //Save the webpage according to the save type.
+ switch (saveType) {
+ case StoragePermissionDialog.SAVE_URL:
+ // Save the URL.
+ new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
+ break;
+
+ case StoragePermissionDialog.SAVE_ARCHIVE:
+ // Save the webpage archive.
+ saveWebpageArchive(saveWebpageFilePath);
+ break;
+
+ case StoragePermissionDialog.SAVE_IMAGE:
+ // Save the webpage image.
+ new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
+ break;
+ }
+
+ // Reset the strings.
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
+ } else { // The storage permission has not been granted.
+ // Get the external private directory file.
+ File externalPrivateDirectoryFile = getExternalFilesDir(null);
+
+ // Remove the incorrect lint error below that the file might be null.
+ assert externalPrivateDirectoryFile != null;
+
+ // Get the external private directory string.
+ String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
+
+ // Check to see if the file path is in the external private directory.
+ if (saveWebpageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
+ // Save the webpage according to the save type.
+ switch (saveType) {
+ case StoragePermissionDialog.SAVE_URL:
+ // Save the URL.
+ new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
+ break;
+
+ case StoragePermissionDialog.SAVE_ARCHIVE:
+ // Save the webpage archive.
+ saveWebpageArchive(saveWebpageFilePath);
+ break;
+
+ case StoragePermissionDialog.SAVE_IMAGE:
+ // Save the webpage image.
+ new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
+ break;
+ }
+
+ // Reset the strings.
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
+ } else { // The file path is in a public directory.
+ // Check if the user has previously denied the storage permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the storage permission alert dialog.
+ DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(saveType);
+
+ // Show the storage permission alert dialog. The permission will be requested when the dialog is closed.
+ storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
+ } else { // Show the permission request directly.
+ // Request the write external storage permission according to the save type. The URL will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onCloseStoragePermissionDialog(int requestType) {
+ // Request the write external storage permission according to the request type. The file will be opened when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType);
+
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included).
+ if (grantResults.length > 0) {
+ switch (requestCode) {
+ case StoragePermissionDialog.OPEN:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
+ // Load the file.
+ currentWebView.loadUrl("file://" + openFilePath);
+ } else { // The storage permission was not granted.
+ // Display an error snackbar.
+ Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+ }
+ break;
+
+ case StoragePermissionDialog.SAVE_URL:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
+ // Save the raw URL.
+ new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
+ } else { // The storage permission was not granted.
+ // Display an error snackbar.
+ Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+ }
+ break;
+
+ case StoragePermissionDialog.SAVE_ARCHIVE:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
+ // Save the webpage archive.
+ saveWebpageArchive(saveWebpageFilePath);
+ } else { // The storage permission was not granted.
+ // Display an error snackbar.
+ Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+ }
+ break;
+
+ case StoragePermissionDialog.SAVE_IMAGE:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
+ // Save the webpage image.
+ new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
+ } else { // The storage permission was not granted.
+ // Display an error snackbar.
+ Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+ }
+ break;
+ }
+
+ // Reset the strings.
+ openFilePath = "";
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
+ }
+ }
+
+ private void initializeApp() {
+ // Get a handle for the input method.
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // Remove the lint warning below that the input method manager might be null.
+ assert inputMethodManager != null;
+
+ // Initialize the gray foreground color spans for highlighting the URLs. The deprecated `getResources()` must be used until API >= 23.
+ initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
+ finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
+
+ // Get the current theme status.
+ int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
+ // Set the red color span according to the theme.
+ if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+ redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
+ } else {
+ redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
+ }
+
+ // Get handles for the URL views.
+ EditText urlEditText = findViewById(R.id.url_edittext);
+
+ // Remove the formatting from the URL edit text when the user is editing the text.
+ urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
+ if (hasFocus) { // The user is editing the URL text box.
+ // Remove the highlighting.
+ urlEditText.getText().removeSpan(redColorSpan);
+ urlEditText.getText().removeSpan(initialGrayColorSpan);
+ urlEditText.getText().removeSpan(finalGrayColorSpan);
+ } else { // The user has stopped editing the URL text box.
+ // Move to the beginning of the string.
+ urlEditText.setSelection(0);
+
+ // Reapply the highlighting.
+ highlightUrlText();
+ }
+ });
+
+ // Set the go button on the keyboard to load the URL in `urlTextBox`.
+ urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
+ // If the event is a key-down event on the `enter` button, load the URL.
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ // Load the URL into the mainWebView and consume the event.
+ loadUrlFromTextBox();
+
+ // If the enter key was pressed, consume the event.
+ return true;
+ } else {
+ // If any other key was pressed, do not consume the event.
+ return false;
+ }
+ });
+
+ // 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("ON") && waitingForProxy) {
+ // Reset the waiting for proxy status.
+ waitingForProxy = false;
+
+ // Get a handle for the waiting for proxy dialog.
+ DialogFragment waitingForProxyDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog));
+
+ // Dismiss the waiting for proxy dialog if it is displayed.
+ if (waitingForProxyDialogFragment != null) {
+ waitingForProxyDialogFragment.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.resetWaitingForProxyUrlString();
+ } 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.
+ NavigationView navigationView = findViewById(R.id.navigationview);
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+ 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);
+
+ // Listen for touches on the navigation menu.
+ navigationView.setNavigationItemSelectedListener(this);
+
+ // Get handles for the navigation menu and the back and forward menu items.
+ Menu navigationMenu = navigationView.getMenu();
+ MenuItem navigationBackMenuItem = navigationMenu.findItem(R.id.back);
+ MenuItem navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
+ MenuItem navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
+ MenuItem navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
+
+ // 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());
+
+ // Display the View SSL Certificate dialog.
+ viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
+ }
+ });
+
+ // 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.getFavoriteOrDefaultIcon();
+
+ // 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);
+ });