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 // Instantiate 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 `User-Agent` header property.
150 httpUrlConnection.setRequestProperty("User-Agent", userAgent);
152 // Add the `User-Agent` 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("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
156 } else { // Older versions not so much.
157 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
158 requestHeadersBuilder.append("User-Agent");
159 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
160 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
162 requestHeadersBuilder.append(": ");
163 requestHeadersBuilder.append(userAgent);
166 // Set the `Upgrade-Insecure-Requests` header property.
167 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
169 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
170 requestHeadersBuilder.append(System.getProperty("line.separator"));
171 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
172 requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
173 } else { // Older versions not so much.
174 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
175 requestHeadersBuilder.append("Upgrade-Insecure_Requests");
176 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
177 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
179 requestHeadersBuilder.append(": 1");
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");
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");
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 indicated locale from the list.
259 localesStringBuilder.append(localeList.get(i));
261 // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
263 localesStringBuilder.append(";q=0.");
264 localesStringBuilder.append(q);
271 // Store the populated string builder in the locale string.
272 localeString = localesStringBuilder.toString();
273 } else { // SDK < 24 only has a primary locale.
274 // Store the locale in the locale string.
275 localeString = Locale.getDefault().toString();
278 // Set the `Accept-Language` header property.
279 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
281 // Add the `Accept-Language` header to the string builder and format the text.
282 requestHeadersBuilder.append(System.getProperty("line.separator"));
283 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
284 requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
285 } else { // Older versions not so much.
286 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
287 requestHeadersBuilder.append("Accept-Language");
288 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
289 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
291 requestHeadersBuilder.append(": ");
292 requestHeadersBuilder.append(localeString);
295 // Get the cookies for the current domain.
296 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
298 // Only process the cookies if they are not null.
299 if (cookiesString != null) {
300 // Set the `Cookie` header property.
301 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
303 // Add the `Cookie` header to the string builder and format the text.
304 requestHeadersBuilder.append(System.getProperty("line.separator"));
305 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
306 requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
307 } else { // Older versions not so much.
308 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
309 requestHeadersBuilder.append("Cookie");
310 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
311 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
313 requestHeadersBuilder.append(": ");
314 requestHeadersBuilder.append(cookiesString);
318 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default. If the property is manually set, than `HttpUrlConnection` does not process the decoding.
319 // Add the `Accept-Encoding` header to the string builder and format the text.
320 requestHeadersBuilder.append(System.getProperty("line.separator"));
321 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
322 requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
323 } else { // Older versions not so much.
324 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
325 requestHeadersBuilder.append("Accept-Encoding");
326 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
327 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
329 requestHeadersBuilder.append(": gzip");
332 // 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.
334 // Initialize the string builders.
335 responseMessageBuilder = new SpannableStringBuilder();
336 responseHeadersBuilder = new SpannableStringBuilder();
338 // Get the response code, which causes the connection to the server to be made.
339 int responseCode = httpUrlConnection.getResponseCode();
341 // Populate the response message string builder.
342 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
343 responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
344 } else { // Older versions not so much.
345 responseMessageBuilder.append(String.valueOf(responseCode));
346 int newLength = responseMessageBuilder.length();
347 responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
349 responseMessageBuilder.append(": ");
350 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
352 // Initialize the iteration variable.
355 // Iterate through the received header fields.
356 while (httpUrlConnection.getHeaderField(i) != null) {
357 // Add a new line if there is already information in the string builder.
359 responseHeadersBuilder.append(System.getProperty("line.separator"));
362 // Add the header to the string builder and format the text.
363 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
364 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
365 } else { // Older versions not so much.
366 int oldLength = responseHeadersBuilder.length();
367 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
368 int newLength = responseHeadersBuilder.length();
369 responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength + 1, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
371 responseHeadersBuilder.append(": ");
372 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
374 // Increment the iteration variable.
378 // Instantiate an input stream for the response body.
379 InputStream inputStream;
381 // Get the correct input stream based on the response code.
382 if (responseCode == 404) { // Get the error stream.
383 inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
384 } else { // Get the response body stream.
385 inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
388 // Initialize the byte array output stream and the conversion buffer byte array.
389 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
390 byte[] conversionBufferByteArray = new byte[1024];
392 // Instantiate the variable to track the buffer length.
396 // 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.
397 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer is > 0.
398 // Write the contents of the conversion buffer to the byte array output stream.
399 byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
401 } catch (IOException e) {
405 // Close the input stream.
408 // Populate the response body string with the contents of the byte array output stream.
409 responseBodyBuilder.append(byteArrayOutputStream.toString());
411 // Disconnect `httpUrlConnection`.
412 httpUrlConnection.disconnect();
414 } catch (IOException e) {
418 // Return the response body string as the result.
419 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
422 // `onPostExecute()` operates on the UI thread.
424 protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
425 // Get a handle for the activity.
426 Activity activity = activityWeakReference.get();
428 // Abort if the activity is gone.
429 if ((activity == null) || activity.isFinishing()) {
433 // Get handles for the text views.
434 TextView requestHeadersTextView = activity.findViewById(R.id.request_headers);
435 TextView responseMessageTextView = activity.findViewById(R.id.response_message);
436 TextView responseHeadersTextView = activity.findViewById(R.id.response_headers);
437 TextView responseBodyTextView = activity.findViewById(R.id.response_body);
438 ProgressBar progressBar = activity.findViewById(R.id.progress_bar);
439 SwipeRefreshLayout swipeRefreshLayout = activity.findViewById(R.id.view_source_swiperefreshlayout);
441 // Populate the text views. This can take a long time, and freeze the user interface, if the response body is particularly large.
442 requestHeadersTextView.setText(viewSourceStringArray[0]);
443 responseMessageTextView.setText(viewSourceStringArray[1]);
444 responseHeadersTextView.setText(viewSourceStringArray[2]);
445 responseBodyTextView.setText(viewSourceStringArray[3]);
447 // Hide the progress bar.
448 progressBar.setIndeterminate(false);
449 progressBar.setVisibility(View.GONE);
451 //Stop the swipe to refresh indicator if it is running
452 swipeRefreshLayout.setRefreshing(false);