2 * Copyright 2020-2023 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.helpers
22 import android.content.Context
23 import android.net.Uri
24 import android.text.Spanned
25 import android.text.style.ForegroundColorSpan
26 import android.webkit.CookieManager
27 import android.webkit.MimeTypeMap
28 import android.widget.EditText
30 import com.stoutner.privacybrowser.R
32 import java.net.HttpURLConnection
34 import java.text.NumberFormat
37 // Content dispositions can contain other text besides the file name, and they can be in any order.
38 // Elements are separated by semicolons. Sometimes the file names are contained in quotes.
39 fun getFileName(context: Context, contentDispositionString: String?, contentTypeString: String?, urlString: String): String {
40 // Define a file name string.
41 var fileNameString: String
43 // Only process the content disposition string if it isn't null.
44 if (contentDispositionString != null) { // The content disposition is not null.
45 // Check to see if the content disposition contains a file name.
46 if (contentDispositionString.contains("filename=")) { // The content disposition contains a filename.
47 // Get the part of the content disposition after `filename=`.
48 fileNameString = contentDispositionString.substring(contentDispositionString.indexOf("filename=") + 9)
50 // Remove any `;` and anything after it. This removes any entries after the filename.
51 if (fileNameString.contains(";"))
52 fileNameString = fileNameString.substring(0, fileNameString.indexOf(";") - 1)
54 // Remove any `"` at the beginning of the string.
55 if (fileNameString.startsWith("\""))
56 fileNameString = fileNameString.substring(1)
58 // Remove any `"` at the end of the string.
59 if (fileNameString.endsWith("\""))
60 fileNameString = fileNameString.substring(0, fileNameString.length - 1)
61 } else { // The headers contain no useful information.
62 // Get the file name string from the URL.
63 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
65 } else { // The content disposition is null.
66 // Get the file name string from the URL.
67 fileNameString = getFileNameFromUrl(context, urlString, contentTypeString)
70 // Return the file name string.
74 private fun getFileNameFromUrl(context: Context, urlString: String, contentTypeString: String?): String {
75 // Convert the URL string to a URI.
76 val uri = Uri.parse(urlString)
78 // Get the last path segment.
79 var lastPathSegment = uri.lastPathSegment
81 // Use a default file name if the last path segment is null.
82 if (lastPathSegment == null) {
83 // Set the last path segment to be the generic file name.
84 lastPathSegment = context.getString(R.string.file)
86 // Add a file extension if it can be detected.
87 if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString))
88 lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString)
91 // Return the last path segment as the file name.
92 return lastPathSegment
95 fun getNameAndSize(context: Context, urlString: String, userAgent: String, cookiesEnabled: Boolean): Pair<String, String> {
96 // Define the strings.
97 var fileNameString: String
98 var formattedFileSize: String
100 // Populate the file size and name strings.
101 if (urlString.startsWith("data:")) { // The URL contains the entire data of an image.
102 // Remove `data:` from the beginning of the URL.
103 val urlWithoutData = urlString.substring(5)
105 // Get the URL MIME type, which ends with a `;`.
106 val urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";"))
108 // Get the Base64 data, which begins after a `,`.
109 val base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1)
111 // Calculate the file size of the data URL. Each Base64 character represents 6 bits.
112 formattedFileSize = NumberFormat.getInstance().format(base64DataString.length * 3L / 4) + " " + context.getString(R.string.bytes)
114 // Set the file name according to the MIME type.
115 fileNameString = context.getString(R.string.file) + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(urlMimeType)
116 } else { // The URL refers to the location of the data.
117 // Initialize the formatted file size string.
118 formattedFileSize = context.getString(R.string.unknown_size)
120 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions.
122 // Convert the URL string to a URL.
123 val url = URL(urlString)
125 // Instantiate the proxy helper.
126 val proxyHelper = ProxyHelper()
128 // Get the current proxy.
129 val proxy = proxyHelper.getCurrentProxy(context)
131 // Open a connection to the URL. No data is actually sent at this point.
132 val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
134 // Add the user agent to the header property.
135 httpUrlConnection.setRequestProperty("User-Agent", userAgent)
137 // Add the cookies if they are enabled.
138 if (cookiesEnabled) {
139 // Get the cookies for the current domain.
140 val cookiesString = CookieManager.getInstance().getCookie(url.toString())
142 // Add the cookies if they are not null.
143 if (cookiesString != null)
144 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
147 // 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.
149 // Get the status code. This initiates a network connection.
150 val responseCode = httpUrlConnection.responseCode
152 // Check the response code.
153 if (responseCode >= 400) { // The response code is an error message.
154 // Set the formatted file size to indicate a bad URL.
155 formattedFileSize = context.getString(R.string.invalid_url)
157 // Set the file name according to the URL.
158 fileNameString = getFileNameFromUrl(context, urlString, null)
159 } else { // The response code is not an error message.
161 val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
162 val contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition")
163 var contentTypeString = httpUrlConnection.contentType
165 // Remove anything after the MIME type in the content type string.
166 if (contentTypeString.contains(";"))
167 contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";"))
169 // Only process the content length string if it isn't null.
170 if (contentLengthString != null) {
171 // Convert the content length string to a long.
172 val fileSize = contentLengthString.toLong()
174 // Format the file size.
175 formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes)
178 // Get the file name string from the content disposition.
179 fileNameString = getFileName(context, contentDispositionString, contentTypeString, urlString)
182 // Disconnect the HTTP URL connection.
183 httpUrlConnection.disconnect()
185 } catch (exception: Exception) {
186 // Set the formatted file size to indicate a bad URL.
187 formattedFileSize = context.getString(R.string.invalid_url)
189 // Set the file name according to the URL.
190 fileNameString = getFileNameFromUrl(context, urlString, null)
194 // Return the file name and size.
195 return Pair(fileNameString, formattedFileSize)
198 fun highlightSyntax(urlEditText: EditText, initialGrayColorSpan: ForegroundColorSpan, finalGrayColorSpan: ForegroundColorSpan, redColorSpan: ForegroundColorSpan) {
199 // Get the URL string.
200 val urlString: String = urlEditText.text.toString()
202 // Highlight the URL according to the protocol.
203 if (urlString.startsWith("file://") || urlString.startsWith("content://")) { // This is a file or content URL.
204 // De-emphasize everything before the file name.
205 urlEditText.text.setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
206 } else { // This is a web URL.
207 // Get the index of the `/` immediately after the domain name.
208 val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
211 val baseUrl: String = if (endOfDomainName > 0) // There is at least one character after the base URL.
212 urlString.substring(0, endOfDomainName)
213 else // There are no characters after the base URL.
216 // Get the index of the last `.` in the domain.
217 val lastDotIndex = baseUrl.lastIndexOf(".")
219 // Get the index of the penultimate `.` in the domain.
220 val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
222 // Markup the beginning of the URL.
223 if (urlString.startsWith("http://")) { // The protocol is not encrypted.
224 // Highlight the protocol in red.
225 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
227 // De-emphasize subdomains.
228 if (penultimateDotIndex > 0) // There is more than one subdomain in the domain name.
229 urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
230 } else if (urlString.startsWith("https://")) { // The protocol is encrypted.
231 // De-emphasize the protocol of connections that are encrypted.
232 if (penultimateDotIndex > 0) // There is more than one subdomain in the domain name. De-emphasize the protocol and the additional subdomains.
233 urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
234 else // There is only one subdomain in the domain name. De-emphasize only the protocol.
235 urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
238 // De-emphasize the text after the domain name.
239 if (endOfDomainName > 0)
240 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)