]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt
Add a cancel action to the save URL snackbar. https://redmine.stoutner.com/issues/782
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / coroutines / SaveUrlCoroutine.kt
1 /*
2  * Copyright 2020-2023 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
48 class SaveUrlCoroutine {
49     fun save(context: Context, activity: Activity, urlString: String, fileUri: Uri, userAgent: String, cookiesEnabled: Boolean) {
50         // Create a canceled boolean.
51         var canceled = false
52
53         // Use a coroutine to save the URL.
54         CoroutineScope(Dispatchers.Main).launch {
55             // Create a file name string.
56             val fileNameString: String
57
58             // Query the exact file name if the API >= 26.
59             if (Build.VERSION.SDK_INT >= 26) {
60                 // Get a cursor from the content resolver.
61                 val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)!!
62
63                 // Move to the first row.
64                 contentResolverCursor.moveToFirst()
65
66                 // Get the file name from the cursor.
67                 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
68
69                 // Close the cursor.
70                 contentResolverCursor.close()
71             } else {
72                 // Use the file URI last path segment as the file name string.
73                 fileNameString = fileUri.lastPathSegment!!
74             }
75
76             // Get a handle for the no swipe view pager.
77             val webViewViewPager2 = activity.findViewById<ViewPager2>(R.id.webview_viewpager2)
78
79             // Create a saving file snackbar.
80             val savingFileSnackbar = Snackbar.make(webViewViewPager2, activity.getString(R.string.saving_file, 0, fileNameString), Snackbar.LENGTH_INDEFINITE)
81                                              .setAction(R.string.cancel) { canceled = true }
82
83             // Display the saving file snackbar.
84             savingFileSnackbar.show()
85
86             // Download the URL on the IO thread.
87             withContext(Dispatchers.IO) {
88                 try {
89                     // Open an output stream.
90                     val outputStream = activity.contentResolver.openOutputStream(fileUri)!!
91
92                     // Save the URL.
93                     if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
94                         // Get the Base64 data, which begins after a `,`.
95                         val base64DataString = urlString.substring(urlString.indexOf(",") + 1)
96
97                         // Decode the Base64 string to a byte array.
98                         val base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT)
99
100                         // Write the Base64 byte array to the output stream.
101                         outputStream.write(base64DecodedDataByteArray)
102                     } else {  // The URL points to the data location on the internet.
103                         // Get the URL from the calling activity.
104                         val url = URL(urlString)
105
106                         // Instantiate the proxy helper.
107                         val proxyHelper = ProxyHelper()
108
109                         // Get the current proxy.
110                         val proxy = proxyHelper.getCurrentProxy(context)
111
112                         // Open a connection to the URL.  No data is actually sent at this point.
113                         val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
114
115                         // Add the user agent to the header property.
116                         httpUrlConnection.setRequestProperty("User-Agent", userAgent)
117
118                         // Add the cookies if they are enabled.
119                         if (cookiesEnabled) {
120                             // Get the cookies for the current domain.
121                             val cookiesString = CookieManager.getInstance().getCookie(url.toString())
122
123                             // Only add the cookies if they are not null.
124                             if (cookiesString != null) {
125                                 // Add the cookies to the header property.
126                                 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
127                             }
128                         }
129
130                         // Create the file size value.
131                         val fileSize: Long
132
133                         // Create the formatted file size variable.
134                         var formattedFileSize = ""
135
136                         // 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.
137                         try {
138                             // Get the content length header, which causes the connection to the server to be made.
139                             val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
140
141                             // Check to see if the content length is populated.
142                             if (contentLengthString != null) {  // The content length is populated.
143                                 // Convert the content length to an long.
144                                 fileSize = contentLengthString.toLong()
145
146                                 // Format the file size for display.
147                                 formattedFileSize = NumberFormat.getInstance().format(fileSize)
148                             } else {  // The content length is null.
149                                 // Set the file size to be `-1`.
150                                 fileSize = -1
151                             }
152
153                             // Get the response body stream.
154                             val inputStream: InputStream = BufferedInputStream(httpUrlConnection.inputStream)
155
156                             // Initialize the conversion buffer byte array.
157                             // 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>
158                             val conversionBufferByteArray = ByteArray(1048576)
159
160                             // Initialize the downloaded kilobytes counter.
161                             var downloadedKilobytesCounter: Long = 0
162
163                             // Define the buffer length variable.
164                             var bufferLength: Int
165
166                             // 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.
167                             while ((inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) && !canceled) {  // Proceed while the amount of data stored in the buffer in > 0.
168                                 // Write the contents of the conversion buffer to the file output stream.
169                                 outputStream.write(conversionBufferByteArray, 0, bufferLength)
170
171                                 // Update the downloaded kilobytes counter.
172                                 downloadedKilobytesCounter += bufferLength
173
174                                 // Format the number of bytes downloaded.
175                                 val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedKilobytesCounter)
176
177                                 // Update the UI.
178                                 withContext(Dispatchers.Main) {
179                                     // Check to see if the file size is known.
180                                     if (fileSize == -1L) {  // The size of the download file is not known.
181                                         // Update the snackbar.
182                                         savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString))
183                                     } else {  // The size of the download file is known.
184                                         // Calculate the download percentage.
185                                         val downloadPercentage = downloadedKilobytesCounter * 100 / fileSize
186
187                                         // Update the snackbar.
188                                         savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize,
189                                             fileNameString)
190                                         )
191                                     }
192                                 }
193                             }
194
195                             // Close the input stream.
196                             inputStream.close()
197                         } finally {
198                             // Disconnect the HTTP URL connection.
199                             httpUrlConnection.disconnect()
200                         }
201                     }
202
203                     // Close the output stream.
204                     outputStream.close()
205
206                     // Update the UI.
207                     withContext(Dispatchers.Main) {
208                         // Dismiss the saving file snackbar.
209                         savingFileSnackbar.dismiss()
210
211                         // Display a final disposition snackbar.
212                         if (canceled)
213                             // Display the download cancelled snackbar.
214                             Snackbar.make(webViewViewPager2, activity.getString(R.string.download_cancelled), Snackbar.LENGTH_SHORT).show()
215                         else
216                             // Display the file saved snackbar.
217                             Snackbar.make(webViewViewPager2, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show()
218                     }
219                 } catch (exception: Exception) {
220                     // Update the UI.
221                     withContext(Dispatchers.Main) {
222                         // Dismiss the saving file snackbar.
223                         savingFileSnackbar.dismiss()
224
225                         // Display the file saving error.
226                         Snackbar.make(webViewViewPager2, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
227                     }
228                 }
229             }
230         }
231     }
232 }