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