2 * Copyright © 2019-2021 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.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.app.Dialog;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.SharedPreferences;
29 import android.content.res.Configuration;
30 import android.os.AsyncTask;
31 import android.os.Bundle;
32 import android.text.Editable;
33 import android.text.InputType;
34 import android.text.TextWatcher;
35 import android.view.View;
36 import android.view.WindowManager;
37 import android.widget.Button;
38 import android.widget.EditText;
39 import android.widget.TextView;
41 import androidx.annotation.NonNull;
42 import androidx.appcompat.app.AlertDialog;
43 import androidx.fragment.app.DialogFragment;
44 import androidx.preference.PreferenceManager;
46 import com.google.android.material.textfield.TextInputLayout;
47 import com.stoutner.privacybrowser.R;
48 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
49 import com.stoutner.privacybrowser.asynctasks.GetUrlSize;
51 public class SaveWebpageDialog extends DialogFragment {
52 public static final int SAVE_URL = 0;
53 public static final int SAVE_IMAGE = 1;
55 // Define the save webpage listener.
56 private SaveWebpageListener saveWebpageListener;
58 // The public interface is used to send information back to the parent activity.
59 public interface SaveWebpageListener {
60 void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment);
63 // Define the get URL size AsyncTask. This allows previous instances of the task to be cancelled if a new one is run.
64 @SuppressWarnings("rawtypes")
65 private AsyncTask getUrlSize;
68 public void onAttach(@NonNull Context context) {
69 // Run the default commands.
70 super.onAttach(context);
72 // Get a handle for the save webpage listener from the launching context.
73 saveWebpageListener = (SaveWebpageListener) context;
76 public static SaveWebpageDialog saveWebpage(int saveType, String urlString, String fileSizeString, String contentDispositionFileNameString, String userAgentString, boolean cookiesEnabled) {
77 // Create an arguments bundle.
78 Bundle argumentsBundle = new Bundle();
80 // Store the arguments in the bundle.
81 argumentsBundle.putInt("save_type", saveType);
82 argumentsBundle.putString("url_string", urlString);
83 argumentsBundle.putString("file_size_string", fileSizeString);
84 argumentsBundle.putString("content_disposition_file_name_string", contentDispositionFileNameString);
85 argumentsBundle.putString("user_agent_string", userAgentString);
86 argumentsBundle.putBoolean("cookies_enabled", cookiesEnabled);
88 // Create a new instance of the save webpage dialog.
89 SaveWebpageDialog saveWebpageDialog = new SaveWebpageDialog();
91 // Add the arguments bundle to the new dialog.
92 saveWebpageDialog.setArguments(argumentsBundle);
94 // Return the new dialog.
95 return saveWebpageDialog;
98 // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
99 @SuppressLint("InflateParams")
102 public Dialog onCreateDialog(Bundle savedInstanceState) {
103 // Get a handle for the arguments.
104 Bundle arguments = getArguments();
106 // Remove the incorrect lint warning that the arguments might be null.
107 assert arguments != null;
109 // Get the arguments from the bundle.
110 int saveType = arguments.getInt("save_type");
111 String originalUrlString = arguments.getString("url_string");
112 String fileSizeString = arguments.getString("file_size_string");
113 String contentDispositionFileNameString = arguments.getString("content_disposition_file_name_string");
114 String userAgentString = arguments.getString("user_agent_string");
115 boolean cookiesEnabled = arguments.getBoolean("cookies_enabled");
117 // Get handles for the context and the activity.
118 Context context = requireContext();
119 Activity activity = requireActivity();
121 // Use an alert dialog builder to create the alert dialog.
122 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog);
124 // Get the current theme status.
125 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
127 // Set the title and icon according to the save type.
131 dialogBuilder.setTitle(R.string.save_url);
133 // Set the icon according to the theme.
134 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
135 dialogBuilder.setIcon(R.drawable.copy_enabled_day);
137 dialogBuilder.setIcon(R.drawable.copy_enabled_night);
143 dialogBuilder.setTitle(R.string.save_image);
145 // Set the icon according to the theme.
146 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
147 dialogBuilder.setIcon(R.drawable.images_enabled_day);
150 dialogBuilder.setIcon(R.drawable.images_enabled_night);
155 // Set the view. The parent view is null because it will be assigned by the alert dialog.
156 dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_webpage_dialog, null));
158 // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
159 dialogBuilder.setNegativeButton(R.string.cancel, null);
161 // Set the save button listener.
162 dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> {
163 // Return the dialog fragment to the parent activity.
164 saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this);
167 // Create an alert dialog from the builder.
168 AlertDialog alertDialog = dialogBuilder.create();
170 // Remove the incorrect lint warning below that the window might be null.
171 assert alertDialog.getWindow() != null;
173 // Get a handle for the shared preferences.
174 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
176 // Get the screenshot preference.
177 boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
179 // Disable screenshots if not allowed.
180 if (!allowScreenshots) {
181 alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
184 // The alert dialog must be shown before items in the layout can be modified.
187 // Get handles for the layout items.
188 TextInputLayout urlTextInputLayout = alertDialog.findViewById(R.id.url_textinputlayout);
189 EditText urlEditText = alertDialog.findViewById(R.id.url_edittext);
190 EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext);
191 Button browseButton = alertDialog.findViewById(R.id.browse_button);
192 TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview);
193 Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
195 // Remove the incorrect warnings that the views might be null.
196 assert urlTextInputLayout != null;
197 assert urlEditText != null;
198 assert fileNameEditText != null;
199 assert browseButton != null;
200 assert fileSizeTextView != null;
202 // Set the file size text view.
203 fileSizeTextView.setText(fileSizeString);
205 // Modify the layout based on the save type.
206 if (saveType == SAVE_URL) { // A URL is being saved.
207 // Remove the incorrect lint error below that the URL string might be null.
208 assert originalUrlString != null;
210 // 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.
211 if (originalUrlString.startsWith("data:")) { // The URL contains the entire data of an image.
212 // 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.
213 String urlSubstring = originalUrlString.substring(0, 100) + "…";
215 // Populate the URL edit text with the truncated URL.
216 urlEditText.setText(urlSubstring);
218 // Disable the editing of the URL edit text.
219 urlEditText.setInputType(InputType.TYPE_NULL);
220 } else { // The URL contains a reference to the location of the data.
221 // Populate the URL edit text with the full URL.
222 urlEditText.setText(originalUrlString);
225 // Update the file size and the status of the save button when the URL changes.
226 urlEditText.addTextChangedListener(new TextWatcher() {
228 public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
233 public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
238 public void afterTextChanged(Editable editable) {
239 // Cancel the get URL size AsyncTask if it is running.
240 if ((getUrlSize != null)) {
241 getUrlSize.cancel(true);
244 // Get the current URL to save.
245 String urlToSave = urlEditText.getText().toString();
247 // Wipe the file size text view.
248 fileSizeTextView.setText("");
250 // Get the file size for the current URL.
251 getUrlSize = new GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave);
253 // Enable the save button if the URL and file name are populated.
254 saveButton.setEnabled(!urlToSave.isEmpty() && !fileNameEditText.getText().toString().isEmpty());
257 } else { // An archive or an image is being saved.
258 // Hide the URL edit text and the file size text view.
259 urlTextInputLayout.setVisibility(View.GONE);
260 fileSizeTextView.setVisibility(View.GONE);
263 // Initially disable the save button.
264 saveButton.setEnabled(false);
266 // Update the status of the save button when the file name changes.
267 fileNameEditText.addTextChangedListener(new TextWatcher() {
269 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
274 public void onTextChanged(CharSequence s, int start, int before, int count) {
279 public void afterTextChanged(Editable s) {
280 // Get the current file name.
281 String fileNameString = fileNameEditText.getText().toString();
283 // Enable the save button based on the save type.
284 if (saveType == SAVE_URL) { // A URL is being saved.
285 // Enable the save button if the file name and the URL is populated.
286 saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty());
287 } else { // An archive or an image is being saved.
288 // Enable the save button if the file name is populated.
289 saveButton.setEnabled(!fileNameString.isEmpty());
294 // Create a file name string.
295 String fileName = "";
297 // Set the file name according to the type.
300 // Use the file name from the content disposition.
301 fileName = contentDispositionFileNameString;
305 // Use a file name ending in `.png`.
306 fileName = getString(R.string.webpage_png);
310 // Save the file name as the default file name. This must be final to be used in the lambda below.
311 final String defaultFileName = fileName;
313 // Handle clicks on the browse button.
314 browseButton.setOnClickListener((View view) -> {
315 // Create the file picker intent.
316 Intent browseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
318 // Set the intent MIME type to include all files so that everything is visible.
319 browseIntent.setType("*/*");
321 // Set the initial file name according to the type.
322 browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
324 // Request a file that can be opened.
325 browseIntent.addCategory(Intent.CATEGORY_OPENABLE);
327 // Start the file picker. This must be started under `activity` so that the request code is returned correctly.
328 activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE);
331 // Return the alert dialog.