]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java
Add share, copy, and save options to About > Version. https://redmine.stoutner.com...
[PrivacyBrowserAndroid.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         switch (menuItemId) {
181             case R.id.copy:
182                 // Get a handle for the clipboard manager.
183                 ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
184
185                 // Remove the incorrect lint error below that the clipboard manager might be null.
186                 assert clipboardManager != null;
187
188                 // Save the logcat in a clip data.
189                 ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText());
190
191                 // Place the clip data on the clipboard.
192                 clipboardManager.setPrimaryClip(logcatClipData);
193
194                 // Display a snackbar.
195                 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show();
196
197                 // Consume the event.
198                 return true;
199
200             case R.id.save:
201                 // Instantiate the save alert dialog.
202                 DialogFragment saveDialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT);
203
204                 // Show the save alert dialog.
205                 saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat));
206
207                 // Consume the event.
208                 return true;
209
210             case R.id.clear:
211                 try {
212                     // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
213                     Process process = Runtime.getRuntime().exec("logcat -b all -c");
214
215                     // Wait for the process to finish.
216                     process.waitFor();
217
218                     // Reload the logcat.
219                     new GetLogcat(this, 0).execute();
220                 } catch (IOException|InterruptedException exception) {
221                     // Do nothing.
222                 }
223
224                 // Consume the event.
225                 return true;
226
227             default:
228                 // Don't consume the event.
229                 return super.onOptionsItemSelected(menuItem);
230         }
231     }
232
233     @Override
234     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
235         // Run the default commands.
236         super.onSaveInstanceState(savedInstanceState);
237
238         // Get a handle for the logcat scrollview.
239         ScrollView logcatScrollView = findViewById(R.id.logcat_scrollview);
240
241         // Get the scrollview Y position.
242         int scrollViewYPositionInt = logcatScrollView.getScrollY();
243
244         // Store the scrollview Y position in the bundle.
245         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
246     }
247
248     @Override
249     public void onSave(int saveType, DialogFragment dialogFragment) {
250         // Get a handle for the dialog.
251         Dialog dialog = dialogFragment.getDialog();
252
253         // Remove the lint warning below that the dialog might be null.
254         assert dialog != null;
255
256         // Get a handle for the file name edit text.
257         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
258
259         // Get the file path string.
260         filePathString = fileNameEditText.getText().toString();
261
262         // Check to see if the storage permission is needed.
263         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
264             // Save the logcat.
265             saveLogcat(filePathString);
266         } else {  // The storage permission has not been granted.
267             // Get the external private directory file.
268             File externalPrivateDirectoryFile = getExternalFilesDir(null);
269
270             // Remove the incorrect lint error below that the file might be null.
271             assert externalPrivateDirectoryFile != null;
272
273             // Get the external private directory string.
274             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
275
276             // Check to see if the file path is in the external private directory.
277             if (filePathString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
278                 // Save the logcat.
279                 saveLogcat(filePathString);
280             } else {  // The file path is in a public directory.
281                 // Check if the user has previously denied the storage permission.
282                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
283                     // Instantiate the storage permission alert dialog.  The type is specified as `0` because it currently isn't used for this activity.
284                     DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
285
286                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
287                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
288                 } else {  // Show the permission request directly.
289                     // Request the write external storage permission.  The logcat will be saved when it finishes.
290                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
291
292                 }
293             }
294         }
295     }
296
297     @Override
298     public void onCloseStoragePermissionDialog(int requestType) {
299         // Request the write external storage permission.  The logcat will be saved when it finishes.
300         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
301     }
302
303     @Override
304     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
305         // Check to see if the storage permission was granted.  If the dialog was canceled the grant result will be empty.
306         if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
307             // Save the logcat.
308             saveLogcat(filePathString);
309         } else {  // The storage permission was not granted.
310             // Display an error snackbar.
311             Snackbar.make(logcatTextView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
312         }
313     }
314
315     // The activity result is called after browsing for a file in the save alert dialog.
316     @Override
317     public void onActivityResult(int requestCode, int resultCode, Intent data) {
318         // Run the default commands.
319         super.onActivityResult(requestCode, resultCode, data);
320
321         // Only do something if the user didn't press back from the file picker.
322         if (resultCode == Activity.RESULT_OK) {
323             // Get a handle for the save dialog fragment.
324             DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat));
325
326             // Only update the file name if the dialog still exists.
327             if (saveDialogFragment != null) {
328                 // Get a handle for the save dialog.
329                 Dialog saveDialog = saveDialogFragment.getDialog();
330
331                 // Remove the lint warning below that the save dialog might be null.
332                 assert saveDialog != null;
333
334                 // Get a handle for the dialog views.
335                 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
336                 TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview);
337
338                 // Get the file name URI from the intent.
339                 Uri fileNameUri = data.getData();
340
341                 // Process the file name URI if it is not null.
342                 if (fileNameUri != null) {
343                     // Instantiate a file name helper.
344                     FileNameHelper fileNameHelper = new FileNameHelper();
345
346                     // Convert the file name URI to a file name path.
347                     String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
348
349                     // Set the file name path as the text of the file name edit text.
350                     fileNameEditText.setText(fileNamePath);
351
352                     // Move the cursor to the end of the file name edit text.
353                     fileNameEditText.setSelection(fileNamePath.length());
354
355                     // Hide the file exists warning.
356                     fileExistsWarningTextView.setVisibility(View.GONE);
357                 }
358             }
359         }
360     }
361
362     private void saveLogcat(String fileNameString) {
363         try {
364             // Get the logcat as a string.
365             String logcatString = logcatTextView.getText().toString();
366
367             // Create an input stream with the contents of the logcat.
368             InputStream logcatInputStream = new ByteArrayInputStream(logcatString.getBytes(StandardCharsets.UTF_8));
369
370             // Create a logcat buffered reader.
371             BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
372
373             // Create a file from the file name string.
374             File saveFile = new File(fileNameString);
375
376             // Delete the file if it already exists.
377             if (saveFile.exists()) {
378                 //noinspection ResultOfMethodCallIgnored
379                 saveFile.delete();
380             }
381
382             // Create a file buffered writer.
383             BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
384
385             // Create a transfer string.
386             String transferString;
387
388             // Use the transfer string to copy the logcat from the buffered reader to the buffered writer.
389             while ((transferString = logcatBufferedReader.readLine()) != null) {
390                 // Append the line to the buffered writer.
391                 fileBufferedWriter.append(transferString);
392
393                 // Append a line break.
394                 fileBufferedWriter.append("\n");
395             }
396
397             // Close the buffered reader and writer.
398             logcatBufferedReader.close();
399             fileBufferedWriter.close();
400
401             // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
402             MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null);
403
404             // Create a logcat saved snackbar.
405             Snackbar logcatSavedSnackbar = Snackbar.make(logcatTextView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT);
406
407             // Add an open action to the snackbar.
408             logcatSavedSnackbar.setAction(R.string.open, (View view) -> {
409                 // Get a file for the file name string.
410                 File file = new File(fileNameString);
411
412                 // Declare a file URI variable.
413                 Uri fileUri;
414
415                 // Get the URI for the file according to the Android version.
416                 if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
417                     fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
418                 } else {  // Get the raw file path URI.
419                     fileUri = Uri.fromFile(file);
420                 }
421
422                 // Get a handle for the content resolver.
423                 ContentResolver contentResolver = getContentResolver();
424
425                 // Create an open intent with `ACTION_VIEW`.
426                 Intent openIntent = new Intent(Intent.ACTION_VIEW);
427
428                 // Set the URI and the MIME type.
429                 openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
430
431                 // Allow the app to read the file URI.
432                 openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
433
434                 // Show the chooser.
435                 startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
436             });
437
438             // Show the logcat saved snackbar.
439             logcatSavedSnackbar.show();
440         } catch (Exception exception) {
441             // Display a snackbar with the error message.
442             Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
443         }
444     }
445 }