+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcEmailTypeClipData);
+ return false;
+ });
+
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ menu.add(R.string.cancel);
+ break;
+
+ // `IMAGE_TYPE` is an image.
+ case WebView.HitTestResult.IMAGE_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
+
+ // Set the image URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(imageUrl);
+
+ // Add a View Image entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
+ loadUrl(imageUrl);
+ return false;
+ });
+
+ // Add a Download Image entry.
+ menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ openUrlWithExternalApp(imageUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the image URL for use by `onRequestPermissionResult()`.
+ downloadImageUrl = imageUrl;
+
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+
+ // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download image alert dialog.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+
+ // Show the download image alert dialog.
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+ return false;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
+ // Save the image URL in a `ClipData`.
+ ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
+
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcImageTypeClipData);
+ return false;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(imageUrl);
+ return false;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(imageUrl);
+ return false;
+ });
+
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ 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();
+
+ // Set the image URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(imageUrl);
+
+ // Add a `View Image` entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
+ loadUrl(imageUrl);
+ return false;
+ });
+
+ // Add a `Download Image` entry.
+ menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ openUrlWithExternalApp(imageUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the image URL for use by `onRequestPermissionResult()`.
+ downloadImageUrl = imageUrl;
+
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+
+ // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download image alert dialog.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+
+ // Show the download image alert dialog.
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+ return false;
+ });
+
+ // Add a `Copy URL` entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
+ // Save the image URL in a `ClipData`.
+ ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
+
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
+ return false;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(imageUrl);
+ return false;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(imageUrl);
+ return false;
+ });
+
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ menu.add(R.string.cancel);
+ break;
+ }
+ }
+
+ @Override
+ public void onCreateBookmark(DialogFragment dialogFragment) {
+ // 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();
+
+ // Get a copy of the favorite icon bitmap.
+ Bitmap favoriteIcon = favoriteIconBitmap;
+
+ // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
+ favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
+ }
+
+ // 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).
+ favoriteIcon.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) {
+ // 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.
+ // Get a copy of the favorite icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+
+ // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
+ folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
+ }
+ }
+
+ // 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) {
+ // 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.
+ // Get a copy of the favorite icon bitmap.
+ Bitmap favoriteIcon = favoriteIconBitmap;
+
+ // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
+ favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
+ }
+
+ // 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).
+ favoriteIcon.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) {
+ // 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.
+ // Get a copy of the favorite icon bitmap.
+ folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
+
+ // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
+ folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
+ }
+ }
+
+ // 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.
+ // Get a copy of the favorite icon bitmap.
+ folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
+
+ // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
+ folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ @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));
+
+ // 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 (firstPartyCookiesEnabled) {
+ // 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));
+
+ // 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 (firstPartyCookiesEnabled) {
+ // 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
+ public void onHttpAuthenticationCancel() {
+ // Cancel the `HttpAuthHandler`.
+ httpAuthHandler.cancel();
+ }
+
+ @Override
+ public void onHttpAuthenticationProceed(DialogFragment dialogFragment) {
+ // Get handles for the `EditTexts`.
+ EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
+ EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
+
+ // Proceed with the HTTP authentication.
+ httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
+ }
+
+ @Override
+ public void onSslErrorCancel() {
+ sslErrorHandler.cancel();
+ }
+
+ @Override
+ public void onSslErrorProceed() {
+ sslErrorHandler.proceed();
+ }
+
+ @Override
+ public void onPinnedMismatchBack() {
+ if (currentWebView.canGoBack()) { // There is a back page in the history.
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
+
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
+
+ // Go back.
+ currentWebView.goBack();
+ } else { // There are no pages to go back to.
+ // Load a blank page
+ loadUrl("");
+ }
+ }
+
+ @Override
+ public void onPinnedMismatchProceed() {
+ // Do not check the pinned information for this domain again until the domain changes.
+ ignorePinnedDomainInformation = true;
+ }
+
+ @Override
+ public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
+
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
+
+ // Load the history entry.
+ currentWebView.goBackOrForward(moveBackOrForwardSteps);
+ }
+
+ @Override
+ public void onClearHistory() {
+ // Clear the history.
+ currentWebView.clearHistory();
+ }
+
+ // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
+ @Override
+ public void onBackPressed() {
+ // Get a handle for the drawer layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+
+ 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 formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
+
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
+
+ // Go back.
+ currentWebView.goBack();
+ } else { // There isn't anything to do in Privacy Browser.
+ // Pass `onBackPressed()` to the system.
+ super.onBackPressed();
+ }
+ }
+
+ // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // 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));
+ }
+ }
+
+ private void loadUrlFromTextBox() {
+ // 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 = urlTextBox.getText().toString().trim();
+
+ // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
+ if (unformattedUrlString.startsWith("content://")) {
+ // Load the entire content URL.
+ formattedUrlString = unformattedUrlString;
+ } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
+ || unformattedUrlString.startsWith("file://")) {
+ // 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 formattedUri = new Uri.Builder();
+ formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
+
+ // Decode `formattedUri` as a `String` in `UTF-8`.
+ try {
+ formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ // Load a blank string.
+ formattedUrlString = "";
+ }
+ } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
+ // Load a blank string.
+ formattedUrlString = "";
+ } else { // Search for the contents of the URL box.
+ // 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.
+ formattedUrlString = searchURL + encodedUrlString;
+ }
+
+ // Clear the focus from the URL text box. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
+ urlTextBox.clearFocus();
+
+ // Make it so.
+ loadUrl(formattedUrlString);
+ }
+
+ private void loadUrl(String url) {// Apply any custom domain settings.
+ // Set the URL as the formatted URL string so that checking third-party requests works correctly.
+ formattedUrlString = url;
+
+ // Apply the domain settings.
+ applyDomainSettings(url, true, false);
+
+ // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
+ urlIsLoading = !url.equals("");
+
+ // 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);
+
+ // Delete the contents of `find_on_page_edittext`.
+ findOnPageEditText.setText(null);
+
+ // Clear the highlighted phrases.
+ currentWebView.clearMatches();
+
+ // Hide the find on page linear layout.
+ findOnPageLinearLayout.setVisibility(View.GONE);
+
+ // Show the toolbar.
+ toolbar.setVisibility(View.VISIBLE);
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+ }
+
+ private void applyAppSettings() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Store the values from the shared preferences in variables.
+ incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
+ boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
+ proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
+ fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
+ hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
+ downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
+
+ // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ ActionBar actionBar = getSupportActionBar();
+
+ // Remove the incorrect lint warnings 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");
+ }
+
+ // Set the app bar scrolling.
+ currentWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
+
+ // 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) {
+ actionBar.hide();
+ } else {
+ 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 app 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(), fragmentManager, 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);
+ }
+ }
+
+
+ // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
+ // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
+ @SuppressWarnings("deprecation")
+ private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
+ // Get a handle for the URL edit text.
+ EditText urlEditText = findViewById(R.id.url_edittext);
+
+ // Get the current user agent.
+ String initialUserAgent = currentWebView.getSettings().getUserAgentString();
+
+ // Initialize a variable to track if the user agent changes.
+ boolean userAgentChanged = false;
+
+ // Parse the URL into a URI.
+ Uri uri = Uri.parse(url);
+
+ // Extract the domain from `uri`.
+ String hostName = uri.getHost();
+
+ // Initialize `loadingNewDomainName`.
+ boolean loadingNewDomainName;
+
+ // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
+ // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
+ //noinspection SimplifiableIfStatement
+ if ((hostName == null) || (currentDomainName == null)) {
+ loadingNewDomainName = true;
+ } else { // Determine if `hostName` equals `currentDomainName`.
+ loadingNewDomainName = !hostName.equals(currentDomainName);
+ }
+
+ // Strings don't like to be null.
+ if (hostName == null) {
+ hostName = "";
+ }
+
+ // Only apply the domain settings if a new domain is being loaded. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
+ if (loadingNewDomainName) {
+ // Set the new `hostname` as the `currentDomainName`.
+ currentDomainName = hostName;
+
+ // Reset the ignoring of pinned domain information.
+ ignorePinnedDomainInformation = false;
+
+ // Reset the favorite icon if specified.
+ if (resetFavoriteIcon) {
+ // Store the favorite icon bitmap.
+ favoriteIconBitmap = favoriteIconDefaultBitmap;
+
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+
+ // Get the current tab.
+ TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition());
+
+ // Remove the warning below that the current tab might be null.
+ assert currentTab != null;
+
+ // Get the current tab custom view.
+ View currentTabCustomView = currentTab.getCustomView();
+
+ // Remove the warning below that the current tab custom view might be null.
+ assert currentTabCustomView != null;
+
+ // Get the current tab favorite icon image view.
+ ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
+
+ // Set the default favorite icon as the favorite icon for this tab.
+ currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
+ }
+
+ // Get a handle for the swipe refresh layout.
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+ DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+
+ // Get a full cursor from `domainsDatabaseHelper`.
+ Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
+
+ // Initialize `domainSettingsSet`.
+ Set<String> domainSettingsSet = new HashSet<>();
+
+ // Get the domain name column index.
+ int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
+
+ // Populate `domainSettingsSet`.
+ for (int i = 0; i < domainNameCursor.getCount(); i++) {
+ // Move `domainsCursor` to the current row.
+ domainNameCursor.moveToPosition(i);
+
+ // Store the domain name in `domainSettingsSet`.
+ domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
+ }
+
+ // Close `domainNameCursor.
+ domainNameCursor.close();
+
+ // Initialize variables to track if domain settings will be applied and, if so, under which name.
+ domainSettingsApplied = false;
+ String domainNameInDatabase = null;
+
+ // Check the hostname.
+ if (domainSettingsSet.contains(hostName)) {
+ domainSettingsApplied = true;
+ domainNameInDatabase = hostName;
+ }
+
+ // Check all the subdomains of the host name against wildcard domains in the domain cursor.
+ while (!domainSettingsApplied && hostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
+ if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`.
+ // Apply the domain settings.
+ domainSettingsApplied = true;
+
+ // Store the applied domain names as it appears in the database.
+ domainNameInDatabase = "*." + hostName;
+ }
+
+ // Strip out the lowest subdomain of of the host name.
+ hostName = hostName.substring(hostName.indexOf(".") + 1);
+ }
+
+
+ // Get a handle for the shared preference.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Store the general preference information.
+ String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
+ String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
+ defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
+ boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
+ nightMode = sharedPreferences.getBoolean("night_mode", false);
+ boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
+
+ if (domainSettingsApplied) { // The url has custom domain settings.
+ // Get a cursor for the current host and move it to the first position.
+ Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
+ currentHostDomainSettingsCursor.moveToFirst();
+
+ // Get the settings from the cursor.
+ domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
+ javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
+ firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
+ thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
+ domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
+ // Form data can be removed once the minimum API >= 26.
+ saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
+ easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
+ easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
+ fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
+ fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
+ ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
+ blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
+ String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
+ int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
+ int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
+ int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
+ int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
+ pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
+ pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
+ pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
+ pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
+ pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
+ pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
+ pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
+ pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
+ pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
+
+ // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
+ switch (nightModeInt) {
+ case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
+ nightMode = true;
+ break;
+
+ case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
+ nightMode = false;
+ break;
+ }
+
+ // Store the domain JavaScript status. This is used by the options menu night mode toggle.
+ domainSettingsJavaScriptEnabled = javaScriptEnabled;
+
+ // Enable JavaScript if night mode is enabled.
+ if (nightMode) {
+ javaScriptEnabled = true;
+ }
+
+ // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
+ if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
+ pinnedSslStartDate = null;
+ } else {
+ pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+ }
+
+ // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
+ if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
+ pinnedSslEndDate = null;
+ } else {
+ pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+ }
+
+ // Close `currentHostDomainSettingsCursor`.
+ currentHostDomainSettingsCursor.close();
+
+ // Apply the domain settings.
+ currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+ cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
+ currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
+
+ // Apply the form data setting if the API < 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
+ }
+
+ // Apply the font size.
+ if (fontSize == 0) { // Apply the default font size.
+ currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+ } else { // Apply the specified font size.
+ currentWebView.getSettings().setTextZoom(fontSize);
+ }
+
+ // Set third-party cookies status if API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
+ }
+
+ // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
+ // <https://redmine.stoutner.com/issues/160>
+ if (!urlIsLoading) {
+ // Set the user agent.
+ if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
+ // Get the array position of the default user agent name.
+ int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
+
+ // Set the user agent according to the system default.
+ switch (defaultUserAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
+ // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
+ currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
+ break;
+
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ currentWebView.getSettings().setUserAgentString("");
+ break;
+
+ case SETTINGS_CUSTOM_USER_AGENT:
+ // Set the custom user agent.
+ currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
+ break;
+
+ default:
+ // Get the user agent string from the user agent data array
+ currentWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
+ }
+ } else { // Set the user agent according to the stored name.
+ // Get the array position of the user agent name.
+ int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
+
+ switch (userAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
+ currentWebView.getSettings().setUserAgentString(userAgentName);
+ break;
+
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ currentWebView.getSettings().setUserAgentString("");
+ break;
+
+ default:
+ // Get the user agent string from the user agent data array.
+ currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
+ }
+ }
+
+ // Store the applied user agent string, which is used in the View Source activity.
+ appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
+
+ // Update the user agent change tracker.
+ userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
+ }
+
+ // Set swipe to refresh.
+ switch (swipeToRefreshInt) {
+ case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
+ // Set swipe to refresh according to the default.
+ swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
+ break;
+
+ case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
+ // Enable swipe to refresh.
+ swipeRefreshLayout.setEnabled(true);
+ break;
+
+ case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
+ // Disable swipe to refresh.
+ swipeRefreshLayout.setEnabled(false);
+ }
+
+ // Set the loading of webpage images.
+ switch (displayWebpageImagesInt) {
+ case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
+ currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
+ break;
+
+ case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
+ currentWebView.getSettings().setLoadsImagesAutomatically(true);
+ break;
+
+ case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
+ currentWebView.getSettings().setLoadsImagesAutomatically(false);
+ break;
+ }
+
+ // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
+ if (darkTheme) {
+ urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
+ } else {
+ urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
+ }
+ } else { // The new URL does not have custom domain settings. Load the defaults.
+ // Store the values from `sharedPreferences` in variables.
+ javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
+ firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
+ thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
+ domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
+ saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
+ easyListEnabled = sharedPreferences.getBoolean("easylist", true);
+ easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
+ fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
+ fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
+ ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
+ blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
+
+ // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
+ if (nightMode) {
+ javaScriptEnabled = true;
+ }
+
+ // Apply the default settings.
+ currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+ cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
+ currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
+ currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+ swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
+
+ // Apply the form data setting if the API < 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
+ }
+
+ // Reset the pinned variables.
+ domainSettingsDatabaseId = -1;
+ pinnedSslCertificate = false;
+ pinnedSslIssuedToCName = "";
+ pinnedSslIssuedToOName = "";
+ pinnedSslIssuedToUName = "";
+ pinnedSslIssuedByCName = "";
+ pinnedSslIssuedByOName = "";
+ pinnedSslIssuedByUName = "";
+ pinnedSslStartDate = null;
+ pinnedSslEndDate = null;
+ pinnedIpAddresses = false;
+ pinnedHostIpAddresses = "";
+
+ // Set third-party cookies status if API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
+ }
+
+ // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
+ // <https://redmine.stoutner.com/issues/160>
+ if (!urlIsLoading) {
+ // Get the array position of the user agent name.
+ int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
+
+ // Set the user agent.
+ switch (userAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
+ // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
+ currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
+ break;
+
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ currentWebView.getSettings().setUserAgentString("");
+ break;
+
+ case SETTINGS_CUSTOM_USER_AGENT:
+ // Set the custom user agent.
+ currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
+ break;
+
+ default:
+ // Get the user agent string from the user agent data array
+ currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
+ }
+
+ // Store the applied user agent string, which is used in the View Source activity.
+ appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
+
+ // Update the user agent change tracker.
+ userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
+ }
+
+ // Set the loading of webpage images.
+ currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
+
+ // Set a transparent background on `urlTextBox`. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
+ urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
+ }
+
+ // Close the domains database helper.
+ domainsDatabaseHelper.close();
+
+ // Update the privacy icons, but only if `mainMenu` has already been populated.
+ if (mainMenu != null) {
+ updatePrivacyIcons(true);
+ }
+ }
+
+ // Reload the website if returning from the Domains activity.
+ if (reloadWebsite) {
+ currentWebView.reload();
+ }
+
+ // Return the user agent changed status.
+ return userAgentChanged;
+ }
+
+ private void applyProxyThroughOrbot(boolean reloadWebsite) {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the search preferences.
+ String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
+ String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
+ String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
+ String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
+ String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
+ String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));