Track URL loading by tab.
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / asynctasks / GetSource.java
1 /*
2  * Copyright © 2017-2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.asynctasks;
21
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;
36
37 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
38
39 import com.stoutner.privacybrowser.R;
40
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;
47 import java.net.URL;
48 import java.util.Locale;
49
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;
54
55     // Store the user agent.
56     private String userAgent;
57
58     public GetSource(Activity activity, String userAgent) {
59         // Populate the weak reference to the calling activity.
60         activityWeakReference = new WeakReference<>(activity);
61
62         // Store the user agent.
63         this.userAgent = userAgent;
64     }
65
66     // `onPreExecute()` operates on the UI thread.
67     @Override
68     protected void onPreExecute() {
69         // Get a handle for the activity.
70         Activity viewSourceActivity = activityWeakReference.get();
71
72         // Abort if the activity is gone.
73         if ((viewSourceActivity == null) || viewSourceActivity.isFinishing()) {
74             return;
75         }
76
77         // Get a handle for the progress bar.
78         ProgressBar progressBar = viewSourceActivity.findViewById(R.id.progress_bar);
79
80         // Make the progress bar visible.
81         progressBar.setVisibility(View.VISIBLE);
82
83         // Set the progress bar to be indeterminate.
84         progressBar.setIndeterminate(true);
85     }
86
87     @Override
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();
94
95         // Get a handle for the activity.
96         Activity activity = activityWeakReference.get();
97
98         // Abort if the activity is gone.
99         if ((activity == null) || activity.isFinishing()) {
100             return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
101         }
102
103         // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
104         try {
105             // Get the current URL from the main activity.
106             URL url = new URL(formattedUrlString[0]);
107
108             // Open a connection to the URL.  No data is actually sent at this point.
109             HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
110
111             // Instantiate the variables necessary to build the request headers.
112             requestHeadersBuilder = new SpannableStringBuilder();
113             int oldRequestHeadersBuilderLength;
114             int newRequestHeadersBuilderLength;
115
116
117             // Set the `Host` header property.
118             httpUrlConnection.setRequestProperty("Host", url.getHost());
119
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);
128             }
129             requestHeadersBuilder.append(":  ");
130             requestHeadersBuilder.append(url.getHost());
131
132
133             // Set the `Connection` header property.
134             httpUrlConnection.setRequestProperty("Connection", "keep-alive");
135
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);
145             }
146             requestHeadersBuilder.append(":  keep-alive");
147
148
149             // Set the `User-Agent` header property.
150             httpUrlConnection.setRequestProperty("User-Agent", userAgent);
151
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);
161             }
162             requestHeadersBuilder.append(":  ");
163             requestHeadersBuilder.append(userAgent);
164
165
166             // Set the `Upgrade-Insecure-Requests` header property.
167             httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
168
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);
178             }
179             requestHeadersBuilder.append(":  1");
180
181
182             // Set the `x-requested-with` header property.
183             httpUrlConnection.setRequestProperty("x-requested-with", "");
184
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);
194             }
195             requestHeadersBuilder.append(":  ");
196
197
198             // Get a handle for the shared preferences.
199             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
200
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");
205
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);
215                 }
216                 requestHeadersBuilder.append(":  1");
217             }
218
219
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");
222
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);
232             }
233             requestHeadersBuilder.append(":  ");
234             requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
235
236
237             // Instantiate a locale string.
238             String localeString;
239
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();
244
245                 // Initialize a string builder to extract the locales from the list.
246                 StringBuilder localesStringBuilder = new StringBuilder();
247
248                 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
249                 int q = 10;
250
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.
254                     if (i > 0) {
255                         localesStringBuilder.append(",");
256                     }
257
258                     // Get the indicated locale from the list.
259                     localesStringBuilder.append(localeList.get(i));
260
261                     // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
262                     if (q < 10) {
263                         localesStringBuilder.append(";q=0.");
264                         localesStringBuilder.append(q);
265                     }
266
267                     // Decrement `q`.
268                     q--;
269                 }
270
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();
276             }
277
278             // Set the `Accept-Language` header property.
279             httpUrlConnection.setRequestProperty("Accept-Language", localeString);
280
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);
290             }
291             requestHeadersBuilder.append(":  ");
292             requestHeadersBuilder.append(localeString);
293
294
295             // Get the cookies for the current domain.
296             String cookiesString = CookieManager.getInstance().getCookie(url.toString());
297
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);
302
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);
312                 }
313                 requestHeadersBuilder.append(":  ");
314                 requestHeadersBuilder.append(cookiesString);
315             }
316
317
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);
328             }
329             requestHeadersBuilder.append(":  gzip");
330
331
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.
333             try {
334                 // Initialize the string builders.
335                 responseMessageBuilder = new SpannableStringBuilder();
336                 responseHeadersBuilder = new SpannableStringBuilder();
337
338                 // Get the response code, which causes the connection to the server to be made.
339                 int responseCode = httpUrlConnection.getResponseCode();
340
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);
348                 }
349                 responseMessageBuilder.append(":  ");
350                 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
351
352                 // Initialize the iteration variable.
353                 int i = 0;
354
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.
358                     if (i > 0) {
359                         responseHeadersBuilder.append(System.getProperty("line.separator"));
360                     }
361
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);
370                     }
371                     responseHeadersBuilder.append(":  ");
372                     responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
373
374                     // Increment the iteration variable.
375                     i++;
376                 }
377
378                 // Instantiate an input stream for the response body.
379                 InputStream inputStream;
380
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());
386                 }
387
388                 // Initialize the byte array output stream and the conversion buffer byte array.
389                 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
390                 byte[] conversionBufferByteArray = new byte[1024];
391
392                 // Instantiate the variable to track the buffer length.
393                 int bufferLength;
394
395                 try {
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);
400                     }
401                 } catch (IOException e) {
402                     e.printStackTrace();
403                 }
404
405                 // Close the input stream.
406                 inputStream.close();
407
408                 // Populate the response body string with the contents of the byte array output stream.
409                 responseBodyBuilder.append(byteArrayOutputStream.toString());
410             } finally {
411                 // Disconnect `httpUrlConnection`.
412                 httpUrlConnection.disconnect();
413             }
414         } catch (IOException e) {
415             e.printStackTrace();
416         }
417
418         // Return the response body string as the result.
419         return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
420     }
421
422     // `onPostExecute()` operates on the UI thread.
423     @Override
424     protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
425         // Get a handle for the activity.
426         Activity activity = activityWeakReference.get();
427
428         // Abort if the activity is gone.
429         if ((activity == null) || activity.isFinishing()) {
430             return;
431         }
432
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);
440
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]);
446
447         // Hide the progress bar.
448         progressBar.setIndeterminate(false);
449         progressBar.setVisibility(View.GONE);
450
451         //Stop the swipe to refresh indicator if it is running
452         swipeRefreshLayout.setRefreshing(false);
453     }
454 }