]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java
Save and restore the app state. https://redmine.stoutner.com/issues/461
[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.Bundle;
34 import android.preference.PreferenceManager;
35 import android.util.TypedValue;
36 import android.view.Menu;
37 import android.view.MenuItem;
38 import android.view.View;
39 import android.view.WindowManager;
40 import android.widget.EditText;
41 import android.widget.ScrollView;
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.asynctasks.GetLogcat;
57 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
58 import com.stoutner.privacybrowser.dialogs.SaveLogcatDialog;
59 import com.stoutner.privacybrowser.helpers.FileNameHelper;
60
61 import java.io.BufferedReader;
62 import java.io.BufferedWriter;
63 import java.io.ByteArrayInputStream;
64 import java.io.File;
65 import java.io.FileOutputStream;
66 import java.io.IOException;
67 import java.io.InputStream;
68 import java.io.InputStreamReader;
69 import java.io.OutputStreamWriter;
70 import java.nio.charset.StandardCharsets;
71
72 public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialog.SaveLogcatListener, StoragePermissionDialog.StoragePermissionDialogListener {
73     // Initialize the saved instance state constants.
74     private final String SCROLLVIEW_POSITION = "scrollview_position";
75
76     // Define the class variables.
77     private String filePathString;
78
79     // Define the class views.
80     private TextView logcatTextView;
81
82     @Override
83     public void onCreate(Bundle savedInstanceState) {
84         // Get a handle for the shared preferences.
85         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
86
87         // Get the screenshot preference.
88         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
89
90         // Disable screenshots if not allowed.
91         if (!allowScreenshots) {
92             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
93         }
94
95         // Set the theme.
96         setTheme(R.style.PrivacyBrowser);
97
98         // Run the default commands.
99         super.onCreate(savedInstanceState);
100
101         // Set the content view.
102         setContentView(R.layout.logcat_coordinatorlayout);
103
104         // Set the toolbar as the action bar.
105         Toolbar toolbar = findViewById(R.id.logcat_toolbar);
106         setSupportActionBar(toolbar);
107
108         // Get a handle for the action bar.
109         ActionBar actionBar = getSupportActionBar();
110
111         // Remove the incorrect lint warning that the action bar might be null.
112         assert actionBar != null;
113
114         // Display the the back arrow in the action bar.
115         actionBar.setDisplayHomeAsUpEnabled(true);
116
117         // Populate the class views.
118         logcatTextView = findViewById(R.id.logcat_textview);
119
120         // Implement swipe to refresh.
121         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.logcat_swiperefreshlayout);
122         swipeRefreshLayout.setOnRefreshListener(() -> {
123             // Get the current logcat.
124             new GetLogcat(this, 0).execute();
125         });
126
127         // Get the current theme status.
128         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
129
130         // Set the refresh color scheme according to the theme.
131         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
132             swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
133         } else {
134             swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
135         }
136
137         // Initialize a color background typed value.
138         TypedValue colorBackgroundTypedValue = new TypedValue();
139
140         // Get the color background from the theme.
141         getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
142
143         // Get the color background int from the typed value.
144         int colorBackgroundInt = colorBackgroundTypedValue.data;
145
146         // Set the swipe refresh background color.
147         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
148
149         // Initialize the scrollview Y position int.
150         int scrollViewYPositionInt = 0;
151
152         // Check to see if the activity has been restarted.
153         if (savedInstanceState != null) {
154             // Get the saved scrollview position.
155             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION);
156         }
157
158         // Get the logcat.
159         new GetLogcat(this, scrollViewYPositionInt).execute();
160     }
161
162     @Override
163     public boolean onCreateOptionsMenu(Menu menu) {
164         // Inflate the menu.  This adds items to the action bar.
165         getMenuInflater().inflate(R.menu.logcat_options_menu, menu);
166
167         // Display the menu.
168         return true;
169     }
170
171     @Override
172     public boolean onOptionsItemSelected(MenuItem menuItem) {
173         // Get the selected menu item ID.
174         int menuItemId = menuItem.getItemId();
175
176         // Run the commands that correlate to the selected menu item.
177         switch (menuItemId) {
178             case R.id.copy:
179                 // Get a handle for the clipboard manager.
180                 ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
181
182                 // Save the logcat in a ClipData.
183                 ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText());
184
185                 // Remove the incorrect lint error that `clipboardManager.setPrimaryClip()` might produce a null pointer exception.
186                 assert clipboardManager != null;
187
188                 // Place the ClipData on the clipboard.
189                 clipboardManager.setPrimaryClip(logcatClipData);
190
191                 // Display a snackbar.
192                 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show();
193
194                 // Consume the event.
195                 return true;
196
197             case R.id.save:
198                 // Instantiate the save alert dialog.
199                 DialogFragment saveDialogFragment = new SaveLogcatDialog();
200
201                 // Show the save alert dialog.
202                 saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat));
203
204                 // Consume the event.
205                 return true;
206
207             case R.id.clear:
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
224             default:
225                 // Don't consume the event.
226                 return super.onOptionsItemSelected(menuItem);
227         }
228     }
229
230     @Override
231     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
232         // Run the default commands.
233         super.onSaveInstanceState(savedInstanceState);
234
235         // Get a handle for the logcat scrollview.
236         ScrollView logcatScrollView = findViewById(R.id.logcat_scrollview);
237
238         // Get the scrollview Y position.
239         int scrollViewYPositionInt = logcatScrollView.getScrollY();
240
241         // Store the scrollview Y position in the bundle.
242         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
243     }
244
245     @Override
246     public void onSaveLogcat(DialogFragment dialogFragment) {
247         // Get a handle for the dialog fragment.
248         Dialog dialog = dialogFragment.getDialog();
249
250         // Remove the lint warning below that the dialog fragment might be null.
251         assert dialog != null;
252
253         // Get a handle for the file name edit text.
254         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
255
256         // Get the file path string.
257         filePathString = fileNameEditText.getText().toString();
258
259         // Check to see if the storage permission is needed.
260         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
261             // Save the logcat.
262             saveLogcat(filePathString);
263         } else {  // The storage permission has not been granted.
264             // Get the external private directory `File`.
265             File externalPrivateDirectoryFile = getExternalFilesDir(null);
266
267             // Remove the incorrect lint error below that the file might be null.
268             assert externalPrivateDirectoryFile != null;
269
270             // Get the external private directory string.
271             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
272
273             // Check to see if the file path is in the external private directory.
274             if (filePathString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
275                 // Save the logcat.
276                 saveLogcat(filePathString);
277             } else {  // The file path in in a public directory.
278                 // Check if the user has previously denied the storage permission.
279                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
280                     // Instantiate the storage permission alert dialog.
281                     DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
282
283                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
284                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
285                 } else {  // Show the permission request directly.
286                     // Request the write external storage permission.  The logcat will be saved when it finishes.
287                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
288
289                 }
290             }
291         }
292     }
293
294     @Override
295     public void onCloseStoragePermissionDialog(int type) {
296         // Request the write external storage permission.  The logcat will be saved when it finishes.
297         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
298     }
299
300     @Override
301     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
302         // Check to see if the storage permission was granted.  If the dialog was canceled the grant result will be empty.
303         if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
304             // Save the logcat.
305             saveLogcat(filePathString);
306         } else {  // The storage permission was not granted.
307             // Display an error snackbar.
308             Snackbar.make(logcatTextView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
309         }
310     }
311
312     private void saveLogcat(String fileNameString) {
313         try {
314             // Get the logcat as a string.
315             String logcatString = logcatTextView.getText().toString();
316
317             // Create an input stream with the contents of the logcat.
318             InputStream logcatInputStream = new ByteArrayInputStream(logcatString.getBytes(StandardCharsets.UTF_8));
319
320             // Create a logcat buffered reader.
321             BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
322
323             // Create a file from the file name string.
324             File saveFile = new File(fileNameString);
325
326             // Create a file buffered writer.
327             BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
328
329             // Create a transfer string.
330             String transferString;
331
332             // Use the transfer string to copy the logcat from the buffered reader to the buffered writer.
333             while ((transferString = logcatBufferedReader.readLine()) != null) {
334                 // Append the line to the buffered writer.
335                 fileBufferedWriter.append(transferString);
336
337                 // Append a line break.
338                 fileBufferedWriter.append("\n");
339             }
340
341             // Close the buffered reader and writer.
342             logcatBufferedReader.close();
343             fileBufferedWriter.close();
344
345             // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
346             MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null);
347
348             // Display a snackbar.
349             Snackbar.make(logcatTextView, getString(R.string.file_saved_successfully), Snackbar.LENGTH_SHORT).show();
350         } catch (Exception exception) {
351             // Display a snackbar with the error message.
352             Snackbar.make(logcatTextView, getString(R.string.save_failed) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
353         }
354     }
355
356     // The activity result is called after browsing for a file in the save alert dialog.
357     @Override
358     public void onActivityResult(int requestCode, int resultCode, Intent data) {
359         // Run the default commands.
360         super.onActivityResult(requestCode, resultCode, data);
361
362         // Don't do anything if the user pressed back from the file picker.
363         if (resultCode == Activity.RESULT_OK) {
364             // Get a handle for the save dialog fragment.
365             DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat));
366
367             // Only update the file name if the dialog still exists.
368             if (saveDialogFragment != null) {
369                 // Get a handle for the save dialog.
370                 Dialog saveDialog = saveDialogFragment.getDialog();
371
372                 // Remove the lint warning below that the save dialog might be null.
373                 assert saveDialog != null;
374
375                 // Get a handle for the dialog views.
376                 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
377                 TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview);
378
379                 // Instantiate the file name helper.
380                 FileNameHelper fileNameHelper = new FileNameHelper();
381
382                 // Get the file name URI from the intent.
383                 Uri fileNameUri= data.getData();
384
385                 // Process the file name URI if it is not null.
386                 if (fileNameUri != null) {
387                     // Convert the file name URI to a file name path.
388                     String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
389
390                     // Set the file name path as the text of the file name edit text.
391                     fileNameEditText.setText(fileNamePath);
392
393                     // Move the cursor to the end of the file name edit text.
394                     fileNameEditText.setSelection(fileNamePath.length());
395
396                     // Hide the file exists warning.
397                     fileExistsWarningTextView.setVisibility(View.GONE);
398                 }
399             }
400         }
401     }
402 }