Combine the light and dark Guide and About pages. https://redmine.stoutner.com/issue...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / LogcatActivity.java
1 /*
2  * Copyright © 2019-2020 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.activities;
21
22 import android.Manifest;
23 import android.app.Activity;
24 import android.app.Dialog;
25 import android.content.ClipData;
26 import android.content.ClipboardManager;
27 import android.content.ContentResolver;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.pm.PackageManager;
31 import android.content.res.Configuration;
32 import android.media.MediaScannerConnection;
33 import android.net.Uri;
34 import android.os.Build;
35 import android.os.Bundle;
36 import android.preference.PreferenceManager;
37 import android.util.TypedValue;
38 import android.view.Menu;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.WindowManager;
42 import android.widget.EditText;
43 import android.widget.ScrollView;
44 import android.widget.TextView;
45
46 import androidx.annotation.NonNull;
47 import androidx.appcompat.app.ActionBar;
48 import androidx.appcompat.app.AppCompatActivity;
49 import androidx.appcompat.widget.Toolbar;
50 import androidx.core.app.ActivityCompat;
51 import androidx.core.content.ContextCompat;
52 import androidx.core.content.FileProvider;
53 import androidx.fragment.app.DialogFragment;
54 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
55
56 import com.google.android.material.snackbar.Snackbar;
57
58 import com.stoutner.privacybrowser.R;
59 import com.stoutner.privacybrowser.asynctasks.GetLogcat;
60 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
61 import com.stoutner.privacybrowser.dialogs.SaveDialog;
62 import com.stoutner.privacybrowser.helpers.FileNameHelper;
63
64 import java.io.BufferedReader;
65 import java.io.BufferedWriter;
66 import java.io.ByteArrayInputStream;
67 import java.io.File;
68 import java.io.FileOutputStream;
69 import java.io.IOException;
70 import java.io.InputStream;
71 import java.io.InputStreamReader;
72 import java.io.OutputStreamWriter;
73 import java.nio.charset.StandardCharsets;
74
75 public class LogcatActivity extends AppCompatActivity implements SaveDialog.SaveListener, StoragePermissionDialog.StoragePermissionDialogListener {
76     // Declare the class constants.
77     private final String SCROLLVIEW_POSITION = "scrollview_position";
78
79     // Declare the class variables.
80     private String filePathString;
81
82     // Define the class views.
83     private TextView logcatTextView;
84
85     @Override
86     public void onCreate(Bundle savedInstanceState) {
87         // Get a handle for the shared preferences.
88         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
89
90         // Get the screenshot preference.
91         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
92
93         // Disable screenshots if not allowed.
94         if (!allowScreenshots) {
95             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
96         }
97
98         // Set the theme.
99         setTheme(R.style.PrivacyBrowser);
100
101         // Run the default commands.
102         super.onCreate(savedInstanceState);
103
104         // Set the content view.
105         setContentView(R.layout.logcat_coordinatorlayout);
106
107         // Set the toolbar as the action bar.
108         Toolbar toolbar = findViewById(R.id.logcat_toolbar);
109         setSupportActionBar(toolbar);
110
111         // Get a handle for the action bar.
112         ActionBar actionBar = getSupportActionBar();
113
114         // Remove the incorrect lint warning that the action bar might be null.
115         assert actionBar != null;
116
117         // Display the the back arrow in the action bar.
118         actionBar.setDisplayHomeAsUpEnabled(true);
119
120         // Populate the class views.
121         logcatTextView = findViewById(R.id.logcat_textview);
122
123         // Implement swipe to refresh.
124         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.logcat_swiperefreshlayout);
125         swipeRefreshLayout.setOnRefreshListener(() -> {
126             // Get the current logcat.
127             new GetLogcat(this, 0).execute();
128         });
129
130         // Get the current theme status.
131         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
132
133         // Set the refresh color scheme according to the theme.
134         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
135             swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
136         } else {
137             swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
138         }
139
140         // Initialize a color background typed value.
141         TypedValue colorBackgroundTypedValue = new TypedValue();
142
143         // Get the color background from the theme.
144         getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
145
146         // Get the color background int from the typed value.
147         int colorBackgroundInt = colorBackgroundTypedValue.data;
148
149         // Set the swipe refresh background color.
150         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
151
152         // Initialize the scrollview Y position int.
153         int scrollViewYPositionInt = 0;
154
155         // Check to see if the activity has been restarted.
156         if (savedInstanceState != null) {
157             // Get the saved scrollview position.
158             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION);
159         }
160
161         // Get the logcat.
162         new GetLogcat(this, scrollViewYPositionInt).execute();
163     }
164
165     @Override
166     public boolean onCreateOptionsMenu(Menu menu) {
167         // Inflate the menu.  This adds items to the action bar.
168         getMenuInflater().inflate(R.menu.logcat_options_menu, menu);
169
170         // Display the menu.
171         return true;
172     }
173
174     @Override
175     public boolean onOptionsItemSelected(MenuItem menuItem) {
176         // Get the selected menu item ID.
177         int menuItemId = menuItem.getItemId();
178
179         // Run the commands that correlate to the selected menu item.
180         if (menuItemId == R.id.copy) {  // Copy was selected.
181             // Get a handle for the clipboard manager.
182             ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
183
184             // Remove the incorrect lint error below that the clipboard manager might be null.
185             assert clipboardManager != null;
186
187             // Save the logcat in a clip data.
188             ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText());
189
190             // Place the clip data on the clipboard.
191             clipboardManager.setPrimaryClip(logcatClipData);
192
193             // Display a snackbar.
194             Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show();
195
196             // Consume the event.
197             return true;
198         } else if (menuItemId == R.id.save) {  // Save was selected.
199             // Instantiate the save alert dialog.
200             DialogFragment saveDialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT);
201
202             // Show the save alert dialog.
203             saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat));
204
205             // Consume the event.
206             return true;
207         } else if (menuItemId == R.id.clear) {  // Clear was selected.
208             try {
209                 // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
210                 Process process = Runtime.getRuntime().exec("logcat -b all -c");
211
212                 // Wait for the process to finish.
213                 process.waitFor();
214
215                 // Reload the logcat.
216                 new GetLogcat(this, 0).execute();
217             } catch (IOException | InterruptedException exception) {
218                 // Do nothing.
219             }
220
221             // Consume the event.
222             return true;
223         } else {  // The home button was pushed.
224             // Do not consume the event.  The system will process the home command.
225             return super.onOptionsItemSelected(menuItem);
226         }
227     }
228
229     @Override
230     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
231         // Run the default commands.
232         super.onSaveInstanceState(savedInstanceState);
233
234         // Get a handle for the logcat scrollview.
235         ScrollView logcatScrollView = findViewById(R.id.logcat_scrollview);
236
237         // Get the scrollview Y position.
238         int scrollViewYPositionInt = logcatScrollView.getScrollY();
239
240         // Store the scrollview Y position in the bundle.
241         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
242     }
243
244     @Override
245     public void onSave(int saveType, DialogFragment dialogFragment) {
246         // Get a handle for the dialog.
247         Dialog dialog = dialogFragment.getDialog();
248
249         // Remove the lint warning below that the dialog might be null.
250         assert dialog != null;
251
252         // Get a handle for the file name edit text.
253         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
254
255         // Get the file path string.
256         filePathString = fileNameEditText.getText().toString();
257
258         // Check to see if the storage permission is needed.
259         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
260             // Save the logcat.
261             saveLogcat(filePathString);
262         } else {  // The storage permission has not been granted.
263             // Get the external private directory file.
264             File externalPrivateDirectoryFile = getExternalFilesDir(null);
265
266             // Remove the incorrect lint error below that the file might be null.
267             assert externalPrivateDirectoryFile != null;
268
269             // Get the external private directory string.
270             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
271
272             // Check to see if the file path is in the external private directory.
273             if (filePathString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
274                 // Save the logcat.
275                 saveLogcat(filePathString);
276             } else {  // The file path is in a public directory.
277                 // Check if the user has previously denied the storage permission.
278                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
279                     // Instantiate the storage permission alert dialog.  The type is specified as `0` because it currently isn't used for this activity.
280                     DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
281
282                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
283                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
284                 } else {  // Show the permission request directly.
285                     // Request the write external storage permission.  The logcat will be saved when it finishes.
286                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
287
288                 }
289             }
290         }
291     }
292
293     @Override
294     public void onCloseStoragePermissionDialog(int requestType) {
295         // Request the write external storage permission.  The logcat will be saved when it finishes.
296         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
297     }
298
299     @Override
300     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
301         // Check to see if the storage permission was granted.  If the dialog was canceled the grant result will be empty.
302         if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
303             // Save the logcat.
304             saveLogcat(filePathString);
305         } else {  // The storage permission was not granted.
306             // Display an error snackbar.
307             Snackbar.make(logcatTextView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
308         }
309     }
310
311     // The activity result is called after browsing for a file in the save alert dialog.
312     @Override
313     public void onActivityResult(int requestCode, int resultCode, Intent data) {
314         // Run the default commands.
315         super.onActivityResult(requestCode, resultCode, data);
316
317         // Only do something if the user didn't press back from the file picker.
318         if (resultCode == Activity.RESULT_OK) {
319             // Get a handle for the save dialog fragment.
320             DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat));
321
322             // Only update the file name if the dialog still exists.
323             if (saveDialogFragment != null) {
324                 // Get a handle for the save dialog.
325                 Dialog saveDialog = saveDialogFragment.getDialog();
326
327                 // Remove the lint warning below that the save dialog might be null.
328                 assert saveDialog != null;
329
330                 // Get a handle for the dialog views.
331                 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
332                 TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview);
333
334                 // Get the file name URI from the intent.
335                 Uri fileNameUri = data.getData();
336
337                 // Process the file name URI if it is not null.
338                 if (fileNameUri != null) {
339                     // Instantiate a file name helper.
340                     FileNameHelper fileNameHelper = new FileNameHelper();
341
342                     // Convert the file name URI to a file name path.
343                     String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
344
345                     // Set the file name path as the text of the file name edit text.
346                     fileNameEditText.setText(fileNamePath);
347
348                     // Move the cursor to the end of the file name edit text.
349                     fileNameEditText.setSelection(fileNamePath.length());
350
351                     // Hide the file exists warning.
352                     fileExistsWarningTextView.setVisibility(View.GONE);
353                 }
354             }
355         }
356     }
357
358     private void saveLogcat(String fileNameString) {
359         try {
360             // Get the logcat as a string.
361             String logcatString = logcatTextView.getText().toString();
362
363             // Create an input stream with the contents of the logcat.
364             InputStream logcatInputStream = new ByteArrayInputStream(logcatString.getBytes(StandardCharsets.UTF_8));
365
366             // Create a logcat buffered reader.
367             BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
368
369             // Create a file from the file name string.
370             File saveFile = new File(fileNameString);
371
372             // Delete the file if it already exists.
373             if (saveFile.exists()) {
374                 //noinspection ResultOfMethodCallIgnored
375                 saveFile.delete();
376             }
377
378             // Create a file buffered writer.
379             BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
380
381             // Create a transfer string.
382             String transferString;
383
384             // Use the transfer string to copy the logcat from the buffered reader to the buffered writer.
385             while ((transferString = logcatBufferedReader.readLine()) != null) {
386                 // Append the line to the buffered writer.
387                 fileBufferedWriter.append(transferString);
388
389                 // Append a line break.
390                 fileBufferedWriter.append("\n");
391             }
392
393             // Close the buffered reader and writer.
394             logcatBufferedReader.close();
395             fileBufferedWriter.close();
396
397             // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
398             MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null);
399
400             // Create a logcat saved snackbar.
401             Snackbar logcatSavedSnackbar = Snackbar.make(logcatTextView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT);
402
403             // Add an open action to the snackbar.
404             logcatSavedSnackbar.setAction(R.string.open, (View view) -> {
405                 // Get a file for the file name string.
406                 File file = new File(fileNameString);
407
408                 // Declare a file URI variable.
409                 Uri fileUri;
410
411                 // Get the URI for the file according to the Android version.
412                 if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
413                     fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
414                 } else {  // Get the raw file path URI.
415                     fileUri = Uri.fromFile(file);
416                 }
417
418                 // Get a handle for the content resolver.
419                 ContentResolver contentResolver = getContentResolver();
420
421                 // Create an open intent with `ACTION_VIEW`.
422                 Intent openIntent = new Intent(Intent.ACTION_VIEW);
423
424                 // Set the URI and the MIME type.
425                 openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
426
427                 // Allow the app to read the file URI.
428                 openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
429
430                 // Show the chooser.
431                 startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
432             });
433
434             // Show the logcat saved snackbar.
435             logcatSavedSnackbar.show();
436         } catch (Exception exception) {
437             // Display a snackbar with the error message.
438             Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
439         }
440     }
441 }