2 * Copyright © 2020-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.asynctasks;
22 import android.app.Activity;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.util.Base64;
27 import android.webkit.CookieManager;
29 import com.google.android.material.snackbar.Snackbar;
30 import com.stoutner.privacybrowser.R;
31 import com.stoutner.privacybrowser.helpers.ProxyHelper;
32 import com.stoutner.privacybrowser.views.NoSwipeViewPager;
34 import java.io.BufferedInputStream;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.lang.ref.WeakReference;
38 import java.net.HttpURLConnection;
39 import java.net.Proxy;
41 import java.text.NumberFormat;
43 public class SaveUrl extends AsyncTask<String, Long, String> {
44 // Define a weak references.
45 private final WeakReference<Context> contextWeakReference;
46 private final WeakReference<Activity> activityWeakReference;
48 // Define a success string constant.
49 private final String SUCCESS = "Success";
51 // Define the class variables.
52 private final String filePathString;
53 private final String userAgent;
54 private final boolean cookiesEnabled;
55 private Snackbar savingFileSnackbar;
56 private long fileSize;
57 private String formattedFileSize;
58 private String urlString = "";
60 // The public constructor.
61 public SaveUrl(Context context, Activity activity, String filePathString, String userAgent, boolean cookiesEnabled) {
62 // Populate weak references to the calling context and activity.
63 contextWeakReference = new WeakReference<>(context);
64 activityWeakReference = new WeakReference<>(activity);
66 // Store the class variables.
67 this.filePathString = filePathString;
68 this.userAgent = userAgent;
69 this.cookiesEnabled = cookiesEnabled;
72 // `onPreExecute()` operates on the UI thread.
74 protected void onPreExecute() {
75 // Get a handle for the activity.
76 Activity activity = activityWeakReference.get();
78 // Abort if the activity is gone.
79 if ((activity==null) || activity.isFinishing()) {
83 // Get a handle for the no swipe view pager.
84 NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
86 // Create a saving file snackbar.
87 savingFileSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.saving_file) + " 0% - " + urlString, Snackbar.LENGTH_INDEFINITE);
89 // Display the saving file snackbar.
90 savingFileSnackbar.show();
94 protected String doInBackground(String... urlToSave) {
95 // Get handles for the context and activity.
96 Context context = contextWeakReference.get();
97 Activity activity = activityWeakReference.get();
99 // Abort if the activity is gone.
100 if ((activity == null) || activity.isFinishing()) {
104 // Define a save disposition string.
105 String saveDisposition = SUCCESS;
107 // Get the URL string.
108 urlString = urlToSave[0];
111 // Open an output stream.
112 OutputStream outputStream = activity.getContentResolver().openOutputStream(Uri.parse(filePathString));
115 if (urlString.startsWith("data:")) { // The URL contains the entire data of an image.
116 // Get the Base64 data, which begins after a `,`.
117 String base64DataString = urlString.substring(urlString.indexOf(",") + 1);
119 // Decode the Base64 string to a byte array.
120 byte[] base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT);
122 // Write the Base64 byte array to the output stream.
123 outputStream.write(base64DecodedDataByteArray);
124 } else { // The URL points to the data location on the internet.
125 // Get the URL from the calling activity.
126 URL url = new URL(urlString);
128 // Instantiate the proxy helper.
129 ProxyHelper proxyHelper = new ProxyHelper();
131 // Get the current proxy.
132 Proxy proxy = proxyHelper.getCurrentProxy(context);
134 // Open a connection to the URL. No data is actually sent at this point.
135 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
137 // Add the user agent to the header property.
138 httpUrlConnection.setRequestProperty("User-Agent", userAgent);
140 // Add the cookies if they are enabled.
141 if (cookiesEnabled) {
142 // Get the cookies for the current domain.
143 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
145 // Only add the cookies if they are not null.
146 if (cookiesString != null) {
147 // Add the cookies to the header property.
148 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
152 // 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.
154 // Get the content length header, which causes the connection to the server to be made.
155 String contentLengthString = httpUrlConnection.getHeaderField("Content-Length");
157 // Make sure the content length isn't null.
158 if (contentLengthString != null) { // The content length isn't null.
159 // Convert the content length to an long.
160 fileSize = Long.parseLong(contentLengthString);
162 // Format the file size for display.
163 formattedFileSize = NumberFormat.getInstance().format(fileSize);
164 } else { // The content length is null.
165 // Set the file size to be `-1`.
169 // Get the response body stream.
170 InputStream inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
172 // Initialize the conversion buffer byte array.
173 byte[] conversionBufferByteArray = new byte[1024];
175 // Initialize the downloaded kilobytes counter.
176 long downloadedKilobytesCounter = 0;
178 // Define the buffer length variable.
181 // 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.
182 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer in > 0.
183 // Write the contents of the conversion buffer to the file output stream.
184 outputStream.write(conversionBufferByteArray, 0, bufferLength);
186 // Update the downloaded kilobytes counter.
187 downloadedKilobytesCounter = downloadedKilobytesCounter + bufferLength;
189 // Update the file download progress snackbar.
190 publishProgress(downloadedKilobytesCounter);
193 // Close the input stream.
196 // Disconnect the HTTP URL connection.
197 httpUrlConnection.disconnect();
201 // Flush the output stream.
202 outputStream.flush();
204 // Close the output stream.
205 outputStream.close();
206 } catch (Exception exception) {
207 // Store the error in the save disposition string.
208 saveDisposition = exception.toString();
211 // Return the save disposition string.
212 return saveDisposition;
215 // `onProgressUpdate()` operates on the UI thread.
217 protected void onProgressUpdate(Long... numberOfBytesDownloaded) {
218 // Get a handle for the activity.
219 Activity activity = activityWeakReference.get();
221 // Abort if the activity is gone.
222 if ((activity == null) || activity.isFinishing()) {
226 // Format the number of bytes downloaded.
227 String formattedNumberOfBytesDownloaded = NumberFormat.getInstance().format(numberOfBytesDownloaded[0]);
229 // Check to see if the file size is known.
230 if (fileSize == -1) { // The size of the download file is not known.
231 // Update the snackbar.
232 savingFileSnackbar.setText(activity.getString(R.string.saving_file) + " " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " - " + urlString);
233 } else { // The size of the download file is known.
234 // Calculate the download percentage.
235 long downloadPercentage = (numberOfBytesDownloaded[0] * 100) / fileSize;
237 // Update the snackbar.
238 savingFileSnackbar.setText(activity.getString(R.string.saving_file) + " " + downloadPercentage + "% - " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " / " + formattedFileSize + " " +
239 activity.getString(R.string.bytes) + " - " + urlString);
243 // `onPostExecute()` operates on the UI thread.
245 protected void onPostExecute(String saveDisposition) {
246 // Get handles for the context and activity.
247 Activity activity = activityWeakReference.get();
249 // Abort if the activity is gone.
250 if ((activity == null) || activity.isFinishing()) {
254 // Get a handle for the no swipe view pager.
255 NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
257 // Dismiss the saving file snackbar.
258 savingFileSnackbar.dismiss();
260 // Display a save disposition snackbar.
261 if (saveDisposition.equals(SUCCESS)) {
262 // Display the file saved snackbar.
263 Snackbar.make(noSwipeViewPager, activity.getString(R.string.file_saved) + " " + urlString, Snackbar.LENGTH_LONG).show();
265 // Display the file saving error.
266 Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file) + " " + saveDisposition, Snackbar.LENGTH_INDEFINITE).show();