]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/coroutines/PrepareSaveDialogCoroutine.kt
Add DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION explanation. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / coroutines / PrepareSaveDialogCoroutine.kt
1 /*
2  * Copyright 2020-2022 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.content.Context
23 import android.net.Uri
24 import android.webkit.CookieManager
25 import android.webkit.MimeTypeMap
26
27 import androidx.fragment.app.DialogFragment
28 import androidx.fragment.app.FragmentManager
29
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
39
40 import java.lang.Exception
41 import java.net.HttpURLConnection
42 import java.net.URL
43 import java.text.NumberFormat
44
45 object PrepareSaveDialogCoroutine {
46     @JvmStatic
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
55
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)
60
61                     // Get the URL MIME type, which ends with a `;`.
62                     val urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";"))
63
64                     // Get the Base64 data, which begins after a `,`.
65                     val base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1)
66
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)
69
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)
75
76                     // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
77                     try {
78                         // Convert the URL string to a URL.
79                         val url = URL(urlString)
80
81                         // Instantiate the proxy helper.
82                         val proxyHelper = ProxyHelper()
83
84                         // Get the current proxy.
85                         val proxy = proxyHelper.getCurrentProxy(context)
86
87                         // Open a connection to the URL.  No data is actually sent at this point.
88                         val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
89
90                         // Add the user agent to the header property.
91                         httpUrlConnection.setRequestProperty("User-Agent", userAgent)
92
93                         // Add the cookies if they are enabled.
94                         if (cookiesEnabled) {
95                             // Get the cookies for the current domain.
96                             val cookiesString = CookieManager.getInstance().getCookie(url.toString())
97
98                             // Add the cookies if they are not null.
99                             if (cookiesString != null)
100                                 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
101                         }
102
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.
104                         try {
105                             // Get the status code.  This initiates a network connection.
106                             val responseCode = httpUrlConnection.responseCode
107
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)
112
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.
116                                 // Get the headers.
117                                 val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
118                                 val contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition")
119                                 var contentTypeString = httpUrlConnection.contentType
120
121                                 // Remove anything after the MIME type in the content type string.
122                                 if (contentTypeString.contains(";"))
123                                     contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";"))
124
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()
129
130                                     // Format the file size.
131                                     formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
132                                 }
133
134                                 // Get the file name string from the content disposition.
135                                 fileNameString = getFileNameFromHeaders(context, contentDispositionString, contentTypeString, urlString)
136                             }
137                         } finally {
138                             // Disconnect the HTTP URL connection.
139                             httpUrlConnection.disconnect()
140                         }
141                     } catch (exception: Exception) {
142                         // Set the formatted file size to indicate a bad URL.
143                         formattedFileSize = context.getString(R.string.invalid_url)
144
145                         // Set the file name according to the URL.
146                         fileNameString = getFileNameFromUrl(context, urlString, null)
147                     }
148                 }
149
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)
154
155                     // Try to show the dialog.  Sometimes the window is not active.
156                     try {
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)))
162                     }
163                 }
164             }
165         }
166     }
167
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.
170     @JvmStatic
171     fun getFileNameFromHeaders(context: Context, contentDispositionString: String?, contentTypeString: String?, urlString: String): String {
172         // Define a file name string.
173         var fileNameString: String
174
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)
181
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)
185
186                 // Remove any `"` at the beginning of the string.
187                 if (fileNameString.startsWith("\""))
188                     fileNameString = fileNameString.substring(1)
189
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)
196             }
197         } else {  // The content disposition is null.
198             // Get the file name string from the URL.
199             fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
200         }
201
202         // Return the file name string.
203         return fileNameString
204     }
205
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)
209
210         // Get the last path segment.
211         var lastPathSegment = uri.lastPathSegment
212
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)
217
218             // Add a file extension if it can be detected.
219             if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString))
220                 lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString)
221         }
222
223         // Return the last path segment as the file name.
224         return lastPathSegment
225     }
226 }