2 * Copyright 2020-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.coroutines
22 import android.content.Context
23 import android.net.Uri
24 import android.webkit.CookieManager
25 import android.webkit.MimeTypeMap
27 import androidx.fragment.app.DialogFragment
28 import androidx.fragment.app.FragmentManager
30 import com.stoutner.privacybrowser.R
31 import com.stoutner.privacybrowser.activities.MainWebViewActivity
32 import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass
33 import com.stoutner.privacybrowser.dialogs.SaveDialog
34 import com.stoutner.privacybrowser.helpers.ProxyHelper
35 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.Dispatchers
37 import kotlinx.coroutines.launch
38 import kotlinx.coroutines.withContext
40 import java.lang.Exception
41 import java.net.HttpURLConnection
43 import java.text.NumberFormat
45 object PrepareSaveDialogCoroutine {
47 fun prepareSaveDialog(context: Context, supportFragmentManager: FragmentManager, urlString: String, userAgent: String, cookiesEnabled: Boolean) {
48 // Use a coroutine to prepare the save dialog.
49 CoroutineScope(Dispatchers.Main).launch {
50 // Make the network requests on the IO thread.
51 withContext(Dispatchers.IO) {
52 // Define the strings.
53 var formattedFileSize: String
54 var fileNameString: String
56 // Populate the file size and name strings.
57 if (urlString.startsWith("data:")) { // The URL contains the entire data of an image.
58 // Remove `data:` from the beginning of the URL.
59 val urlWithoutData = urlString.substring(5)
61 // Get the URL MIME type, which ends with a `;`.
62 val urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";"))
64 // Get the Base64 data, which begins after a `,`.
65 val base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1)
67 // Calculate the file size of the data URL. Each Base64 character represents 6 bits.
68 formattedFileSize = NumberFormat.getInstance().format(base64DataString.length * 3L / 4) + " " + context.getString(R.string.bytes)
70 // Set the file name according to the MIME type.
71 fileNameString = context.getString(R.string.file) + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(urlMimeType)
72 } else { // The URL refers to the location of the data.
73 // Initialize the formatted file size string.
74 formattedFileSize = context.getString(R.string.unknown_size)
76 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
78 // Convert the URL string to a URL.
79 val url = URL(urlString)
81 // Instantiate the proxy helper.
82 val proxyHelper = ProxyHelper()
84 // Get the current proxy.
85 val proxy = proxyHelper.getCurrentProxy(context)
87 // Open a connection to the URL. No data is actually sent at this point.
88 val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
90 // Add the user agent to the header property.
91 httpUrlConnection.setRequestProperty("User-Agent", userAgent)
93 // Add the cookies if they are enabled.
95 // Get the cookies for the current domain.
96 val cookiesString = CookieManager.getInstance().getCookie(url.toString())
98 // Add the cookies if they are not null.
99 if (cookiesString != null)
100 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
103 // 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.
105 // Get the status code. This initiates a network connection.
106 val responseCode = httpUrlConnection.responseCode
108 // Check the response code.
109 if (responseCode >= 400) { // The response code is an error message.
110 // Set the formatted file size to indicate a bad URL.
111 formattedFileSize = context.getString(R.string.invalid_url)
113 // Set the file name according to the URL.
114 fileNameString = getFileNameFromUrl(context, urlString, null)
115 } else { // The response code is not an error message.
117 val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
118 val contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition")
119 var contentTypeString = httpUrlConnection.contentType
121 // Remove anything after the MIME type in the content type string.
122 if (contentTypeString.contains(";"))
123 contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";"))
125 // Only process the content length string if it isn't null.
126 if (contentLengthString != null) {
127 // Convert the content length string to a long.
128 val fileSize = contentLengthString.toLong()
130 // Format the file size.
131 formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
134 // Get the file name string from the content disposition.
135 fileNameString = getFileNameFromHeaders(context, contentDispositionString, contentTypeString, urlString)
138 // Disconnect the HTTP URL connection.
139 httpUrlConnection.disconnect()
141 } catch (exception: Exception) {
142 // Set the formatted file size to indicate a bad URL.
143 formattedFileSize = context.getString(R.string.invalid_url)
145 // Set the file name according to the URL.
146 fileNameString = getFileNameFromUrl(context, urlString, null)
150 // Display the dialog on the main thread.
151 withContext(Dispatchers.Main) {
152 // Instantiate the save dialog.
153 val saveDialogFragment: DialogFragment = SaveDialog.saveUrl(urlString, formattedFileSize, fileNameString, userAgent, cookiesEnabled)
155 // Try to show the dialog. Sometimes the window is not active.
157 // Show the save dialog.
158 saveDialogFragment.show(supportFragmentManager, context.getString(R.string.save_dialog))
159 } catch (exception: Exception) {
160 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
161 MainWebViewActivity.pendingDialogsArrayList.add(PendingDialogDataClass(saveDialogFragment, context.getString(R.string.save_dialog)))
168 // Content dispositions can contain other text besides the file name, and they can be in any order.
169 // Elements are separated by semicolons. Sometimes the file names are contained in quotes.
171 fun getFileNameFromHeaders(context: Context, contentDispositionString: String?, contentTypeString: String?, urlString: String): String {
172 // Define a file name string.
173 var fileNameString: String
175 // Only process the content disposition string if it isn't null.
176 if (contentDispositionString != null) { // The content disposition is not null.
177 // Check to see if the content disposition contains a file name.
178 if (contentDispositionString.contains("filename=")) { // The content disposition contains a filename.
179 // Get the part of the content disposition after `filename=`.
180 fileNameString = contentDispositionString.substring(contentDispositionString.indexOf("filename=") + 9)
182 // Remove any `;` and anything after it. This removes any entries after the filename.
183 if (fileNameString.contains(";"))
184 fileNameString = fileNameString.substring(0, fileNameString.indexOf(";") - 1)
186 // Remove any `"` at the beginning of the string.
187 if (fileNameString.startsWith("\""))
188 fileNameString = fileNameString.substring(1)
190 // Remove any `"` at the end of the string.
191 if (fileNameString.endsWith("\""))
192 fileNameString = fileNameString.substring(0, fileNameString.length - 1)
193 } else { // The headers contain no useful information.
194 // Get the file name string from the URL.
195 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
197 } else { // The content disposition is null.
198 // Get the file name string from the URL.
199 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
202 // Return the file name string.
203 return fileNameString
206 private fun getFileNameFromUrl(context: Context, urlString: String, contentTypeString: String?): String {
207 // Convert the URL string to a URI.
208 val uri = Uri.parse(urlString)
210 // Get the last path segment.
211 var lastPathSegment = uri.lastPathSegment
213 // Use a default file name if the last path segment is null.
214 if (lastPathSegment == null) {
215 // Set the last path segment to be the generic file name.
216 lastPathSegment = context.getString(R.string.file)
218 // Add a file extension if it can be detected.
219 if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString))
220 lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString)
223 // Return the last path segment as the file name.
224 return lastPathSegment