]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt
Expand the options for selecting a download provider. https://redmine.stoutner.com...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / coroutines / SaveUrlCoroutine.kt
1 /*
2  * Copyright 2020-2024 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.coroutines
21
22 import android.app.Activity
23 import android.content.Context
24 import android.net.Uri
25 import android.os.Build
26 import android.provider.OpenableColumns
27 import android.util.Base64
28 import android.webkit.CookieManager
29
30 import androidx.viewpager2.widget.ViewPager2
31
32 import com.google.android.material.snackbar.Snackbar
33
34 import com.stoutner.privacybrowser.R
35 import com.stoutner.privacybrowser.helpers.ProxyHelper
36
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.Dispatchers
39 import kotlinx.coroutines.launch
40 import kotlinx.coroutines.withContext
41
42 import java.io.BufferedInputStream
43 import java.io.InputStream
44 import java.net.HttpURLConnection
45 import java.net.URL
46 import java.text.NumberFormat
47 import java.util.Date
48
49 class SaveUrlCoroutine {
50     fun save(context: Context, activity: Activity, urlString: String, fileUri: Uri, userAgent: String, cookiesEnabled: Boolean) {
51         // Create a canceled boolean.
52         var canceled = false
53
54         // Use a coroutine to save the URL.
55         CoroutineScope(Dispatchers.Main).launch {
56             // Create a file name string.
57             val fileNameString: String
58
59             // Query the exact file name if the API >= 26.
60             if (Build.VERSION.SDK_INT >= 26) {
61                 // Get a cursor from the content resolver.
62                 val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)!!
63
64                 // Move to the first row.
65                 contentResolverCursor.moveToFirst()
66
67                 // Get the file name from the cursor.
68                 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
69
70                 // Close the cursor.
71                 contentResolverCursor.close()
72             } else {
73                 // Use the file URI last path segment as the file name string.
74                 fileNameString = fileUri.lastPathSegment!!
75             }
76
77             // Get a handle for the no swipe view pager.
78             val webViewViewPager2 = activity.findViewById<ViewPager2>(R.id.webview_viewpager2)
79
80             // Create a saving file snackbar.
81             val savingFileSnackbar = Snackbar.make(webViewViewPager2, activity.getString(R.string.saving_file, 0, fileNameString), Snackbar.LENGTH_INDEFINITE)
82                                              .setAction(R.string.cancel) { canceled = true }
83
84             // Display the saving file snackbar.
85             savingFileSnackbar.show()
86
87             // Download the URL on the IO thread.
88             withContext(Dispatchers.IO) {
89                 try {
90                     // Open an output stream.
91                     val outputStream = activity.contentResolver.openOutputStream(fileUri)!!
92
93                     // Save the URL.
94                     if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
95                         // Get the Base64 data, which begins after a `,`.
96                         val base64DataString = urlString.substring(urlString.indexOf(",") + 1)
97
98                         // Decode the Base64 string to a byte array.
99                         val base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT)
100
101                         // Write the Base64 byte array to the output stream.
102                         outputStream.write(base64DecodedDataByteArray)
103                     } else {  // The URL points to the data location on the internet.
104                         // Get the URL from the calling activity.
105                         val url = URL(urlString)
106
107                         // Instantiate the proxy helper.
108                         val proxyHelper = ProxyHelper()
109
110                         // Get the current proxy.
111                         val proxy = proxyHelper.getCurrentProxy(context)
112
113                         // Open a connection to the URL.  No data is actually sent at this point.
114                         val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
115
116                         // Add the user agent to the header property.
117                         httpUrlConnection.setRequestProperty("User-Agent", userAgent)
118
119                         // Add the cookies if they are enabled.
120                         if (cookiesEnabled) {
121                             // Get the cookies for the current domain.
122                             val cookiesString = CookieManager.getInstance().getCookie(url.toString())
123
124                             // Only add the cookies if they are not null.
125                             if (cookiesString != null) {
126                                 // Add the cookies to the header property.
127                                 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
128                             }
129                         }
130
131                         // Create the file size value.
132                         val fileSize: Long
133
134                         // Create the formatted file size variable.
135                         var formattedFileSize = ""
136
137                         // 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.
138                         try {
139                             // Get the content length header, which causes the connection to the server to be made.
140                             val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
141
142                             // Check to see if the content length is populated.
143                             if (contentLengthString != null) {  // The content length is populated.
144                                 // Convert the content length to an long.
145                                 fileSize = contentLengthString.toLong()
146
147                                 // Format the file size for display.
148                                 formattedFileSize = NumberFormat.getInstance().format(fileSize)
149                             } else {  // The content length is null.
150                                 // Set the file size to be `-1`.
151                                 fileSize = -1
152                             }
153
154                             // Get the response body stream.
155                             val inputStream: InputStream = BufferedInputStream(httpUrlConnection.inputStream)
156
157                             // Initialize the conversion buffer byte array.
158                             // This is set to a 100,000 bytes so that frequent updating of the snackbar doesn't freeze the interface on download, although `inputStream.read` currently used 8,000 as an upper limit.
159                             // <https://redmine.stoutner.com/issues/709>
160                             val conversionBufferByteArray = ByteArray(100_000)
161
162                             // Initialize the longs.
163                             var downloadedBytesCounterLong: Long = 0
164                             var lastSnackbarUpdateLong: Long = 0
165
166                             // Define the buffer length variable.
167                             var bufferLength: Int
168
169                             // 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.
170                             while ((inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) && !canceled) {  // Proceed while the amount of data stored in the buffer in > 0.
171                                 // Write the contents of the conversion buffer to the file output stream.
172                                 outputStream.write(conversionBufferByteArray, 0, bufferLength)
173
174                                 // Update the downloaded bytes counter.
175                                 downloadedBytesCounterLong += bufferLength
176
177                                 // Format the number of bytes downloaded.
178                                 val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedBytesCounterLong)
179
180                                 // Get the current time.
181                                 val currentTimeLong = Date().time
182
183                                 // Update the snackbar if more than 100 milliseconds have passed since the last update.
184                                 // Updating the snackbar is so resource intensive that it will throttle the download if it is done too frequently.
185                                 if (currentTimeLong - lastSnackbarUpdateLong > 100) {
186                                     // Store the update time.
187                                     lastSnackbarUpdateLong = currentTimeLong
188
189                                     // Update the UI.
190                                     withContext(Dispatchers.Main) {
191                                         // Check to see if the file size is known.
192                                         if (fileSize == -1L) {  // The size of the download file is not known.
193                                             // Update the snackbar.
194                                             savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString))
195                                         } else {  // The size of the download file is known.
196                                             // Calculate the download percentage.
197                                             val downloadPercentage = downloadedBytesCounterLong * 100 / fileSize
198
199                                             // Update the snackbar.
200                                             savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize,
201                                                 fileNameString)
202                                             )
203                                         }
204                                     }
205                                 }
206                             }
207
208                             // Close the input stream.
209                             inputStream.close()
210                         } finally {
211                             // Disconnect the HTTP URL connection.
212                             httpUrlConnection.disconnect()
213                         }
214                     }
215
216                     // Close the output stream.
217                     outputStream.close()
218
219                     // Update the UI.
220                     withContext(Dispatchers.Main) {
221                         // Dismiss the saving file snackbar.
222                         savingFileSnackbar.dismiss()
223
224                         // Display a final disposition snackbar.
225                         if (canceled)
226                             // Display the download cancelled snackbar.
227                             Snackbar.make(webViewViewPager2, activity.getString(R.string.download_cancelled), Snackbar.LENGTH_SHORT).show()
228                         else
229                             // Display the file saved snackbar.
230                             Snackbar.make(webViewViewPager2, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show()
231                     }
232                 } catch (exception: Exception) {
233                     // Update the UI.
234                     withContext(Dispatchers.Main) {
235                         // Dismiss the saving file snackbar.
236                         savingFileSnackbar.dismiss()
237
238                         // Display the file saving error.
239                         Snackbar.make(webViewViewPager2, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
240                     }
241                 }
242             }
243         }
244     }
245 }