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 if (menuItemId == R.id.copy) { // Copy was selected.
181 // Get a handle for the clipboard manager.
182 ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
184 // Remove the incorrect lint error below that the clipboard manager might be null.
185 assert clipboardManager != null;
187 // Save the logcat in a clip data.
188 ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText());
190 // Place the clip data on the clipboard.
191 clipboardManager.setPrimaryClip(logcatClipData);
193 // Display a snackbar.
194 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show();
196 // Consume the event.
198 } else if (menuItemId == R.id.save) { // Save was selected.
199 // Instantiate the save alert dialog.
200 DialogFragment saveDialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT);
202 // Show the save alert dialog.
203 saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat));
205 // Consume the event.
207 } else if (menuItemId == R.id.clear) { // Clear was selected.
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");
212 // Wait for the process to finish.
215 // Reload the logcat.
216 new GetLogcat(this, 0).execute();
217 } catch (IOException | InterruptedException exception) {
221 // Consume the event.
223 } else { // The home button was pushed.
224 // Do not consume the event. The system will process the home command.
225 return super.onOptionsItemSelected(menuItem);
230 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
231 // Run the default commands.
232 super.onSaveInstanceState(savedInstanceState);
234 // Get a handle for the logcat scrollview.
235 ScrollView logcatScrollView = findViewById(R.id.logcat_scrollview);
237 // Get the scrollview Y position.
238 int scrollViewYPositionInt = logcatScrollView.getScrollY();
240 // Store the scrollview Y position in the bundle.
241 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
245 public void onSave(int saveType, DialogFragment dialogFragment) {
246 // Get a handle for the dialog.
247 Dialog dialog = dialogFragment.getDialog();
249 // Remove the lint warning below that the dialog might be null.
250 assert dialog != null;
252 // Get a handle for the file name edit text.
253 EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
255 // Get the file path string.
256 filePathString = fileNameEditText.getText().toString();
258 // Check to see if the storage permission is needed.
259 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
261 saveLogcat(filePathString);
262 } else { // The storage permission has not been granted.
263 // Get the external private directory file.
264 File externalPrivateDirectoryFile = getExternalFilesDir(null);
266 // Remove the incorrect lint error below that the file might be null.
267 assert externalPrivateDirectoryFile != null;
269 // Get the external private directory string.
270 String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
272 // Check to see if the file path is in the external private directory.
273 if (filePathString.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
275 saveLogcat(filePathString);
276 } else { // The file path is in a public directory.
277 // Check if the user has previously denied the storage permission.
278 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
279 // Instantiate the storage permission alert dialog. The type is specified as `0` because it currently isn't used for this activity.
280 DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
282 // Show the storage permission alert dialog. The permission will be requested when the dialog is closed.
283 storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
284 } else { // Show the permission request directly.
285 // Request the write external storage permission. The logcat will be saved when it finishes.
286 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
294 public void onCloseStoragePermissionDialog(int requestType) {
295 // Request the write external storage permission. The logcat will be saved when it finishes.
296 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
300 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
301 // Check to see if the storage permission was granted. If the dialog was canceled the grant result will be empty.
302 if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
304 saveLogcat(filePathString);
305 } else { // The storage permission was not granted.
306 // Display an error snackbar.
307 Snackbar.make(logcatTextView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
311 // The activity result is called after browsing for a file in the save alert dialog.
313 public void onActivityResult(int requestCode, int resultCode, Intent data) {
314 // Run the default commands.
315 super.onActivityResult(requestCode, resultCode, data);
317 // Only do something if the user didn't press back from the file picker.
318 if (resultCode == Activity.RESULT_OK) {
319 // Get a handle for the save dialog fragment.
320 DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat));
322 // Only update the file name if the dialog still exists.
323 if (saveDialogFragment != null) {
324 // Get a handle for the save dialog.
325 Dialog saveDialog = saveDialogFragment.getDialog();
327 // Remove the lint warning below that the save dialog might be null.
328 assert saveDialog != null;
330 // Get a handle for the dialog views.
331 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
332 TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview);
334 // Get the file name URI from the intent.
335 Uri fileNameUri = data.getData();
337 // Process the file name URI if it is not null.
338 if (fileNameUri != null) {
339 // Instantiate a file name helper.
340 FileNameHelper fileNameHelper = new FileNameHelper();
342 // Convert the file name URI to a file name path.
343 String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
345 // Set the file name path as the text of the file name edit text.
346 fileNameEditText.setText(fileNamePath);
348 // Move the cursor to the end of the file name edit text.
349 fileNameEditText.setSelection(fileNamePath.length());
351 // Hide the file exists warning.
352 fileExistsWarningTextView.setVisibility(View.GONE);
358 private void saveLogcat(String fileNameString) {
360 // Get the logcat as a string.
361 String logcatString = logcatTextView.getText().toString();
363 // Create an input stream with the contents of the logcat.
364 InputStream logcatInputStream = new ByteArrayInputStream(logcatString.getBytes(StandardCharsets.UTF_8));
366 // Create a logcat buffered reader.
367 BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
369 // Create a file from the file name string.
370 File saveFile = new File(fileNameString);
372 // Delete the file if it already exists.
373 if (saveFile.exists()) {
374 //noinspection ResultOfMethodCallIgnored
378 // Create a file buffered writer.
379 BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
381 // Create a transfer string.
382 String transferString;
384 // Use the transfer string to copy the logcat from the buffered reader to the buffered writer.
385 while ((transferString = logcatBufferedReader.readLine()) != null) {
386 // Append the line to the buffered writer.
387 fileBufferedWriter.append(transferString);
389 // Append a line break.
390 fileBufferedWriter.append("\n");
393 // Close the buffered reader and writer.
394 logcatBufferedReader.close();
395 fileBufferedWriter.close();
397 // Add the file to the list of recent files. This doesn't currently work, but maybe it will someday.
398 MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null);
400 // Create a logcat saved snackbar.
401 Snackbar logcatSavedSnackbar = Snackbar.make(logcatTextView, getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_SHORT);
403 // Add an open action to the snackbar.
404 logcatSavedSnackbar.setAction(R.string.open, (View view) -> {
405 // Get a file for the file name string.
406 File file = new File(fileNameString);
408 // Declare a file URI variable.
411 // Get the URI for the file according to the Android version.
412 if (Build.VERSION.SDK_INT >= 24) { // Use a file provider.
413 fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
414 } else { // Get the raw file path URI.
415 fileUri = Uri.fromFile(file);
418 // Get a handle for the content resolver.
419 ContentResolver contentResolver = getContentResolver();
421 // Create an open intent with `ACTION_VIEW`.
422 Intent openIntent = new Intent(Intent.ACTION_VIEW);
424 // Set the URI and the MIME type.
425 openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
427 // Allow the app to read the file URI.
428 openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
431 startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
434 // Show the logcat saved snackbar.
435 logcatSavedSnackbar.show();
436 } catch (Exception exception) {
437 // Display a snackbar with the error message.
438 Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();