2 * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.activities;
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;
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;
56 import com.google.android.material.snackbar.Snackbar;
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;
64 import java.io.BufferedReader;
65 import java.io.BufferedWriter;
66 import java.io.ByteArrayInputStream;
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;
75 public class LogcatActivity extends AppCompatActivity implements SaveDialog.SaveListener, StoragePermissionDialog.StoragePermissionDialogListener {
76 // Declare the class constants.
77 private final String SCROLLVIEW_POSITION = "scrollview_position";
79 // Declare the class variables.
80 private String filePathString;
82 // Define the class views.
83 private TextView logcatTextView;
86 public void onCreate(Bundle savedInstanceState) {
87 // Get a handle for the shared preferences.
88 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
90 // Get the screenshot preference.
91 boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
93 // Disable screenshots if not allowed.
94 if (!allowScreenshots) {
95 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
99 setTheme(R.style.PrivacyBrowser);
101 // Run the default commands.
102 super.onCreate(savedInstanceState);
104 // Set the content view.
105 setContentView(R.layout.logcat_coordinatorlayout);
107 // Set the toolbar as the action bar.
108 Toolbar toolbar = findViewById(R.id.logcat_toolbar);
109 setSupportActionBar(toolbar);
111 // Get a handle for the action bar.
112 ActionBar actionBar = getSupportActionBar();
114 // Remove the incorrect lint warning that the action bar might be null.
115 assert actionBar != null;
117 // Display the the back arrow in the action bar.
118 actionBar.setDisplayHomeAsUpEnabled(true);
120 // Populate the class views.
121 logcatTextView = findViewById(R.id.logcat_textview);
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();
130 // Get the current theme status.
131 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
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);
137 swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
140 // Initialize a color background typed value.
141 TypedValue colorBackgroundTypedValue = new TypedValue();
143 // Get the color background from the theme.
144 getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
146 // Get the color background int from the typed value.
147 int colorBackgroundInt = colorBackgroundTypedValue.data;
149 // Set the swipe refresh background color.
150 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
152 // Initialize the scrollview Y position int.
153 int scrollViewYPositionInt = 0;
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);
162 new GetLogcat(this, scrollViewYPositionInt).execute();
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);
175 public boolean onOptionsItemSelected(MenuItem menuItem) {
176 // Get the selected menu item ID.
177 int menuItemId = menuItem.getItemId();
179 // Run the commands that correlate to the selected menu item.
180 switch (menuItemId) {
182 // Get a handle for the clipboard manager.
183 ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
185 // Remove the incorrect lint error below that the clipboard manager might be null.
186 assert clipboardManager != null;
188 // Save the logcat in a clip data.
189 ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText());
191 // Place the clip data on the clipboard.
192 clipboardManager.setPrimaryClip(logcatClipData);
194 // Display a snackbar.
195 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show();
197 // Consume the event.
201 // Instantiate the save alert dialog.
202 DialogFragment saveDialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT);
204 // Show the save alert dialog.
205 saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat));
207 // Consume the event.
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");
215 // Wait for the process to finish.
218 // Reload the logcat.
219 new GetLogcat(this, 0).execute();
220 } catch (IOException|InterruptedException exception) {
224 // Consume the event.
228 // Don't consume the event.
229 return super.onOptionsItemSelected(menuItem);
234 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
235 // Run the default commands.
236 super.onSaveInstanceState(savedInstanceState);
238 // Get a handle for the logcat scrollview.
239 ScrollView logcatScrollView = findViewById(R.id.logcat_scrollview);
241 // Get the scrollview Y position.
242 int scrollViewYPositionInt = logcatScrollView.getScrollY();
244 // Store the scrollview Y position in the bundle.
245 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
249 public void onSave(int saveType, DialogFragment dialogFragment) {
250 // Get a handle for the dialog.
251 Dialog dialog = dialogFragment.getDialog();
253 // Remove the lint warning below that the dialog might be null.
254 assert dialog != null;
256 // Get a handle for the file name edit text.
257 EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
259 // Get the file path string.
260 filePathString = fileNameEditText.getText().toString();
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.
265 saveLogcat(filePathString);
266 } else { // The storage permission has not been granted.
267 // Get the external private directory file.
268 File externalPrivateDirectoryFile = getExternalFilesDir(null);
270 // Remove the incorrect lint error below that the file might be null.
271 assert externalPrivateDirectoryFile != null;
273 // Get the external private directory string.
274 String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
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.
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);
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);
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);
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.
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();
315 // The activity result is called after browsing for a file in the save alert dialog.
317 public void onActivityResult(int requestCode, int resultCode, Intent data) {
318 // Run the default commands.
319 super.onActivityResult(requestCode, resultCode, data);
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));
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();
331 // Remove the lint warning below that the save dialog might be null.
332 assert saveDialog != null;
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);
338 // Get the file name URI from the intent.
339 Uri fileNameUri = data.getData();
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();
346 // Convert the file name URI to a file name path.
347 String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
349 // Set the file name path as the text of the file name edit text.
350 fileNameEditText.setText(fileNamePath);
352 // Move the cursor to the end of the file name edit text.
353 fileNameEditText.setSelection(fileNamePath.length());
355 // Hide the file exists warning.
356 fileExistsWarningTextView.setVisibility(View.GONE);
362 private void saveLogcat(String fileNameString) {
364 // Get the logcat as a string.
365 String logcatString = logcatTextView.getText().toString();
367 // Create an input stream with the contents of the logcat.
368 InputStream logcatInputStream = new ByteArrayInputStream(logcatString.getBytes(StandardCharsets.UTF_8));
370 // Create a logcat buffered reader.
371 BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
373 // Create a file from the file name string.
374 File saveFile = new File(fileNameString);
376 // Delete the file if it already exists.
377 if (saveFile.exists()) {
378 //noinspection ResultOfMethodCallIgnored
382 // Create a file buffered writer.
383 BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
385 // Create a transfer string.
386 String transferString;
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);
393 // Append a line break.
394 fileBufferedWriter.append("\n");
397 // Close the buffered reader and writer.
398 logcatBufferedReader.close();
399 fileBufferedWriter.close();
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);
404 // Create a logcat saved snackbar.
405 Snackbar logcatSavedSnackbar = Snackbar.make(logcatTextView, getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_SHORT);
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);
412 // Declare a file URI variable.
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);
422 // Get a handle for the content resolver.
423 ContentResolver contentResolver = getContentResolver();
425 // Create an open intent with `ACTION_VIEW`.
426 Intent openIntent = new Intent(Intent.ACTION_VIEW);
428 // Set the URI and the MIME type.
429 openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
431 // Allow the app to read the file URI.
432 openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
435 startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
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();