2 * Copyright © 2017-2019 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser 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 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. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.asynctasks;
22 import android.app.Activity;
23 import android.content.SharedPreferences;
24 import android.graphics.Typeface;
25 import android.os.AsyncTask;
26 import android.os.Build;
27 import android.os.LocaleList;
28 import android.preference.PreferenceManager;
29 import android.text.SpannableStringBuilder;
30 import android.text.Spanned;
31 import android.text.style.StyleSpan;
32 import android.view.View;
33 import android.webkit.CookieManager;
34 import android.widget.ProgressBar;
35 import android.widget.TextView;
37 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
39 import com.stoutner.privacybrowser.R;
41 import java.io.BufferedInputStream;
42 import java.io.ByteArrayOutputStream;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.lang.ref.WeakReference;
46 import java.net.HttpURLConnection;
48 import java.util.Locale;
50 // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `SpannableStringBuilder[]` contains the results.
51 public class GetSource extends AsyncTask<String, Void, SpannableStringBuilder[]> {
52 // Declare a weak reference to the calling activity.
53 private WeakReference<Activity> activityWeakReference;
55 // Store the user agent.
56 private String userAgent;
58 public GetSource(Activity activity, String userAgent) {
59 // Populate the weak reference to the calling activity.
60 activityWeakReference = new WeakReference<>(activity);
62 // Store the user agent.
63 this.userAgent = userAgent;
66 // `onPreExecute()` operates on the UI thread.
68 protected void onPreExecute() {
69 // Get a handle for the activity.
70 Activity viewSourceActivity = activityWeakReference.get();
72 // Abort if the activity is gone.
73 if ((viewSourceActivity == null) || viewSourceActivity.isFinishing()) {
77 // Get a handle for the progress bar.
78 ProgressBar progressBar = viewSourceActivity.findViewById(R.id.progress_bar);
80 // Make the progress bar visible.
81 progressBar.setVisibility(View.VISIBLE);
83 // Set the progress bar to be indeterminate.
84 progressBar.setIndeterminate(true);
88 protected SpannableStringBuilder[] doInBackground(String... formattedUrlString) {
89 // Initialize the response body String.
90 SpannableStringBuilder requestHeadersBuilder = new SpannableStringBuilder();
91 SpannableStringBuilder responseMessageBuilder = new SpannableStringBuilder();
92 SpannableStringBuilder responseHeadersBuilder = new SpannableStringBuilder();
93 SpannableStringBuilder responseBodyBuilder = new SpannableStringBuilder();
95 // Get a handle for the activity.
96 Activity activity = activityWeakReference.get();
98 // Abort if the activity is gone.
99 if ((activity == null) || activity.isFinishing()) {
100 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
103 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
105 // Get the current URL from the main activity.
106 URL url = new URL(formattedUrlString[0]);
108 // Open a connection to the URL. No data is actually sent at this point.
109 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
111 // Define the variables necessary to build the request headers.
112 requestHeadersBuilder = new SpannableStringBuilder();
113 int oldRequestHeadersBuilderLength;
114 int newRequestHeadersBuilderLength;
117 // Set the `Host` header property.
118 httpUrlConnection.setRequestProperty("Host", url.getHost());
120 // Add the `Host` header to the string builder and format the text.
121 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
122 requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
123 } else { // Older versions not so much.
124 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
125 requestHeadersBuilder.append("Host");
126 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
127 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
129 requestHeadersBuilder.append(": ");
130 requestHeadersBuilder.append(url.getHost());
133 // Set the `Connection` header property.
134 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
136 // Add the `Connection` header to the string builder and format the text.
137 requestHeadersBuilder.append(System.getProperty("line.separator"));
138 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
139 requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
140 } else { // Older versions not so much.
141 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
142 requestHeadersBuilder.append("Connection");
143 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
144 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
146 requestHeadersBuilder.append(": keep-alive");
149 // Set the `Upgrade-Insecure-Requests` header property.
150 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
152 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
153 requestHeadersBuilder.append(System.getProperty("line.separator"));
154 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
155 requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
156 } else { // Older versions not so much.
157 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
158 requestHeadersBuilder.append("Upgrade-Insecure_Requests");
159 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
160 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
162 requestHeadersBuilder.append(": 1");
165 // Set the `User-Agent` header property.
166 httpUrlConnection.setRequestProperty("User-Agent", userAgent);
168 // Add the `User-Agent` header to the string builder and format the text.
169 requestHeadersBuilder.append(System.getProperty("line.separator"));
170 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
171 requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
172 } else { // Older versions not so much.
173 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
174 requestHeadersBuilder.append("User-Agent");
175 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
176 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
178 requestHeadersBuilder.append(": ");
179 requestHeadersBuilder.append(userAgent);
182 // Set the `x-requested-with` header property.
183 httpUrlConnection.setRequestProperty("x-requested-with", "");
185 // Add the `x-requested-with` header to the string builder and format the text.
186 requestHeadersBuilder.append(System.getProperty("line.separator"));
187 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
188 requestHeadersBuilder.append("x-requested-with", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
189 } else { // Older versions not so much.
190 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
191 requestHeadersBuilder.append("x-requested-with");
192 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
193 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
195 requestHeadersBuilder.append(": ");
198 // Get a handle for the shared preferences.
199 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
201 // Only populate `Do Not Track` if it is enabled.
202 if (sharedPreferences.getBoolean("do_not_track", false)) {
203 // Set the `dnt` header property.
204 httpUrlConnection.setRequestProperty("dnt", "1");
206 // Add the `dnt` header to the string builder and format the text.
207 requestHeadersBuilder.append(System.getProperty("line.separator"));
208 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
209 requestHeadersBuilder.append("dnt", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
210 } else { // Older versions not so much.
211 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
212 requestHeadersBuilder.append("dnt");
213 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
214 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
216 requestHeadersBuilder.append(": 1");
220 // Set the `Accept` header property.
221 httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
223 // Add the `Accept` header to the string builder and format the text.
224 requestHeadersBuilder.append(System.getProperty("line.separator"));
225 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
226 requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
227 } else { // Older versions not so much.
228 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
229 requestHeadersBuilder.append("Accept");
230 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
231 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
233 requestHeadersBuilder.append(": ");
234 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
237 // Instantiate a locale string.
240 // Populate the locale string.
241 if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
242 // Get the list of locales.
243 LocaleList localeList = activity.getResources().getConfiguration().getLocales();
245 // Initialize a string builder to extract the locales from the list.
246 StringBuilder localesStringBuilder = new StringBuilder();
248 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
251 // Populate the string builder with the contents of the locales list.
252 for (int i = 0; i < localeList.size(); i++) {
253 // Append a comma if there is already an item in the string builder.
255 localesStringBuilder.append(",");
258 // Get the locale from the list.
259 Locale locale = localeList.get(i);
261 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
262 localesStringBuilder.append(locale.getLanguage());
263 localesStringBuilder.append("-");
264 localesStringBuilder.append(locale.getCountry());
266 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
268 localesStringBuilder.append(";q=0.");
269 localesStringBuilder.append(q);
272 // Decrement `q` if it is greater than 1.
277 // Add a second entry for the language only portion of the locale.
278 localesStringBuilder.append(",");
279 localesStringBuilder.append(locale.getLanguage());
281 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
282 localesStringBuilder.append(";q=0.");
283 localesStringBuilder.append(q);
285 // Decrement `q` if it is greater than 1.
291 // Store the populated string builder in the locale string.
292 localeString = localesStringBuilder.toString();
293 } else { // SDK < 24 only has a primary locale.
294 // Store the locale in the locale string.
295 localeString = Locale.getDefault().toString();
298 // Set the `Accept-Language` header property.
299 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
301 // Add the `Accept-Language` header to the string builder and format the text.
302 requestHeadersBuilder.append(System.getProperty("line.separator"));
303 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
304 requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
305 } else { // Older versions not so much.
306 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
307 requestHeadersBuilder.append("Accept-Language");
308 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
309 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
311 requestHeadersBuilder.append(": ");
312 requestHeadersBuilder.append(localeString);
315 // Get the cookies for the current domain.
316 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
318 // Only process the cookies if they are not null.
319 if (cookiesString != null) {
320 // Set the `Cookie` header property.
321 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
323 // Add the `Cookie` header to the string builder and format the text.
324 requestHeadersBuilder.append(System.getProperty("line.separator"));
325 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
326 requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
327 } else { // Older versions not so much.
328 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
329 requestHeadersBuilder.append("Cookie");
330 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
331 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
333 requestHeadersBuilder.append(": ");
334 requestHeadersBuilder.append(cookiesString);
338 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default. If the property is manually set, than `HttpUrlConnection` does not process the decoding.
339 // Add the `Accept-Encoding` header to the string builder and format the text.
340 requestHeadersBuilder.append(System.getProperty("line.separator"));
341 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
342 requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
343 } else { // Older versions not so much.
344 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
345 requestHeadersBuilder.append("Accept-Encoding");
346 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
347 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
349 requestHeadersBuilder.append(": gzip");
352 // 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.
354 // Initialize the string builders.
355 responseMessageBuilder = new SpannableStringBuilder();
356 responseHeadersBuilder = new SpannableStringBuilder();
358 // Get the response code, which causes the connection to the server to be made.
359 int responseCode = httpUrlConnection.getResponseCode();
361 // Populate the response message string builder.
362 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
363 responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
364 } else { // Older versions not so much.
365 responseMessageBuilder.append(String.valueOf(responseCode));
366 int newLength = responseMessageBuilder.length();
367 responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
369 responseMessageBuilder.append(": ");
370 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
372 // Initialize the iteration variable.
375 // Iterate through the received header fields.
376 while (httpUrlConnection.getHeaderField(i) != null) {
377 // Add a new line if there is already information in the string builder.
379 responseHeadersBuilder.append(System.getProperty("line.separator"));
382 // Add the header to the string builder and format the text.
383 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
384 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
385 } else { // Older versions not so much.
386 int oldLength = responseHeadersBuilder.length();
387 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
388 int newLength = responseHeadersBuilder.length();
389 responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength + 1, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
391 responseHeadersBuilder.append(": ");
392 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
394 // Increment the iteration variable.
398 // Instantiate an input stream for the response body.
399 InputStream inputStream;
401 // Get the correct input stream based on the response code.
402 if (responseCode == 404) { // Get the error stream.
403 inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
404 } else { // Get the response body stream.
405 inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
408 // Initialize the byte array output stream and the conversion buffer byte array.
409 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
410 byte[] conversionBufferByteArray = new byte[1024];
412 // Instantiate the variable to track the buffer length.
416 // Attempt to read data from the input stream and store it in the conversion buffer byte array. Also store the amount of data transferred in the buffer length variable.
417 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer is > 0.
418 // Write the contents of the conversion buffer to the byte array output stream.
419 byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
421 } catch (IOException e) {
425 // Close the input stream.
428 // Populate the response body string with the contents of the byte array output stream.
429 responseBodyBuilder.append(byteArrayOutputStream.toString());
431 // Disconnect `httpUrlConnection`.
432 httpUrlConnection.disconnect();
434 } catch (IOException e) {
438 // Return the response body string as the result.
439 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
442 // `onPostExecute()` operates on the UI thread.
444 protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
445 // Get a handle for the activity.
446 Activity activity = activityWeakReference.get();
448 // Abort if the activity is gone.
449 if ((activity == null) || activity.isFinishing()) {
453 // Get handles for the text views.
454 TextView requestHeadersTextView = activity.findViewById(R.id.request_headers);
455 TextView responseMessageTextView = activity.findViewById(R.id.response_message);
456 TextView responseHeadersTextView = activity.findViewById(R.id.response_headers);
457 TextView responseBodyTextView = activity.findViewById(R.id.response_body);
458 ProgressBar progressBar = activity.findViewById(R.id.progress_bar);
459 SwipeRefreshLayout swipeRefreshLayout = activity.findViewById(R.id.view_source_swiperefreshlayout);
461 // Populate the text views. This can take a long time, and freeze the user interface, if the response body is particularly large.
462 requestHeadersTextView.setText(viewSourceStringArray[0]);
463 responseMessageTextView.setText(viewSourceStringArray[1]);
464 responseHeadersTextView.setText(viewSourceStringArray[2]);
465 responseBodyTextView.setText(viewSourceStringArray[3]);
467 // Hide the progress bar.
468 progressBar.setIndeterminate(false);
469 progressBar.setVisibility(View.GONE);
471 //Stop the swipe to refresh indicator if it is running
472 swipeRefreshLayout.setRefreshing(false);