Redesign file access to work with the scoped storage. https://redmine.stoutner.com...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / LogcatActivity.java
1 /*
2  * Copyright © 2019-2021 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.app.Activity;
23 import android.app.Dialog;
24 import android.content.ClipData;
25 import android.content.ClipboardManager;
26 import android.content.Intent;
27 import android.content.SharedPreferences;
28 import android.content.res.Configuration;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.preference.PreferenceManager;
32 import android.util.TypedValue;
33 import android.view.Menu;
34 import android.view.MenuItem;
35 import android.view.WindowManager;
36 import android.widget.EditText;
37 import android.widget.ScrollView;
38 import android.widget.TextView;
39
40 import androidx.annotation.NonNull;
41 import androidx.appcompat.app.ActionBar;
42 import androidx.appcompat.app.AppCompatActivity;
43 import androidx.appcompat.widget.Toolbar;
44 import androidx.fragment.app.DialogFragment;
45 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
46
47 import com.google.android.material.snackbar.Snackbar;
48
49 import com.stoutner.privacybrowser.R;
50 import com.stoutner.privacybrowser.asynctasks.GetLogcat;
51 import com.stoutner.privacybrowser.dialogs.SaveDialog;
52
53 import java.io.BufferedReader;
54 import java.io.BufferedWriter;
55 import java.io.ByteArrayInputStream;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.io.InputStreamReader;
59 import java.io.OutputStream;
60 import java.io.OutputStreamWriter;
61 import java.nio.charset.StandardCharsets;
62
63 public class LogcatActivity extends AppCompatActivity implements SaveDialog.SaveListener {
64     // Declare the class constants.
65     private final String SCROLLVIEW_POSITION = "scrollview_position";
66
67     // Define the class views.
68     private TextView logcatTextView;
69
70     @Override
71     public void onCreate(Bundle savedInstanceState) {
72         // Get a handle for the shared preferences.
73         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
74
75         // Get the screenshot preference.
76         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
77
78         // Disable screenshots if not allowed.
79         if (!allowScreenshots) {
80             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
81         }
82
83         // Set the theme.
84         setTheme(R.style.PrivacyBrowser);
85
86         // Run the default commands.
87         super.onCreate(savedInstanceState);
88
89         // Set the content view.
90         setContentView(R.layout.logcat_coordinatorlayout);
91
92         // Get handles for the views.
93         Toolbar toolbar = findViewById(R.id.logcat_toolbar);
94         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.logcat_swiperefreshlayout);
95
96         // Set the toolbar as the action bar.
97         setSupportActionBar(toolbar);
98
99         // Get a handle for the action bar.
100         ActionBar actionBar = getSupportActionBar();
101
102         // Remove the incorrect lint warning that the action bar might be null.
103         assert actionBar != null;
104
105         // Display the the back arrow in the action bar.
106         actionBar.setDisplayHomeAsUpEnabled(true);
107
108         // Populate the class views.
109         logcatTextView = findViewById(R.id.logcat_textview);
110
111         // Implement swipe to refresh.
112         swipeRefreshLayout.setOnRefreshListener(() -> {
113             // Get the current logcat.
114             new GetLogcat(this, 0).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_NO) {
122             swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
123         } else {
124             swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
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         // Initialize the scrollview Y position int.
140         int scrollViewYPositionInt = 0;
141
142         // Check to see if the activity has been restarted.
143         if (savedInstanceState != null) {
144             // Get the saved scrollview position.
145             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION);
146         }
147
148         // Get the logcat.
149         new GetLogcat(this, scrollViewYPositionInt).execute();
150     }
151
152     @Override
153     public boolean onCreateOptionsMenu(Menu menu) {
154         // Inflate the menu.  This adds items to the action bar.
155         getMenuInflater().inflate(R.menu.logcat_options_menu, menu);
156
157         // Display the menu.
158         return true;
159     }
160
161     @Override
162     public boolean onOptionsItemSelected(MenuItem menuItem) {
163         // Get the selected menu item ID.
164         int menuItemId = menuItem.getItemId();
165
166         // Run the commands that correlate to the selected menu item.
167         if (menuItemId == R.id.copy) {  // Copy was selected.
168             // Get a handle for the clipboard manager.
169             ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
170
171             // Remove the incorrect lint error below that the clipboard manager might be null.
172             assert clipboardManager != null;
173
174             // Save the logcat in a clip data.
175             ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText());
176
177             // Place the clip data on the clipboard.
178             clipboardManager.setPrimaryClip(logcatClipData);
179
180             // Display a snackbar.
181             Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show();
182
183             // Consume the event.
184             return true;
185         } else if (menuItemId == R.id.save) {  // Save was selected.
186             // Instantiate the save alert dialog.
187             DialogFragment saveDialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT);
188
189             // Show the save alert dialog.
190             saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat));
191
192             // Consume the event.
193             return true;
194         } else if (menuItemId == R.id.clear) {  // Clear was selected.
195             try {
196                 // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
197                 Process process = Runtime.getRuntime().exec("logcat -b all -c");
198
199                 // Wait for the process to finish.
200                 process.waitFor();
201
202                 // Reload the logcat.
203                 new GetLogcat(this, 0).execute();
204             } catch (IOException | InterruptedException exception) {
205                 // Do nothing.
206             }
207
208             // Consume the event.
209             return true;
210         } else {  // The home button was pushed.
211             // Do not consume the event.  The system will process the home command.
212             return super.onOptionsItemSelected(menuItem);
213         }
214     }
215
216     @Override
217     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
218         // Run the default commands.
219         super.onSaveInstanceState(savedInstanceState);
220
221         // Get a handle for the logcat scrollview.
222         ScrollView logcatScrollView = findViewById(R.id.logcat_scrollview);
223
224         // Get the scrollview Y position.
225         int scrollViewYPositionInt = logcatScrollView.getScrollY();
226
227         // Store the scrollview Y position in the bundle.
228         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
229     }
230
231     // The activity result is called after browsing for a file in the save alert dialog.
232     @Override
233     public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
234         // Run the default commands.
235         super.onActivityResult(requestCode, resultCode, returnedIntent);
236
237         // Only do something if the user didn't press back from the file picker.
238         if (resultCode == Activity.RESULT_OK) {
239             // Get a handle for the save dialog fragment.
240             DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat));
241
242             // Only update the file name if the dialog still exists.
243             if (saveDialogFragment != null) {
244                 // Get a handle for the save dialog.
245                 Dialog saveDialog = saveDialogFragment.getDialog();
246
247                 // Remove the lint warning below that the save dialog might be null.
248                 assert saveDialog != null;
249
250                 // Get a handle for the file name edit text.
251                 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
252
253                 // Get the file name URI from the intent.
254                 Uri fileNameUri = returnedIntent.getData();
255
256                 // Get the file name string from the URI.
257                 String fileNameString = fileNameUri.toString();
258
259                 // Set the file name text.
260                 fileNameEditText.setText(fileNameString);
261
262                 // Move the cursor to the end of the file name edit text.
263                 fileNameEditText.setSelection(fileNameString.length());
264             }
265         }
266     }
267
268     @Override
269     public void onSave(int saveType, DialogFragment dialogFragment) {
270         // Get a handle for the dialog.
271         Dialog dialog = dialogFragment.getDialog();
272
273         // Remove the lint warning below that the dialog might be null.
274         assert dialog != null;
275
276         // Get a handle for the file name edit text.
277         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
278
279         // Get the file path string.
280         String fileNameString = fileNameEditText.getText().toString();
281
282         try {
283             // Get the logcat as a string.
284             String logcatString = logcatTextView.getText().toString();
285
286             // Create an input stream with the contents of the logcat.
287             InputStream logcatInputStream = new ByteArrayInputStream(logcatString.getBytes(StandardCharsets.UTF_8));
288
289             // Create a logcat buffered reader.
290             BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
291
292             // Open an output stream.
293             OutputStream outputStream = getContentResolver().openOutputStream(Uri.parse(fileNameString));
294
295             // Create a file buffered writer.
296             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
297
298             // Create a transfer string.
299             String transferString;
300
301             // Use the transfer string to copy the logcat from the buffered reader to the buffered writer.
302             while ((transferString = logcatBufferedReader.readLine()) != null) {
303                 // Append the line to the buffered writer.
304                 bufferedWriter.append(transferString);
305
306                 // Append a line break.
307                 bufferedWriter.append("\n");
308             }
309
310             // Flush the buffered writer.
311             bufferedWriter.flush();
312
313             // Close the inputs and outputs.
314             logcatBufferedReader.close();
315             logcatInputStream.close();
316             bufferedWriter.close();
317             outputStream.close();
318
319             // Display a snackbar with the saved logcat information.
320             Snackbar.make(logcatTextView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
321         } catch (Exception exception) {
322             // Display a snackbar with the error message.
323             Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
324         }
325     }
326 }