import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import com.stoutner.privacybrowser.dialogs.OpenDialog;
import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
-import com.stoutner.privacybrowser.dialogs.SaveDialog;
+import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
- PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
+ PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
// The executor service handles background tasks. It is accessed from `ViewSourceActivity`.
// It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes.
public static String proxyMode = ProxyHelper.NONE;
-
- // The permission result request codes are used in `onCreateContextMenu()`, `onRequestPermissionResult()`, `onSaveWebpage()`, `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
- private final int PERMISSION_OPEN_REQUEST_CODE = 0;
- private final int PERMISSION_SAVE_URL_REQUEST_CODE = 1;
- private final int PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE = 2;
- private final int PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE = 3;
-
// Define the saved instance state constants.
private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
}
@Override
- // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
+ // Remove Android Studio's warning about the dangers of enabling JavaScript. We know. Oh, how we know.
@SuppressLint("SetJavaScriptEnabled")
public boolean onOptionsItemSelected(MenuItem menuItem) {
// Get the selected menu item ID.
// Consume the event.
return true;
- case R.id.save_as_archive:
- // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired.
- new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_ARCHIVE, currentWebView.getSettings().getUserAgentString(),
- currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
+ case R.id.save_archive:
+ // Instantiate the save dialog.
+ DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null,
+ false);
+
+ // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name.
+ saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
// Consume the event.
return true;
- case R.id.save_as_image:
- // Prepare the save dialog. The dialog will be displayed once the file size adn the content disposition have been acquired.
- new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_IMAGE, currentWebView.getSettings().getUserAgentString(),
- currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
+ case R.id.save_image:
+ // Instantiate the save dialog.
+ DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null,
+ false);
+
+ // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name.
+ saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
// Consume the event.
return true;
// Create the share intent.
Intent shareIntent = new Intent(Intent.ACTION_SEND);
+
+ // Add the share string to the intent.
shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
+
+ // Set the MIME type.
shareIntent.setType("text/plain");
+ // Set the intent to open in a new task.
+ shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
// Make it so.
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
- // Store the hit test result.
+ // Get the hit test result.
final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
// Define the URL strings.
// Get the image URL.
imageUrl = hitTestResult.getExtra();
- // Set the image URL as the title of the context menu.
- menu.setHeaderTitle(imageUrl);
+ // Remove the incorrect lint warning below that the image URL might be null.
+ assert imageUrl != null;
+
+ // Set the context menu title.
+ if (imageUrl.startsWith("data:")) { // The image data is contained in within the URL, making it exceedingly long.
+ // Truncate the image URL before making it the title.
+ menu.setHeaderTitle(imageUrl.substring(0, 100));
+ } else { // The image URL does not contain the full image data.
+ // Set the image URL as the title of the context menu.
+ menu.setHeaderTitle(imageUrl);
+ }
// Add an Open in New Tab entry.
menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
// `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Make it so.
- startActivity(emailIntent);
+ try {
+ // Make it so.
+ startActivity(emailIntent);
+ } catch (ActivityNotFoundException exception) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
// Consume the event.
return true;
storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
} else { // Show the permission request directly.
// Request the write external storage permission. The file will be opened when it finishes.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.OPEN);
}
}
}
}
@Override
- public void onSaveWebpage(int saveType, DialogFragment dialogFragment) {
+ public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) {
// Get the dialog.
Dialog dialog = dialogFragment.getDialog();
EditText urlEditText = dialog.findViewById(R.id.url_edittext);
EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
- // Get the strings from the edit texts.
- saveWebpageUrl = urlEditText.getText().toString();
+ // Store the URL.
+ if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
+ // Save the original URL.
+ saveWebpageUrl = originalUrlString;
+ } else {
+ // Get the URL from the edit text, which may have been modified.
+ saveWebpageUrl = urlEditText.getText().toString();
+ }
+
+ // Get the file path from the edit text.
saveWebpageFilePath = fileNameEditText.getText().toString();
// Check to see if the storage permission is needed.
new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
break;
- case StoragePermissionDialog.SAVE_AS_ARCHIVE:
+ case StoragePermissionDialog.SAVE_ARCHIVE:
// Save the webpage archive.
- currentWebView.saveWebArchive(saveWebpageFilePath);
+ saveWebpageArchive(saveWebpageFilePath);
break;
- case StoragePermissionDialog.SAVE_AS_IMAGE:
+ case StoragePermissionDialog.SAVE_IMAGE:
// Save the webpage image.
- new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+ new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
break;
}
+
+ // Reset the strings.
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
} else { // The storage permission has not been granted.
// Get the external private directory file.
File externalPrivateDirectoryFile = getExternalFilesDir(null);
new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
break;
- case StoragePermissionDialog.SAVE_AS_ARCHIVE:
+ case StoragePermissionDialog.SAVE_ARCHIVE:
// Save the webpage archive.
- currentWebView.saveWebArchive(saveWebpageFilePath);
+ saveWebpageArchive(saveWebpageFilePath);
break;
- case StoragePermissionDialog.SAVE_AS_IMAGE:
+ case StoragePermissionDialog.SAVE_IMAGE:
// Save the webpage image.
- new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+ new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
break;
}
+
+ // Reset the strings.
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
} else { // The file path is in a public directory.
// Check if the user has previously denied the storage permission.
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
// 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.
- switch (saveType) {
- case StoragePermissionDialog.SAVE_URL:
- // Request the write external storage permission. The URL will be saved when it finishes.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE);
-
- case StoragePermissionDialog.SAVE_AS_ARCHIVE:
- // Request the write external storage permission. The webpage archive will be saved when it finishes.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE);
- break;
-
- case StoragePermissionDialog.SAVE_AS_IMAGE:
- // 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}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE);
- break;
- }
+ // Request the write external storage permission according to the save type. The URL will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType);
}
}
}
@Override
public void onCloseStoragePermissionDialog(int requestType) {
- switch (requestType) {
- case StoragePermissionDialog.OPEN:
- // Request the write external storage permission. The file will be opened when it finishes.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
- break;
+ // Request the write external storage permission according to the request type. The file will be opened when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType);
- case StoragePermissionDialog.SAVE_URL:
- // Request the write external storage permission. The URL will be saved when it finishes.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE);
- break;
-
- case StoragePermissionDialog.SAVE_AS_ARCHIVE:
- // Request the write external storage permission. The webpage archive will be saved when it finishes.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE);
- break;
-
- case StoragePermissionDialog.SAVE_AS_IMAGE:
- // 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}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE);
- break;
- }
}
@Override
//Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included).
if (grantResults.length > 0) {
switch (requestCode) {
- case PERMISSION_OPEN_REQUEST_CODE:
+ case StoragePermissionDialog.OPEN:
// Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
// Load the file.
// Display an error snackbar.
Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
}
-
- // Reset the open file path.
- openFilePath = "";
break;
- case PERMISSION_SAVE_URL_REQUEST_CODE:
+ case StoragePermissionDialog.SAVE_URL:
// Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
// Save the raw URL.
// Display an error snackbar.
Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
}
-
- // Reset the save strings.
- saveWebpageUrl = "";
- saveWebpageFilePath = "";
break;
- case PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE:
+ case StoragePermissionDialog.SAVE_ARCHIVE:
// Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
// Save the webpage archive.
- currentWebView.saveWebArchive(saveWebpageFilePath);
+ saveWebpageArchive(saveWebpageFilePath);
} else { // The storage permission was not granted.
// Display an error snackbar.
Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
}
-
- // Reset the save webpage file path.
- saveWebpageFilePath = "";
break;
- case PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE:
+ case StoragePermissionDialog.SAVE_IMAGE:
// Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted.
// Save the webpage image.
- new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+ new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
} else { // The storage permission was not granted.
// Display an error snackbar.
Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
}
-
- // Reset the save webpage file path.
- saveWebpageFilePath = "";
break;
}
+
+ // Reset the strings.
+ openFilePath = "";
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
}
}
// Get the intent that started the app.
Intent intent = getIntent();
+ // Reset the intent. This prevents a duplicate tab from being created on restart.
+ setIntent(new Intent());
+
// Get the information from the intent.
String intentAction = intent.getAction();
Uri intentUriData = intent.getData();
}
private void closeCurrentTab() {
+ // Pause the current WebView.
+ currentWebView.onPause();
+
+ // Pause the current WebView JavaScript timers.
+ currentWebView.pauseTimers();
+
// Get the current tab number.
int currentTabNumber = tabLayout.getSelectedTabPosition();
appBarLayout.setExpanded(true);
}
+ private void saveWebpageArchive(String filePath) {
+ // Save the webpage archive.
+ currentWebView.saveWebArchive(filePath);
+
+ // Display a snackbar.
+ Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + filePath, Snackbar.LENGTH_SHORT);
+
+ // Add an open option to the snackbar.
+ saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> {
+ // Get a file for the file name string.
+ File file = new File(filePath);
+
+ // Declare a file URI variable.
+ Uri fileUri;
+
+ // Get the URI for the file according to the Android version.
+ if (Build.VERSION.SDK_INT >= 24) { // Use a file provider.
+ fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
+ } else { // Get the raw file path URI.
+ fileUri = Uri.fromFile(file);
+ }
+
+ // Get a handle for the content resolver.
+ ContentResolver contentResolver = getContentResolver();
+
+ // Create an open intent with `ACTION_VIEW`.
+ Intent openIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Set the URI and the MIME type.
+ openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
+
+ // Allow the app to read the file URI.
+ openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ // Show the chooser.
+ startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
+ });
+
+ // Show the snackbar.
+ saveWebpageArchiveSnackbar.show();
+ }
+
private void clearAndExit() {
// Get a handle for the shared preferences.
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
}
// Get the file name from the content disposition.
- String fileNameString = PrepareSaveDialog.getFileNameFromContentDisposition(this, contentDisposition, downloadUrl);
+ String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
// Instantiate the save dialog.
- DialogFragment saveDialogFragment = SaveDialog.saveUrl(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
+ DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
nestedScrollWebView.getAcceptFirstPartyCookies());
// Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name.
// Open the email program in a new task instead of as part of Privacy Browser.
emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Make it so.
- startActivity(emailIntent);
+ try {
+ // Make it so.
+ startActivity(emailIntent);
+ } catch (ActivityNotFoundException exception) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
+
// Returning true indicates Privacy Browser is handling the URL by creating an intent.
return true;
// Open the dialer in a new task instead of as part of Privacy Browser.
dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Make it so.
- startActivity(dialIntent);
+ try {
+ // Make it so.
+ startActivity(dialIntent);
+ } catch (ActivityNotFoundException exception) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
// Returning true indicates Privacy Browser is handling the URL by creating an intent.
return true;
}
}
- // Clear the cache and history if Incognito Mode is enabled.
+ // Clear the cache, history, and logcat if Incognito Mode is enabled.
if (incognitoModeEnabled) {
// Clear the cache. `true` includes disk files.
nestedScrollWebView.clearCache(true);
// Delete the secondary `Service Worker` cache directory.
// A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
- } catch (IOException e) {
+ } catch (IOException exception) {
// Do nothing if an error is thrown.
}
+
+ // Clear the logcat.
+ try {
+ // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
+ Runtime.getRuntime().exec("logcat -b all -c");
+ } catch (IOException exception) {
+ // Do nothing.
+ }
}
// Get the current page position.
// Get the intent that started the app.
Intent launchingIntent = getIntent();
+ // Reset the intent. This prevents a duplicate tab from being created on restart.
+ setIntent(new Intent());
+
// Get the information from the intent.
String launchingIntentAction = launchingIntent.getAction();
Uri launchingIntentUriData = launchingIntent.getData();