X-Git-Url: https://gitweb.stoutner.com/?a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Fcoroutines%2FSaveUrlCoroutine.kt;fp=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Fcoroutines%2FSaveUrlCoroutine.kt;h=c6fd36a284478ff7e6bdfcd38e0267829572257e;hb=5186b668274b09e37b371c0a134e53255c98ad98;hp=0000000000000000000000000000000000000000;hpb=514e93baaa8389dc9c5abdb79e68c890c260b8d3;p=PrivacyBrowserAndroid.git 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 index 00000000..c6fd36a2 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2020-2023 Soren Stoutner . + * + * This file is part of 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 . + */ + +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(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. + 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