]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt
Migrate five classes to Kotlin. https://redmine.stoutner.com/issues/950
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / coroutines / SaveUrlCoroutine.kt
diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt
new file mode 100644 (file)
index 0000000..c6fd36a
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.coroutines
+
+import android.app.Activity
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.provider.OpenableColumns
+import android.util.Base64
+import android.webkit.CookieManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.helpers.ProxyHelper
+import com.stoutner.privacybrowser.views.NoSwipeViewPager
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+import java.io.BufferedInputStream
+import java.io.InputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.text.NumberFormat
+
+class SaveUrlCoroutine {
+    fun save(context: Context, activity: Activity, urlString: String, fileUri: Uri, userAgent: String, cookiesEnabled: Boolean) {
+        // Use a coroutine to save the URL.
+        CoroutineScope(Dispatchers.Main).launch {
+            // Create a file name string.
+            val fileNameString: String
+
+            // Query the exact file name if the API >= 26.
+            if (Build.VERSION.SDK_INT >= 26) {
+                // Get a cursor from the content resolver.
+                val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)!!
+
+                // Move to the first row.
+                contentResolverCursor.moveToFirst()
+
+                // Get the file name from the cursor.
+                fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
+
+                // Close the cursor.
+                contentResolverCursor.close()
+            } else {
+                // Use the file URI last path segment as the file name string.
+                fileNameString = fileUri.lastPathSegment!!
+            }
+
+            // Get a handle for the no swipe view pager.
+            val noSwipeViewPager = activity.findViewById<NoSwipeViewPager>(R.id.webviewpager)
+
+            // Create a saving file snackbar.
+            val savingFileSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.saving_file, 0, fileNameString), Snackbar.LENGTH_INDEFINITE)
+
+            // Display the saving file snackbar.
+            savingFileSnackbar.show()
+
+            // Download the URL on the IO thread.
+            withContext(Dispatchers.IO) {
+                try {
+                    // Open an output stream.
+                    val outputStream = activity.contentResolver.openOutputStream(fileUri)!!
+
+                    // Save the URL.
+                    if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
+                        // Get the Base64 data, which begins after a `,`.
+                        val base64DataString = urlString.substring(urlString.indexOf(",") + 1)
+
+                        // Decode the Base64 string to a byte array.
+                        val base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT)
+
+                        // Write the Base64 byte array to the output stream.
+                        outputStream.write(base64DecodedDataByteArray)
+                    } else {  // The URL points to the data location on the internet.
+                        // Get the URL from the calling activity.
+                        val url = URL(urlString)
+
+                        // Instantiate the proxy helper.
+                        val proxyHelper = ProxyHelper()
+
+                        // Get the current proxy.
+                        val proxy = proxyHelper.getCurrentProxy(context)
+
+                        // Open a connection to the URL.  No data is actually sent at this point.
+                        val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
+
+                        // Add the user agent to the header property.
+                        httpUrlConnection.setRequestProperty("User-Agent", userAgent)
+
+                        // Add the cookies if they are enabled.
+                        if (cookiesEnabled) {
+                            // Get the cookies for the current domain.
+                            val cookiesString = CookieManager.getInstance().getCookie(url.toString())
+
+                            // Only add the cookies if they are not null.
+                            if (cookiesString != null) {
+                                // Add the cookies to the header property.
+                                httpUrlConnection.setRequestProperty("Cookie", cookiesString)
+                            }
+                        }
+
+                        // Create the file size value.
+                        val fileSize: Long
+
+                        // Create the formatted file size variable.
+                        var formattedFileSize = ""
+
+                        // 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.
+                        try {
+                            // Get the content length header, which causes the connection to the server to be made.
+                            val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
+
+                            // Check to see if the content length is populated.
+                            if (contentLengthString != null) {  // The content length is populated.
+                                // Convert the content length to an long.
+                                fileSize = contentLengthString.toLong()
+
+                                // Format the file size for display.
+                                formattedFileSize = NumberFormat.getInstance().format(fileSize)
+                            } else {  // The content length is null.
+                                // Set the file size to be `-1`.
+                                fileSize = -1
+                            }
+
+                            // Get the response body stream.
+                            val inputStream: InputStream = BufferedInputStream(httpUrlConnection.inputStream)
+
+                            // Initialize the conversion buffer byte array.
+                            // 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>
+                            val conversionBufferByteArray = ByteArray(1048576)
+
+                            // Initialize the downloaded kilobytes counter.
+                            var downloadedKilobytesCounter: Long = 0
+
+                            // Define the buffer length variable.
+                            var bufferLength: Int
+
+                            // 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.
+                            while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) {  // Proceed while the amount of data stored in the buffer in > 0.
+                                // Write the contents of the conversion buffer to the file output stream.
+                                outputStream.write(conversionBufferByteArray, 0, bufferLength)
+
+                                // Update the downloaded kilobytes counter.
+                                downloadedKilobytesCounter += bufferLength
+
+                                // Format the number of bytes downloaded.
+                                val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedKilobytesCounter)
+
+                                // Update the UI.
+                                withContext(Dispatchers.Main) {
+                                    // Check to see if the file size is known.
+                                    if (fileSize == -1L) {  // The size of the download file is not known.
+                                        // Update the snackbar.
+                                        savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString))
+                                    } else {  // The size of the download file is known.
+                                        // Calculate the download percentage.
+                                        val downloadPercentage = downloadedKilobytesCounter * 100 / fileSize
+
+                                        // Update the snackbar.
+                                        savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize,
+                                            fileNameString)
+                                        )
+                                    }
+                                }
+                            }
+
+                            // Close the input stream.
+                            inputStream.close()
+                        } finally {
+                            // Disconnect the HTTP URL connection.
+                            httpUrlConnection.disconnect()
+                        }
+                    }
+
+                    // Close the output stream.
+                    outputStream.close()
+
+                    // Update the UI.
+                    withContext(Dispatchers.Main) {
+                        // Dismiss the saving file snackbar.
+                        savingFileSnackbar.dismiss()
+
+                        // Display the file saved snackbar.
+                        Snackbar.make(noSwipeViewPager, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show()
+                    }
+                } catch (exception: Exception) {
+                    // Update the UI.
+                    withContext(Dispatchers.Main) {
+                        // Dismiss the saving file snackbar.
+                        savingFileSnackbar.dismiss()
+
+                        // Display the file saving error.
+                        Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file