2 * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.dialogs;
22 import android.Manifest;
23 import android.annotation.SuppressLint;
24 import android.app.Activity;
25 import android.app.Dialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.pm.PackageManager;
31 import android.content.res.Configuration;
32 import android.os.AsyncTask;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.os.Environment;
36 import android.provider.DocumentsContract;
37 import android.text.Editable;
38 import android.text.InputType;
39 import android.text.TextWatcher;
40 import android.view.View;
41 import android.view.WindowManager;
42 import android.widget.Button;
43 import android.widget.EditText;
44 import android.widget.TextView;
46 import androidx.annotation.NonNull;
47 import androidx.appcompat.app.AlertDialog;
48 import androidx.core.content.ContextCompat;
49 import androidx.fragment.app.DialogFragment;
50 import androidx.preference.PreferenceManager;
52 import com.google.android.material.textfield.TextInputLayout;
53 import com.stoutner.privacybrowser.R;
54 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
55 import com.stoutner.privacybrowser.asynctasks.GetUrlSize;
56 import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
60 public class SaveWebpageDialog extends DialogFragment {
61 // Define the save webpage listener.
62 private SaveWebpageListener saveWebpageListener;
64 // The public interface is used to send information back to the parent activity.
65 public interface SaveWebpageListener {
66 void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment);
69 // Define the get URL size AsyncTask. This allows previous instances of the task to be cancelled if a new one is run.
70 private AsyncTask getUrlSize;
73 public void onAttach(@NonNull Context context) {
74 // Run the default commands.
75 super.onAttach(context);
77 // Get a handle for the save webpage listener from the launching context.
78 saveWebpageListener = (SaveWebpageListener) context;
81 public static SaveWebpageDialog saveWebpage(int saveType, String urlString, String fileSizeString, String contentDispositionFileNameString, String userAgentString, boolean cookiesEnabled) {
82 // Create an arguments bundle.
83 Bundle argumentsBundle = new Bundle();
85 // Store the arguments in the bundle.
86 argumentsBundle.putInt("save_type", saveType);
87 argumentsBundle.putString("url_string", urlString);
88 argumentsBundle.putString("file_size_string", fileSizeString);
89 argumentsBundle.putString("content_disposition_file_name_string", contentDispositionFileNameString);
90 argumentsBundle.putString("user_agent_string", userAgentString);
91 argumentsBundle.putBoolean("cookies_enabled", cookiesEnabled);
93 // Create a new instance of the save webpage dialog.
94 SaveWebpageDialog saveWebpageDialog = new SaveWebpageDialog();
96 // Add the arguments bundle to the new dialog.
97 saveWebpageDialog.setArguments(argumentsBundle);
99 // Return the new dialog.
100 return saveWebpageDialog;
103 // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
104 @SuppressLint("InflateParams")
107 public Dialog onCreateDialog(Bundle savedInstanceState) {
108 // Get a handle for the arguments.
109 Bundle arguments = getArguments();
111 // Remove the incorrect lint warning that the arguments might be null.
112 assert arguments != null;
114 // Get the arguments from the bundle.
115 int saveType = arguments.getInt("save_type");
116 String urlString = arguments.getString("url_string");
117 String fileSizeString = arguments.getString("file_size_string");
118 String contentDispositionFileNameString = arguments.getString("content_disposition_file_name_string");
119 String userAgentString = arguments.getString("user_agent_string");
120 boolean cookiesEnabled = arguments.getBoolean("cookies_enabled");
122 // Get a handle for the activity and the context.
123 Activity activity = requireActivity();
124 Context context = requireContext();
126 // Use an alert dialog builder to create the alert dialog.
127 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog);
129 // Get the current theme status.
130 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
132 // Set the icon according to the theme.
133 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) { // The night theme is enabled.
134 // Set the icon according to the save type.
136 case StoragePermissionDialog.SAVE_URL:
137 dialogBuilder.setIcon(R.drawable.copy_enabled_night);
140 case StoragePermissionDialog.SAVE_ARCHIVE:
141 dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night);
144 case StoragePermissionDialog.SAVE_IMAGE:
145 dialogBuilder.setIcon(R.drawable.images_enabled_night);
148 } else { // The day theme is enabled.
149 // Set the icon according to the save type.
151 case StoragePermissionDialog.SAVE_URL:
152 dialogBuilder.setIcon(R.drawable.copy_enabled_day);
155 case StoragePermissionDialog.SAVE_ARCHIVE:
156 dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day);
159 case StoragePermissionDialog.SAVE_IMAGE:
160 dialogBuilder.setIcon(R.drawable.images_enabled_day);
165 // Set the title according to the type.
167 case StoragePermissionDialog.SAVE_URL:
168 dialogBuilder.setTitle(R.string.save);
171 case StoragePermissionDialog.SAVE_ARCHIVE:
172 dialogBuilder.setTitle(R.string.save_archive);
175 case StoragePermissionDialog.SAVE_IMAGE:
176 dialogBuilder.setTitle(R.string.save_image);
180 // Set the view. The parent view is null because it will be assigned by the alert dialog.
181 dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_url_dialog, null));
183 // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
184 dialogBuilder.setNegativeButton(R.string.cancel, null);
186 // Set the save button listener.
187 dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> {
188 // Return the dialog fragment to the parent activity.
189 saveWebpageListener.onSaveWebpage(saveType, urlString, this);
192 // Create an alert dialog from the builder.
193 AlertDialog alertDialog = dialogBuilder.create();
195 // Remove the incorrect lint warning below that the window might be null.
196 assert alertDialog.getWindow() != null;
198 // Get a handle for the shared preferences.
199 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
201 // Get the screenshot preference.
202 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
204 // Disable screenshots if not allowed.
205 if (!allowScreenshots) {
206 alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
209 // The alert dialog must be shown before items in the layout can be modified.
212 // Get handles for the layout items.
213 TextInputLayout urlTextInputLayout = alertDialog.findViewById(R.id.url_textinputlayout);
214 EditText urlEditText = alertDialog.findViewById(R.id.url_edittext);
215 EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext);
216 Button browseButton = alertDialog.findViewById(R.id.browse_button);
217 TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview);
218 TextView fileExistsWarningTextView = alertDialog.findViewById(R.id.file_exists_warning_textview);
219 TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview);
220 Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
222 // Remove the incorrect warnings that the views might be null.
223 assert urlTextInputLayout != null;
224 assert urlEditText != null;
225 assert fileNameEditText != null;
226 assert browseButton != null;
227 assert fileSizeTextView != null;
228 assert fileExistsWarningTextView != null;
229 assert storagePermissionTextView != null;
231 // Set the file size text view.
232 fileSizeTextView.setText(fileSizeString);
234 // Modify the layout based on the save type.
235 if (saveType == StoragePermissionDialog.SAVE_URL) { // A URL is being saved.
236 // Remove the incorrect lint error below that the URL string might be null.
237 assert urlString != null;
239 // Populate the URL edit text according to the type. This must be done before the text change listener is created below so that the file size isn't requested again.
240 if (urlString.startsWith("data:")) { // The URL contains the entire data of an image.
241 // Get a substring of the data URL with the first 100 characters. Otherwise, the user interface will freeze while trying to layout the edit text.
242 String urlSubstring = urlString.substring(0, 100) + "…";
244 // Populate the URL edit text with the truncated URL.
245 urlEditText.setText(urlSubstring);
247 // Disable the editing of the URL edit text.
248 urlEditText.setInputType(InputType.TYPE_NULL);
249 } else { // The URL contains a reference to the location of the data.
250 // Populate the URL edit text with the full URL.
251 urlEditText.setText(urlString);
254 // Update the file size and the status of the save button when the URL changes.
255 urlEditText.addTextChangedListener(new TextWatcher() {
257 public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
262 public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
267 public void afterTextChanged(Editable editable) {
268 // Cancel the get URL size AsyncTask if it is running.
269 if ((getUrlSize != null)) {
270 getUrlSize.cancel(true);
273 // Get the current URL to save.
274 String urlToSave = urlEditText.getText().toString();
276 // Wipe the file size text view.
277 fileSizeTextView.setText("");
279 // Get the file size for the current URL.
280 getUrlSize = new GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave);
282 // Enable the save button if the URL and file name are populated.
283 saveButton.setEnabled(!urlToSave.isEmpty() && !fileNameEditText.getText().toString().isEmpty());
286 } else { // An archive or an image is being saved.
287 // Hide the URL edit text and the file size text view.
288 urlTextInputLayout.setVisibility(View.GONE);
289 fileSizeTextView.setVisibility(View.GONE);
292 // Update the status of the save button when the file name changes.
293 fileNameEditText.addTextChangedListener(new TextWatcher() {
295 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
300 public void onTextChanged(CharSequence s, int start, int before, int count) {
305 public void afterTextChanged(Editable s) {
306 // Get the current file name.
307 String fileNameString = fileNameEditText.getText().toString();
309 // Convert the file name string to a file.
310 File file = new File(fileNameString);
312 // Check to see if the file exists.
314 // Show the file exists warning.
315 fileExistsWarningTextView.setVisibility(View.VISIBLE);
317 // Hide the file exists warning.
318 fileExistsWarningTextView.setVisibility(View.GONE);
321 // Enable the save button based on the save type.
322 if (saveType == StoragePermissionDialog.SAVE_URL) { // A URL is being saved.
323 // Enable the save button if the file name and the URL is populated.
324 saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty());
325 } else { // An archive or an image is being saved.
326 // Enable the save button if the file name is populated.
327 saveButton.setEnabled(!fileNameString.isEmpty());
332 // Create a file name string.
333 String fileName = "";
335 // Set the file name according to the type.
337 case StoragePermissionDialog.SAVE_URL:
338 // Use the file name from the content disposition.
339 fileName = contentDispositionFileNameString;
342 case StoragePermissionDialog.SAVE_ARCHIVE:
343 // Use an archive name ending in `.mht`.
344 fileName = getString(R.string.webpage_mht);
347 case StoragePermissionDialog.SAVE_IMAGE:
348 // Use a file name ending in `.png`.
349 fileName = getString(R.string.webpage_png);
353 // Save the file name as the default file name. This must be final to be used in the lambda below.
354 final String defaultFileName = fileName;
356 // Instantiate the download location helper.
357 DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
359 // Get the default file path.
360 String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + defaultFileName;
362 // Populate the file name edit text.
363 fileNameEditText.setText(defaultFilePath);
365 // Move the cursor to the end of the default file path.
366 fileNameEditText.setSelection(defaultFilePath.length());
368 // Handle clicks on the browse button.
369 browseButton.setOnClickListener((View view) -> {
370 // Create the file picker intent.
371 Intent browseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
373 // Set the intent MIME type to include all files so that everything is visible.
374 browseIntent.setType("*/*");
376 // Set the initial file name according to the type.
377 browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
379 // Set the initial directory if the minimum API >= 26.
380 if (Build.VERSION.SDK_INT >= 26) {
381 browseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
384 // Request a file that can be opened.
385 browseIntent.addCategory(Intent.CATEGORY_OPENABLE);
387 // Start the file picker. This must be started under `activity` so that the request code is returned correctly.
388 activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE);
391 // Hide the storage permission text view if the permission has already been granted.
392 if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
393 storagePermissionTextView.setVisibility(View.GONE);
396 // Return the alert dialog.