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