+ }
+ }
+
+ @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 views from the dialog fragment.
+ EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
+ EditText createBookmarkUrlEditText = dialogFragment.getDialog().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, Bitmap favoriteIconBitmap) {
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Get handles for the views in the dialog fragment.
+ EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
+ RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
+ ImageView folderIconImageView = dialogFragment.getDialog().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 onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
+ // Get handles for the views from `dialogFragment`.
+ EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
+ EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
+ RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
+
+ // Store the bookmark strings.
+ String bookmarkNameString = editBookmarkNameEditText.getText().toString();
+ String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
+
+ // Update the bookmark.
+ if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
+ bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
+ } else { // Update the bookmark using the `WebView` favorite icon.
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = 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, newFavoriteIconByteArrayOutputStream);
+
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
+
+ // Update the bookmark and the favorite icon.
+ bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
+ }
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the list view.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ }
+
+ @Override
+ public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
+ // Get handles for the views from `dialogFragment`.
+ EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
+ RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
+ RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
+ ImageView defaultFolderIconImageView = dialogFragment.getDialog().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
+ public void onCloseDownloadLocationPermissionDialog(int downloadType) {
+ switch (downloadType) {
+ case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
+ // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+ break;
+
+ case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
+ // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ break;
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ // Get a handle for the fragment manager.
+ FragmentManager fragmentManager = getSupportFragmentManager();
+
+ switch (requestCode) {
+ case DOWNLOAD_FILE_REQUEST_CODE:
+ // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
+ DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
+
+ // On API 23, displaying the fragment must be delayed or the app will crash.
+ if (Build.VERSION.SDK_INT == 23) {
+ new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+ } else {
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+
+ // Reset the download variables.
+ downloadUrl = "";
+ downloadContentDisposition = "";
+ downloadContentLength = 0;
+ break;
+
+ case DOWNLOAD_IMAGE_REQUEST_CODE:
+ // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
+
+ // On API 23, displaying the fragment must be delayed or the app will crash.
+ if (Build.VERSION.SDK_INT == 23) {
+ new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+ } else {
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+
+ // Reset the image URL variable.
+ downloadImageUrl = "";
+ break;
+
+ case SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant result will be empty.
+ if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } else { // The storage permission was not granted.
+ // Display an error snackbar.
+ Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+ }
+
+ // Reset the save website image file path.
+ saveWebsiteImageFilePath = "";
+ break;
+ }
+ }
+
+ @Override
+ public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
+ // Download the image if it has an HTTP or HTTPS URI.
+ if (imageUrl.startsWith("http")) {
+ // Get a handle for the system `DOWNLOAD_SERVICE`.
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+
+ // Parse `imageUrl`.
+ DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for `imageUrl`.
+ String cookies = cookieManager.getCookie(imageUrl);
+
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
+
+ // Get the file name from the dialog fragment.
+ EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
+ String imageName = downloadImageNameEditText.getText().toString();
+
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
+ }
+
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(imageUrl);
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The image is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ @Override
+ public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
+ // Download the file if it has an HTTP or HTTPS URI.
+ if (downloadUrl.startsWith("http")) {
+ // Get a handle for the system `DOWNLOAD_SERVICE`.
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+
+ // Parse `downloadUrl`.
+ DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for `downloadUrl`.
+ String cookies = cookieManager.getCookie(downloadUrl);
+
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
+
+ // Get the file name from the dialog fragment.
+ EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
+ String fileName = downloadFileNameEditText.getText().toString();
+
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
+ }
+
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(downloadUrl);
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The download is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ // Override `onBackPressed` to handle the navigation drawer and and the WebView.
+ @Override
+ public void onBackPressed() {
+ // Get a handle for the drawer layout and the tab layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+
+ 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.
+ if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
+ // close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ } else { // A subfolder is displayed.
+ // Place the former parent folder in `currentFolder`.
+ currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
+
+ // Load the new folder.
+ loadBookmarksFolder();
+ }
+ } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
+ // Reset the current domain name so that navigation works if third-party requests are blocked.
+ currentWebView.resetCurrentDomainName();
+
+ // Set navigating history so that the domain settings are applied when the new URL is loaded.
+ currentWebView.setNavigatingHistory(true);
+
+ // 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.
+ // Run the default commands.
+ super.onBackPressed();
+
+ // 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 data) {
+ // Run the commands that correlate to the specified request code.
+ switch (requestCode) {
+ case 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, data));
+ }
+ break;
+
+ case BROWSE_SAVE_WEBPAGE_IMAGE_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 saveWebpageImageDialogFragment= (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_as_image));
+
+ // Only update the file name if the dialog still exists.
+ if (saveWebpageImageDialogFragment != null) {
+ // Get a handle for the save webpage image dialog.
+ Dialog saveWebpageImageDialog = saveWebpageImageDialogFragment.getDialog();
+
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = saveWebpageImageDialog.findViewById(R.id.file_name_edittext);
+
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
+
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
+ }
+ }
+ 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(url);
+ }
+
+ private void loadUrl(String url) {
+ // Sanitize the URL.
+ url = sanitizeUrl(url);
+
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView, url, true, false);
+
+ // Load the URL.
+ currentWebView.loadUrl(url, customHeaders);
+ }
+
+ 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 onSaveWebpageImage(DialogFragment dialogFragment) {
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = dialogFragment.getDialog().findViewById(R.id.file_name_edittext);
+
+ // Get the file path string.
+ saveWebsiteImageFilePath = 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 image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } 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 (saveWebsiteImageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } 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 = new StoragePermissionDialog();
+
+ // 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. The webpage image will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onCloseStoragePermissionDialog() {
+ // Request the write external storage permission. The webpage image will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ }
+
+ private void applyAppSettings() {
+ // Initialize the app if this is the first run. This is done here instead of in `onCreate()` to shorten the time that an unthemed background is displayed on app startup.
+ if (webViewDefaultUserAgent == null) {
+ initializeApp();
+ }
+
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Store the values from the shared preferences in variables.
+ incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
+ boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
+ sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true);
+ sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true);
+ sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true);
+ proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
+ fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
+ hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
+ scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
+
+ // Get handles for the views that need to be modified.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
+ ActionBar actionBar = getSupportActionBar();
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+ LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // Remove the incorrect lint warning below that the action bar might be null.
+ assert actionBar != null;
+
+ // Apply the proxy through Orbot settings.
+ applyProxyThroughOrbot(false);
+
+ // Set Do Not Track status.
+ if (doNotTrackEnabled) {
+ customHeaders.put("DNT", "1");
+ } else {
+ customHeaders.remove("DNT");
+ }
+
+ // 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);
+ }
+
+ // Apply the modified layout parameters.
+ swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams);
+ toolbar.setLayoutParams(toolbarLayoutParams);
+ findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams);
+ tabsLinearLayout.setLayoutParams(tabsLayoutParams);
+
+ // 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 banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
+
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ /* 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();
+
+ // Show the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
+ AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
+ }
+
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+ }
+
+ private void initializeApp() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the theme preference.
+ boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+
+ // 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 foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
+ redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
+ initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
+ finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
+
+ // 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;
+ }
+ });
+
+ // Initialize the Orbot status and the waiting for Orbot trackers.
+ orbotStatus = "unknown";
+ waitingForOrbot = false;
+
+ // Create an Orbot status `BroadcastReceiver`.
+ 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 Orbot, load the website now that Orbot is connected.
+ if (orbotStatus.equals("ON") && waitingForOrbot) {
+ // Reset the waiting for Orbot status.
+ waitingForOrbot = false;
+
+ // Get the intent that started the app.
+ Intent launchingIntent = getIntent();
+
+ // Get the information from the intent.
+ String launchingIntentAction = launchingIntent.getAction();
+ Uri launchingIntentUriData = launchingIntent.getData();
+
+ // If the intent action is a web search, perform the search.
+ if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
+ // Create an encoded URL string.
+ String encodedUrlString;
+
+ // Sanitize the search input and convert it to a search.
+ try {
+ encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
+
+ // Load the completed search URL.
+ loadUrl(searchURL + encodedUrlString);
+ } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
+ // Load the URL from the intent.
+ loadUrl(launchingIntentUriData.toString());
+ } else { // The is no URL in the intent.
+ // Select the homepage based on the proxy through Orbot status.
+ if (proxyThroughOrbot) {
+ // Load the Tor homepage.
+ loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
+ } else {
+ // Load the normal homepage.
+ loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
+ }
+ }
+ }
+ }
+ };
+
+ // Register `orbotStatusBroadcastReceiver` on `this` context.
+ this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
+
+ // Get handles for views that need to be modified.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ NavigationView navigationView = findViewById(R.id.navigationview);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+ ViewPager webViewPager = findViewById(R.id.webviewpager);
+ 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. The menu is zero-based.
+ Menu navigationMenu = navigationView.getMenu();
+ MenuItem navigationBackMenuItem = navigationMenu.getItem(2);
+ MenuItem navigationForwardMenuItem = navigationMenu.getItem(3);
+ MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4);
+ MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
+
+ // 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 via swiping in the view pager or by creating a new tab.
+ if (tabLayout.getSelectedTabPosition() != position) {
+ // Create a handler to select the tab.
+ Handler selectTabHandler = new Handler();
+
+ // Create a runnable to select the tab.
+ Runnable selectTabRunnable = () -> {
+ // 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();
+ };
+
+ // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated.
+ selectTabHandler.postDelayed(selectTabRunnable, 150);
+ }
+ }
+
+ @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 bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget.
+ // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
+ if (darkTheme) {
+ launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
+ createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
+ createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
+ bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
+ } else {
+ launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
+ createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
+ createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
+ bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
+ }
+
+ // 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);
+ });
+
+ // 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.getFavoriteOrDefaultIcon());
+
+ // 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.getFavoriteOrDefaultIcon());
+
+ // 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(() -> currentWebView.reload());
+
+ // Store the default progress view offsets for use later in `initializeWebView()`.
+ defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
+ defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
+
+ // Set the swipe to refresh color according to the theme.
+ if (darkTheme) {
+ swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
+ swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
+ } else {
+ swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
+ }
+
+ // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
+ drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
+ drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
+
+ // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
+ bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
+
+ // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
+ currentBookmarksFolder = "";
+
+ // Load the home folder, which is `""` in the database.
+ loadBookmarksFolder();
+
+ 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 and move it to the first row.
+ Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
+ bookmarkCursor.moveToFirst();
+
+ // Act upon the bookmark according to the type.
+ if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
+ // Store the new folder name in `currentBookmarksFolder`.
+ currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
+
+ // Load the new folder.
+ loadBookmarksFolder();
+ } else { // The selected bookmark is not a folder.
+ // Load the bookmark URL.
+ loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
+
+ // Close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ }
+
+ // Close the `Cursor`.
+ bookmarkCursor.close();
+ });
+
+ 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);
+
+ if (isFolder) {
+ // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
+ oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
+
+ // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
+ DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
+ editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
+ } else {
+ // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
+ DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
+ editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
+ }
+
+ // Consume the event.
+ return true;
+ });
+
+ // Get the status bar pixel size.
+ int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
+
+ // Get the resource density.
+ float screenDensity = getResources().getDisplayMetrics().density;
+
+ // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts.
+ drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
+ drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
+ drawerHeaderPaddingBottom = (int) (8 * screenDensity);
+
+ // 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) {
+ }
+
+ @Override
+ public void onDrawerStateChanged(int newState) {
+ if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing.
+ // Get handles for the drawer headers.
+ TextView navigationHeaderTextView = findViewById(R.id.navigationText);
+ TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
+
+ // Apply the navigation header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts.
+ if (navigationHeaderTextView != null) {
+ navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
+ }
+
+ // Apply the bookmarks header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts.
+ if (bookmarksHeaderTextView != null) {
+ bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
+ }
+
+ // 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));