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