2 * Copyright © 2020-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android 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 Android 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 Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.asynctasks;
22 import android.app.Activity;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.net.Uri;
26 import android.os.AsyncTask;
27 import android.os.Build;
28 import android.provider.OpenableColumns;
29 import android.util.Base64;
30 import android.webkit.CookieManager;
32 import com.google.android.material.snackbar.Snackbar;
33 import com.stoutner.privacybrowser.R;
34 import com.stoutner.privacybrowser.helpers.ProxyHelper;
35 import com.stoutner.privacybrowser.views.NoSwipeViewPager;
37 import java.io.BufferedInputStream;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.lang.ref.WeakReference;
41 import java.net.HttpURLConnection;
42 import java.net.Proxy;
44 import java.text.NumberFormat;
46 public class SaveUrl extends AsyncTask<String, Long, String> {
47 // Declare the weak references.
48 private final WeakReference<Context> contextWeakReference;
49 private final WeakReference<Activity> activityWeakReference;
51 // Define a success string constant.
52 private final String SUCCESS = "Success";
54 // Declare the class variables.
55 private final Uri fileUri;
56 private final String userAgent;
57 private final boolean cookiesEnabled;
58 private Snackbar savingFileSnackbar;
59 private long fileSize;
60 private String formattedFileSize;
61 private final String fileNameString;
63 // The public constructor.
64 public SaveUrl(Context context, Activity activity, Uri fileUri, String userAgent, boolean cookiesEnabled) {
65 // Populate weak references to the calling context and activity.
66 contextWeakReference = new WeakReference<>(context);
67 activityWeakReference = new WeakReference<>(activity);
69 // Store the class variables.
70 this.fileUri = fileUri;
71 this.userAgent = userAgent;
72 this.cookiesEnabled = cookiesEnabled;
74 // Query the exact file name if the API >= 26.
75 if (Build.VERSION.SDK_INT >= 26) {
76 // Get a cursor from the content resolver.
77 Cursor contentResolverCursor = activity.getContentResolver().query(fileUri, null, null, null);
79 // Move to the first row.
80 contentResolverCursor.moveToFirst();
82 // Get the file name from the cursor.
83 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
86 contentResolverCursor.close();
88 // Use the file URI last path segment as the file name string.
89 fileNameString = fileUri.getLastPathSegment();
93 // `onPreExecute()` operates on the UI thread.
95 protected void onPreExecute() {
96 // Get a handle for the activity.
97 Activity activity = activityWeakReference.get();
99 // Abort if the activity is gone.
100 if ((activity==null) || activity.isFinishing()) {
104 // Get a handle for the no swipe view pager.
105 NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
107 // Create a saving file snackbar.
108 savingFileSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.saving_file) + " 0% - " + fileNameString, Snackbar.LENGTH_INDEFINITE);
110 // Display the saving file snackbar.
111 savingFileSnackbar.show();
115 protected String doInBackground(String... urlToSave) {
116 // Get handles for the context and activity.
117 Context context = contextWeakReference.get();
118 Activity activity = activityWeakReference.get();
120 // Abort if the activity is gone.
121 if ((activity == null) || activity.isFinishing()) {
125 // Define a save disposition string.
126 String saveDisposition = SUCCESS;
128 // Get the URL string.
129 String urlString = urlToSave[0];
132 // Open an output stream.
133 OutputStream outputStream = activity.getContentResolver().openOutputStream(fileUri);
136 if (urlString.startsWith("data:")) { // The URL contains the entire data of an image.
137 // Get the Base64 data, which begins after a `,`.
138 String base64DataString = urlString.substring(urlString.indexOf(",") + 1);
140 // Decode the Base64 string to a byte array.
141 byte[] base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT);
143 // Write the Base64 byte array to the output stream.
144 outputStream.write(base64DecodedDataByteArray);
145 } else { // The URL points to the data location on the internet.
146 // Get the URL from the calling activity.
147 URL url = new URL(urlString);
149 // Instantiate the proxy helper.
150 ProxyHelper proxyHelper = new ProxyHelper();
152 // Get the current proxy.
153 Proxy proxy = proxyHelper.getCurrentProxy(context);
155 // Open a connection to the URL. No data is actually sent at this point.
156 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
158 // Add the user agent to the header property.
159 httpUrlConnection.setRequestProperty("User-Agent", userAgent);
161 // Add the cookies if they are enabled.
162 if (cookiesEnabled) {
163 // Get the cookies for the current domain.
164 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
166 // Only add the cookies if they are not null.
167 if (cookiesString != null) {
168 // Add the cookies to the header property.
169 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
173 // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
175 // Get the content length header, which causes the connection to the server to be made.
176 String contentLengthString = httpUrlConnection.getHeaderField("Content-Length");
178 // Make sure the content length isn't null.
179 if (contentLengthString != null) { // The content length isn't null.
180 // Convert the content length to an long.
181 fileSize = Long.parseLong(contentLengthString);
183 // Format the file size for display.
184 formattedFileSize = NumberFormat.getInstance().format(fileSize);
185 } else { // The content length is null.
186 // Set the file size to be `-1`.
190 // Get the response body stream.
191 InputStream inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
193 // Initialize the conversion buffer byte array.
194 // This is set to a megabyte so that frequent updating of the snackbar doesn't freeze the interface on download. <https://redmine.stoutner.com/issues/709>
195 byte[] conversionBufferByteArray = new byte[1048576];
197 // Initialize the downloaded kilobytes counter.
198 long downloadedKilobytesCounter = 0;
200 // Define the buffer length variable.
203 // Attempt to read data from the input stream and store it in the output stream. Also store the amount of data read in the buffer length variable.
204 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer in > 0.
205 // Write the contents of the conversion buffer to the file output stream.
206 outputStream.write(conversionBufferByteArray, 0, bufferLength);
208 // Update the downloaded kilobytes counter.
209 downloadedKilobytesCounter = downloadedKilobytesCounter + bufferLength;
211 // Update the file download progress snackbar.
212 publishProgress(downloadedKilobytesCounter);
215 // Close the input stream.
218 // Disconnect the HTTP URL connection.
219 httpUrlConnection.disconnect();
223 // Close the output stream.
224 outputStream.close();
225 } catch (Exception exception) {
226 // Store the error in the save disposition string.
227 saveDisposition = exception.toString();
230 // Return the save disposition string.
231 return saveDisposition;
234 // `onProgressUpdate()` operates on the UI thread.
236 protected void onProgressUpdate(Long... numberOfBytesDownloaded) {
237 // Get a handle for the activity.
238 Activity activity = activityWeakReference.get();
240 // Abort if the activity is gone.
241 if ((activity == null) || activity.isFinishing()) {
245 // Format the number of bytes downloaded.
246 String formattedNumberOfBytesDownloaded = NumberFormat.getInstance().format(numberOfBytesDownloaded[0]);
248 // Check to see if the file size is known.
249 if (fileSize == -1) { // The size of the download file is not known.
250 // Update the snackbar.
251 savingFileSnackbar.setText(activity.getString(R.string.saving_file) + " " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " - " + fileNameString);
252 } else { // The size of the download file is known.
253 // Calculate the download percentage.
254 long downloadPercentage = (numberOfBytesDownloaded[0] * 100) / fileSize;
256 // Update the snackbar.
257 savingFileSnackbar.setText(activity.getString(R.string.saving_file) + " " + downloadPercentage + "% - " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " / " +
258 formattedFileSize + " " + activity.getString(R.string.bytes) + " - " + fileNameString);
262 // `onPostExecute()` operates on the UI thread.
264 protected void onPostExecute(String saveDisposition) {
265 // Get handles for the context and activity.
266 Activity activity = activityWeakReference.get();
268 // Abort if the activity is gone.
269 if ((activity == null) || activity.isFinishing()) {
273 // Get a handle for the no swipe view pager.
274 NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
276 // Dismiss the saving file snackbar.
277 savingFileSnackbar.dismiss();
279 // Display a save disposition snackbar.
280 if (saveDisposition.equals(SUCCESS)) {
281 // Display the file saved snackbar.
282 Snackbar.make(noSwipeViewPager, activity.getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_LONG).show();
284 // Display the file saving error.
285 Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file) + " " + saveDisposition, Snackbar.LENGTH_INDEFINITE).show();