3d63545983e7c9b4b679ba61599e880810ce90af
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / asynctasks / SaveUrl.java
1 /*
2  * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.asynctasks;
21
22 import android.app.Activity;
23 import android.content.ActivityNotFoundException;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Build;
29 import android.view.View;
30 import android.webkit.CookieManager;
31
32 import androidx.core.content.FileProvider;
33
34 import com.google.android.material.snackbar.Snackbar;
35 import com.stoutner.privacybrowser.R;
36 import com.stoutner.privacybrowser.helpers.ProxyHelper;
37 import com.stoutner.privacybrowser.views.NoSwipeViewPager;
38
39 import java.io.BufferedInputStream;
40 import java.io.File;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.OutputStream;
45 import java.lang.ref.WeakReference;
46 import java.net.HttpURLConnection;
47 import java.net.Proxy;
48 import java.net.URL;
49 import java.text.NumberFormat;
50
51 public class SaveUrl extends AsyncTask<String, Long, String> {
52     // Define a weak references for the calling context and activity.
53     private WeakReference<Context> contextWeakReference;
54     private WeakReference<Activity> activityWeakReference;
55
56     // Define a success string constant.
57     private final String SUCCESS = "Success";
58
59     // Define the class variables.
60     private String filePathString;
61     private String userAgent;
62     private boolean cookiesEnabled;
63     private Snackbar savingFileSnackbar;
64
65     // The public constructor.
66     public SaveUrl(Context context, Activity activity, String filePathString, String userAgent, boolean cookiesEnabled) {
67         // Populate weak references to the calling context and activity.
68         contextWeakReference = new WeakReference<>(context);
69         activityWeakReference = new WeakReference<>(activity);
70
71         // Store the class variables.
72         this.filePathString = filePathString;
73         this.userAgent = userAgent;
74         this.cookiesEnabled = cookiesEnabled;
75     }
76
77     // `onPreExecute()` operates on the UI thread.
78     @Override
79     protected void onPreExecute() {
80         // Get a handle for the activity.
81         Activity activity = activityWeakReference.get();
82
83         // Abort if the activity is gone.
84         if ((activity==null) || activity.isFinishing()) {
85             return;
86         }
87
88         // Get a handle for the no swipe view pager.
89         NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
90
91         // Create a saving file snackbar.
92         savingFileSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.saving_file) + "  0% - " + filePathString, Snackbar.LENGTH_INDEFINITE);
93
94         // Display the saving file snackbar.
95         savingFileSnackbar.show();
96     }
97
98     @Override
99     protected String doInBackground(String... urlToSave) {
100         // Get handles for the context and activity.
101         Context context = contextWeakReference.get();
102         Activity activity = activityWeakReference.get();
103
104         // Abort if the activity is gone.
105         if ((activity == null) || activity.isFinishing()) {
106             return null;
107         }
108
109         // Define a save disposition string.
110         String saveDisposition = SUCCESS;
111
112         // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
113         try {
114             // Get the URL from the calling activity.
115             URL url = new URL(urlToSave[0]);
116
117             // Instantiate the proxy helper.
118             ProxyHelper proxyHelper = new ProxyHelper();
119
120             // Get the current proxy.
121             Proxy proxy = proxyHelper.getCurrentProxy(context);
122
123             // Open a connection to the URL.  No data is actually sent at this point.
124             HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
125
126             // Add the user agent to the header property.
127             httpUrlConnection.setRequestProperty("User-Agent", userAgent);
128
129             // Add the cookies if they are enabled.
130             if (cookiesEnabled) {
131                 // Get the cookies for the current domain.
132                 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
133
134                 // Only add the cookies if they are not null.
135                 if (cookiesString != null) {
136                     // Add the cookies to the header property.
137                     httpUrlConnection.setRequestProperty("Cookie", cookiesString);
138                 }
139             }
140
141             // 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.
142             try {
143                 // Get the content length header, which causes the connection to the server to be made.
144                 String contentLengthString = httpUrlConnection.getHeaderField("Content-Length");
145
146                 // Define the file size long.
147                 long fileSize;
148
149                 // Make sure the content length isn't null.
150                 if (contentLengthString != null) {  // The content length isn't null.
151                     // Convert the content length to an long.
152                     fileSize = Long.parseLong(contentLengthString);
153                 } else {  // The content length is null.
154                     // Set the file size to be `-1`.
155                     fileSize = -1;
156                 }
157
158                 // Get the response body stream.
159                 InputStream inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
160
161                 // Get the file.
162                 File file = new File(filePathString);
163
164                 // Delete the file if it exists.
165                 if (file.exists()) {
166                     //noinspection ResultOfMethodCallIgnored
167                     file.delete();
168                 }
169
170                 // Create a new file.
171                 //noinspection ResultOfMethodCallIgnored
172                 file.createNewFile();
173
174                 // Create an output file stream.
175                 OutputStream outputStream = new FileOutputStream(file);
176
177                 // Initialize the conversion buffer byte array.
178                 byte[] conversionBufferByteArray = new byte[1024];
179
180                 // Initialize the downloaded kilobytes counter.
181                 long downloadedKilobytesCounter = 0;
182
183                 // Define the buffer length variable.
184                 int bufferLength;
185
186                 // 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.
187                 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) {  // Proceed while the amount of data stored in the buffer in > 0.
188                     // Write the contents of the conversion buffer to the output stream.
189                     outputStream.write(conversionBufferByteArray, 0, bufferLength);
190
191                     // Increment the downloaded kilobytes counter.
192                     downloadedKilobytesCounter++;
193
194                     // Update the file download progress snackbar.
195                     if (fileSize == -1) {  // The file size is unknown.
196                         // Convert the downloaded kilobytes counter to a negative number
197                         long downloadedKilobytes = 0 - downloadedKilobytesCounter;
198
199                         publishProgress(downloadedKilobytes);
200                     } else {  // The file size is known.
201                         // Calculate the download percentage.
202                         long downloadPercentage = (downloadedKilobytesCounter * 1024 * 100) / fileSize;
203
204                         // Update the download percentage.
205                         publishProgress(downloadPercentage);
206                     }
207                 }
208
209                 // Close the input stream.
210                 inputStream.close();
211
212                 // Close the output stream.
213                 outputStream.close();
214
215                 // Define a media scanner intent, which adds items like pictures to Android's recent file list.
216                 Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
217
218                 // Add the URI to the media scanner intent.
219                 mediaScannerIntent.setData(Uri.fromFile(file));
220
221                 // Make it so.
222                 activity.sendBroadcast(mediaScannerIntent);
223             } finally {
224                 // Disconnect the HTTP URL connection.
225                 httpUrlConnection.disconnect();
226             }
227         } catch (IOException exception) {
228             // Store the error in the save disposition string.
229             saveDisposition = exception.toString();
230         }
231
232         // Return the save disposition string.
233         return saveDisposition;
234     }
235
236     // `onProgressUpdate()` operates on the UI thread.
237     @Override
238     protected void onProgressUpdate(Long... downloadPercentage) {
239         // Get a handle for the activity.
240         Activity activity = activityWeakReference.get();
241
242         // Abort if the activity is gone.
243         if ((activity == null) || activity.isFinishing()) {
244             return;
245         }
246
247         // Check to see if a download percentage has been calculated.
248         if (downloadPercentage[0] < 0) {  // There is no download percentage.  The negative number represents the raw downloaded kilobytes.
249             // Calculate the number of bytes downloaded.
250             long numberOfBytesDownloaded = (0 - downloadPercentage[0]) * 1024;
251
252             // Format the number of bytes downloaded.
253             String formattedNumberOfBytesDownloaded = NumberFormat.getInstance().format(numberOfBytesDownloaded);
254
255             // Update the snackbar.
256             savingFileSnackbar.setText(activity.getString(R.string.saving_file) + "  " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " - " + filePathString);
257         } else {  // There is a download percentage.
258             // Update the snackbar.
259             savingFileSnackbar.setText(activity.getString(R.string.saving_file) + "  " + downloadPercentage[0] + "% - " + filePathString);
260         }
261     }
262
263     // `onPostExecute()` operates on the UI thread.
264     @Override
265     protected void onPostExecute(String saveDisposition) {
266         // Get handles for the context and activity.
267         Context context = contextWeakReference.get();
268         Activity activity = activityWeakReference.get();
269
270         // Abort if the activity is gone.
271         if ((activity == null) || activity.isFinishing()) {
272             return;
273         }
274
275         // Get a handle for the no swipe view pager.
276         NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
277
278         // Dismiss the saving file snackbar.
279         savingFileSnackbar.dismiss();
280
281         // Display a save disposition snackbar.
282         if (saveDisposition.equals(SUCCESS)) {
283             // Create a file saved snackbar.
284             Snackbar fileSavedSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.file_saved) + "  " + filePathString, Snackbar.LENGTH_LONG);
285
286             // Add an open action if the file is not an APK on API >= 26 (that scenario requires the REQUEST_INSTALL_PACKAGES permission).
287             if (!(Build.VERSION.SDK_INT >= 26 && filePathString.endsWith(".apk"))) {
288                 fileSavedSnackbar.setAction(R.string.open, (View v) -> {
289                     // Get a file for the file path string.
290                     File file = new File(filePathString);
291
292                     // Create an open intent with `ACTION_VIEW`.
293                     Intent openIntent = new Intent(Intent.ACTION_VIEW);
294
295                     // Set the URI but not the MIME type.  This should open all available apps.
296                     openIntent.setData(FileProvider.getUriForFile(context, activity.getString(R.string.file_provider), file));
297
298                     // Allow the app to read the file URI.
299                     openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
300
301                     // Try the intent.
302                     try {
303                         // Show the chooser.
304                         activity.startActivity(openIntent);
305                     } catch (ActivityNotFoundException exception) {  // There are no apps available to open the URL.
306                         // Show a snackbar with the error.
307                         Snackbar.make(noSwipeViewPager, activity.getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
308                     }
309                 });
310             }
311
312             // Show the file saved snackbar.
313             fileSavedSnackbar.show();
314         } else {
315             // Display the file saving error.
316             Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file) + "  " + saveDisposition, Snackbar.LENGTH_INDEFINITE).show();
317         }
318     }
319 }