From: Soren Stoutner Date: Sat, 26 Sep 2020 01:37:42 +0000 (-0700) Subject: Add share, copy, and save options to About > Version. https://redmine.stoutner.com... X-Git-Tag: v3.6~20 X-Git-Url: https://gitweb.stoutner.com/?a=commitdiff_plain;h=1003c7842a01f338c8aaf9d4f07216111f294202;p=PrivacyBrowserAndroid.git Add share, copy, and save options to About > Version. https://redmine.stoutner.com/issues/467 --- diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index 12947148..d11ce2c7 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -68,7 +68,7 @@ @@ -78,7 +78,8 @@ diff --git a/app/src/main/assets/de/about_licenses_dark.html b/app/src/main/assets/de/about_licenses_dark.html index ae50467d..5234e446 100644 --- a/app/src/main/assets/de/about_licenses_dark.html +++ b/app/src/main/assets/de/about_licenses_dark.html @@ -139,6 +139,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/de/about_licenses_light.html b/app/src/main/assets/de/about_licenses_light.html index a77653cc..734d04b2 100644 --- a/app/src/main/assets/de/about_licenses_light.html +++ b/app/src/main/assets/de/about_licenses_light.html @@ -139,6 +139,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/en/about_licenses_dark.html b/app/src/main/assets/en/about_licenses_dark.html index 3591332e..d5686eae 100644 --- a/app/src/main/assets/en/about_licenses_dark.html +++ b/app/src/main/assets/en/about_licenses_dark.html @@ -135,6 +135,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/en/about_licenses_light.html b/app/src/main/assets/en/about_licenses_light.html index 0ca5a629..84b93359 100644 --- a/app/src/main/assets/en/about_licenses_light.html +++ b/app/src/main/assets/en/about_licenses_light.html @@ -136,6 +136,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/es/about_licenses_dark.html b/app/src/main/assets/es/about_licenses_dark.html index dee84fc1..0731f5f4 100644 --- a/app/src/main/assets/es/about_licenses_dark.html +++ b/app/src/main/assets/es/about_licenses_dark.html @@ -139,6 +139,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/es/about_licenses_light.html b/app/src/main/assets/es/about_licenses_light.html index 4b606f58..ef5cfb4f 100644 --- a/app/src/main/assets/es/about_licenses_light.html +++ b/app/src/main/assets/es/about_licenses_light.html @@ -140,6 +140,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/fr/about_licenses_dark.html b/app/src/main/assets/fr/about_licenses_dark.html index 985883d2..1a5e86f1 100644 --- a/app/src/main/assets/fr/about_licenses_dark.html +++ b/app/src/main/assets/fr/about_licenses_dark.html @@ -144,6 +144,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/fr/about_licenses_light.html b/app/src/main/assets/fr/about_licenses_light.html index d45e2ae0..84b7d9d5 100644 --- a/app/src/main/assets/fr/about_licenses_light.html +++ b/app/src/main/assets/fr/about_licenses_light.html @@ -144,6 +144,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/it/about_licenses_dark.html b/app/src/main/assets/it/about_licenses_dark.html index 228c7f65..8df59a4d 100644 --- a/app/src/main/assets/it/about_licenses_dark.html +++ b/app/src/main/assets/it/about_licenses_dark.html @@ -143,6 +143,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/it/about_licenses_light.html b/app/src/main/assets/it/about_licenses_light.html index 09480e18..96fac142 100644 --- a/app/src/main/assets/it/about_licenses_light.html +++ b/app/src/main/assets/it/about_licenses_light.html @@ -144,6 +144,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/ru/about_licenses_dark.html b/app/src/main/assets/ru/about_licenses_dark.html index ff0e5ab2..31185614 100644 --- a/app/src/main/assets/ru/about_licenses_dark.html +++ b/app/src/main/assets/ru/about_licenses_dark.html @@ -137,6 +137,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/ru/about_licenses_light.html b/app/src/main/assets/ru/about_licenses_light.html index b6b088e2..9d6fce31 100644 --- a/app/src/main/assets/ru/about_licenses_light.html +++ b/app/src/main/assets/ru/about_licenses_light.html @@ -137,6 +137,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/shared_images/share_day.png b/app/src/main/assets/shared_images/share_day.png new file mode 100644 index 00000000..d534ef76 Binary files /dev/null and b/app/src/main/assets/shared_images/share_day.png differ diff --git a/app/src/main/assets/shared_images/share_night.png b/app/src/main/assets/shared_images/share_night.png new file mode 100644 index 00000000..23c0ecb7 Binary files /dev/null and b/app/src/main/assets/shared_images/share_night.png differ diff --git a/app/src/main/assets/tr/about_licenses_dark.html b/app/src/main/assets/tr/about_licenses_dark.html index 453ebde1..9d16f4dd 100644 --- a/app/src/main/assets/tr/about_licenses_dark.html +++ b/app/src/main/assets/tr/about_licenses_dark.html @@ -139,6 +139,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/assets/tr/about_licenses_light.html b/app/src/main/assets/tr/about_licenses_light.html index d8031280..5878e2dd 100644 --- a/app/src/main/assets/tr/about_licenses_light.html +++ b/app/src/main/assets/tr/about_licenses_light.html @@ -139,6 +139,7 @@

select_all.

settings.

settings_overscan.

+

share.

smartphone.

sort.

style.

diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.java index 9ce67ad7..0ff3f825 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.java @@ -19,23 +19,63 @@ package com.stoutner.privacybrowser.activities; +import android.Manifest; +import android.app.Activity; +import android.app.Dialog; +import android.content.ContentResolver; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; +import android.view.View; import android.view.WindowManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.fragment.app.DialogFragment; import androidx.viewpager.widget.ViewPager; +import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; import com.stoutner.privacybrowser.adapters.AboutPagerAdapter; import com.stoutner.privacybrowser.R; +import com.stoutner.privacybrowser.asynctasks.SaveAboutVersionImage; +import com.stoutner.privacybrowser.dialogs.SaveDialog; +import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog; +import com.stoutner.privacybrowser.fragments.AboutVersionFragment; +import com.stoutner.privacybrowser.helpers.FileNameHelper; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; + +public class AboutActivity extends AppCompatActivity implements SaveDialog.SaveListener, StoragePermissionDialog.StoragePermissionDialogListener { + // Declare the class variables. + private String filePathString; + private AboutPagerAdapter aboutPagerAdapter; + + // Declare the class views. + private LinearLayout aboutVersionLinearLayout; -public class AboutActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { // Get a handle for the shared preferences. @@ -81,8 +121,11 @@ public class AboutActivity extends AppCompatActivity { // Display the home arrow on action bar. actionBar.setDisplayHomeAsUpEnabled(true); - // Setup the ViewPager. - aboutViewPager.setAdapter(new AboutPagerAdapter(getSupportFragmentManager(), getApplicationContext(), blocklistVersions)); + // Initialize the about pager adapter. + aboutPagerAdapter = new AboutPagerAdapter(getSupportFragmentManager(), getApplicationContext(), blocklistVersions); + + // Setup the ViewPager. + aboutViewPager.setAdapter(aboutPagerAdapter); // Keep all the tabs in memory. This prevents the memory usage updater from running multiple times. aboutViewPager.setOffscreenPageLimit(10); @@ -90,4 +133,265 @@ public class AboutActivity extends AppCompatActivity { // Connect the tab layout to the view pager. aboutTabLayout.setupWithViewPager(aboutViewPager); } + + @Override + public void onSave(int saveType, DialogFragment dialogFragment) { + // Get a handle for the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the lint warning below that the dialog might be null. + assert dialog != null; + + // Get a handle for the file name edit text. + EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext); + + // Get the file path string. + filePathString = fileNameEditText.getText().toString(); + + // Get a handle for the about version linear layout. + aboutVersionLinearLayout = findViewById(R.id.about_version_linearlayout); + + // check to see if the storage permission is needed. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted. + // Save the file according to the type. + switch (saveType) { + case SaveDialog.SAVE_ABOUT_VERSION_TEXT: + // Save the about version text. + saveAsText(filePathString); + break; + + case SaveDialog.SAVE_ABOUT_VERSION_IMAGE: + // Save the about version image. + new SaveAboutVersionImage(this, this, filePathString, aboutVersionLinearLayout).execute(); + break; + } + + // Reset the file path string. + filePathString = ""; + } else { // The storage permission has not been granted. + // Get the external private directory file. + File externalPrivateDirectoryFile = getExternalFilesDir(null); + + // Remove the incorrect lint error below that the file might be null. + assert externalPrivateDirectoryFile != null; + + // Get the external private directory string. + String externalPrivateDirectory = externalPrivateDirectoryFile.toString(); + + // Check to see if the file path is in the external private directory. + if (filePathString.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory. + // Save the webpage according to the type. + switch (saveType) { + case SaveDialog.SAVE_ABOUT_VERSION_TEXT: + // Save the about version text. + saveAsText(filePathString); + break; + + case SaveDialog.SAVE_ABOUT_VERSION_IMAGE: + // Save the about version image. + new SaveAboutVersionImage(this, this, filePathString, aboutVersionLinearLayout).execute(); + break; + } + + // Reset the file path string. + filePathString = ""; + } else { // The file path is in a public directory. + // Check if the user has previously denied the storage permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Declare a storage permission dialog fragment. + DialogFragment storagePermissionDialogFragment; + + // Instantiate the storage permission alert dialog according to the type. + if (saveType == SaveDialog.SAVE_ABOUT_VERSION_TEXT) { + storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.SAVE_TEXT); + } else { + storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.SAVE_IMAGE); + } + + // Show the storage permission alert dialog. The permission will be requested when the dialog is closed. + storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); + } else { // Show the permission request directly. + switch (saveType) { + case SaveDialog.SAVE_ABOUT_VERSION_TEXT: + // Request the write external storage permission. The text will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.SAVE_TEXT); + break; + + case SaveDialog.SAVE_ABOUT_VERSION_IMAGE: + // Request the write external storage permission. The image will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.SAVE_IMAGE); + break; + } + + } + } + } + } + + @Override + public void onCloseStoragePermissionDialog(int requestType) { + // Request the write external storage permission according to the request type. About version will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included). + if (grantResults.length > 0) { + // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. + switch (requestCode) { + case StoragePermissionDialog.SAVE_TEXT: + // Save the about version text. + saveAsText(filePathString); + break; + + case StoragePermissionDialog.SAVE_IMAGE: + // Save the about version image. + new SaveAboutVersionImage(this, this, filePathString, aboutVersionLinearLayout).execute(); + break; + } + } else{ // the storage permission was not granted. + // Display an error snackbar. + Snackbar.make(aboutVersionLinearLayout, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); + } + + // Reset the file path string. + filePathString = ""; + } + } + + // The activity result is called after browsing for a file in the save alert dialog. + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + // Run the default commands. + super.onActivityResult(requestCode, resultCode, data); + + // Only do something if the user didn't press back from the file picker. + if (resultCode == Activity.RESULT_OK) { + // Get a handle for the save dialog fragment. + DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog)); + + // Only update the file name if the dialog still exists. + if (saveDialogFragment != null) { + // Get a handle for the save dialog. + Dialog saveDialog = saveDialogFragment.getDialog(); + + // Remove the lint warning below that the dialog might be null. + assert saveDialog != null; + + // Get a handle for the dialog view. + EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext); + TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview); + + // Get the file name URI from the intent. + Uri fileNameUri = data.getData(); + + // Process the file name URI if it is not null. + if (fileNameUri != null) { + // Instantiate a file name helper. + FileNameHelper fileNameHelper = new FileNameHelper(); + + // Convert the file name URI to a file name path. + String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri); + + // Set the file name path as the text of the file nam edit text. + fileNameEditText.setText(fileNamePath); + + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNamePath.length()); + + // Hid ethe file exists warning. + fileExistsWarningTextView.setVisibility(View.GONE); + } + } + } + } + + private void saveAsText(String fileNameString) { + try { + // Get a handle for the about about version fragment. + AboutVersionFragment aboutVersionFragment = (AboutVersionFragment) aboutPagerAdapter.getTabFragment(0); + + // Get the about version text. + String aboutVersionString = aboutVersionFragment.getAboutVersionString(); + + // Create an input stream with the contents of about version. + InputStream aboutVersionInputStream = new ByteArrayInputStream(aboutVersionString.getBytes(StandardCharsets.UTF_8)); + + // Create an about version buffered reader. + BufferedReader aboutVersionBufferedReader = new BufferedReader(new InputStreamReader(aboutVersionInputStream)); + + // Create a file from the file name string. + File saveFile = new File(fileNameString); + + // Delete the file if it already exists. + if (saveFile.exists()) { + //noinspection ResultOfMethodCallIgnored + saveFile.delete(); + } + + // Create a file buffered writer. + BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile))); + + // Create a transfer string. + String transferString; + + // Use the transfer string to copy the about version text from the buffered reader to the buffered writer. + while ((transferString = aboutVersionBufferedReader.readLine()) != null) { + // Append the line to the buffered writer. + fileBufferedWriter.append(transferString); + + // Append a line break. + fileBufferedWriter.append("\n"); + } + + // Close the buffered reader and writer. + aboutVersionBufferedReader.close(); + fileBufferedWriter.close(); + + // Add the file to the list of recent files. This doesn't currently work, but maybe it will someday. + MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null); + + // Create an about version saved snackbar. + Snackbar aboutVersionSavedSnackbar = Snackbar.make(aboutVersionLinearLayout, getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_SHORT); + + // Add an open option to the snackbar. + aboutVersionSavedSnackbar.setAction(R.string.open, (View view) -> { + // Get a file for the file name string. + File file = new File(fileNameString); + + // Declare a file URI variable. + Uri fileUri; + + // Get the URI for the file according to the Android version. + if (Build.VERSION.SDK_INT >= 24) { // Use a file provider. + fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file); + } else { // Get the raw file path URI. + fileUri = Uri.fromFile(file); + } + + // Get a handle for the content resolver. + ContentResolver contentResolver = getContentResolver(); + + // Create an open intent with `ACTION_VIEW`. + Intent openIntent = new Intent(Intent.ACTION_VIEW); + + // Set the URI and the MIME type. + openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); + + // Allow the app to read the file URI. + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + // Show the chooser. + startActivity(Intent.createChooser(openIntent, getString(R.string.open))); + }); + + // Show the about version saved snackbar. + aboutVersionSavedSnackbar.show(); + } catch (Exception exception) { + // Display a snackbar with the error message. + Snackbar.make(aboutVersionLinearLayout, getString(R.string.error_saving_file) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show(); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java index 5d0589fc..574dc725 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java @@ -330,12 +330,12 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo @Override public boolean onOptionsItemSelected(MenuItem menuItem) { // Get the ID of the menu item that was selected. - int menuItemID = menuItem.getItemId(); + int menuItemId = menuItem.getItemId(); // Get a handle for the fragment manager. FragmentManager fragmentManager = getSupportFragmentManager(); - switch (menuItemID) { + switch (menuItemId) { case android.R.id.home: // The home arrow is identified as `android.R.id.home`, not just `R.id.home`. if (twoPanedMode) { // The device is in two-paned mode. // Save the current domain settings if the domain settings fragment is displayed. diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java index 7878f985..2540ce49 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java @@ -24,12 +24,14 @@ import android.app.Activity; import android.app.Dialog; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.ContentResolver; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.media.MediaScannerConnection; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.TypedValue; @@ -47,6 +49,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.fragment.app.DialogFragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -55,7 +58,7 @@ import com.google.android.material.snackbar.Snackbar; import com.stoutner.privacybrowser.R; import com.stoutner.privacybrowser.asynctasks.GetLogcat; import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog; -import com.stoutner.privacybrowser.dialogs.SaveLogcatDialog; +import com.stoutner.privacybrowser.dialogs.SaveDialog; import com.stoutner.privacybrowser.helpers.FileNameHelper; import java.io.BufferedReader; @@ -69,11 +72,11 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialog.SaveLogcatListener, StoragePermissionDialog.StoragePermissionDialogListener { - // Initialize the saved instance state constants. +public class LogcatActivity extends AppCompatActivity implements SaveDialog.SaveListener, StoragePermissionDialog.StoragePermissionDialogListener { + // Declare the class constants. private final String SCROLLVIEW_POSITION = "scrollview_position"; - // Define the class variables. + // Declare the class variables. private String filePathString; // Define the class views. @@ -85,7 +88,7 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -128,10 +131,10 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; // Set the refresh color scheme according to the theme. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) { - swipeRefreshLayout.setColorSchemeResources(R.color.blue_500); - } else { + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { swipeRefreshLayout.setColorSchemeResources(R.color.blue_700); + } else { + swipeRefreshLayout.setColorSchemeResources(R.color.blue_500); } // Initialize a color background typed value. @@ -179,13 +182,13 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo // Get a handle for the clipboard manager. ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - // Save the logcat in a ClipData. - ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText()); - - // Remove the incorrect lint error that `clipboardManager.setPrimaryClip()` might produce a null pointer exception. + // Remove the incorrect lint error below that the clipboard manager might be null. assert clipboardManager != null; - // Place the ClipData on the clipboard. + // Save the logcat in a clip data. + ClipData logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.getText()); + + // Place the clip data on the clipboard. clipboardManager.setPrimaryClip(logcatClipData); // Display a snackbar. @@ -196,7 +199,7 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo case R.id.save: // Instantiate the save alert dialog. - DialogFragment saveDialogFragment = new SaveLogcatDialog(); + DialogFragment saveDialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT); // Show the save alert dialog. saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_logcat)); @@ -243,11 +246,11 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo } @Override - public void onSaveLogcat(DialogFragment dialogFragment) { - // Get a handle for the dialog fragment. + public void onSave(int saveType, DialogFragment dialogFragment) { + // Get a handle for the dialog. Dialog dialog = dialogFragment.getDialog(); - // Remove the lint warning below that the dialog fragment might be null. + // Remove the lint warning below that the dialog might be null. assert dialog != null; // Get a handle for the file name edit text. @@ -261,7 +264,7 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo // Save the logcat. saveLogcat(filePathString); } else { // The storage permission has not been granted. - // Get the external private directory `File`. + // Get the external private directory file. File externalPrivateDirectoryFile = getExternalFilesDir(null); // Remove the incorrect lint error below that the file might be null. @@ -274,10 +277,10 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo if (filePathString.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory. // Save the logcat. saveLogcat(filePathString); - } else { // The file path in in a public directory. + } else { // The file path is in a public directory. // Check if the user has previously denied the storage permission. if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the storage permission alert dialog. + // Instantiate the storage permission alert dialog. The type is specified as `0` because it currently isn't used for this activity. DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0); // Show the storage permission alert dialog. The permission will be requested when the dialog is closed. @@ -292,7 +295,7 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo } @Override - public void onCloseStoragePermissionDialog(int type) { + public void onCloseStoragePermissionDialog(int requestType) { // Request the write external storage permission. The logcat will be saved when it finishes. ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0); } @@ -309,6 +312,53 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo } } + // The activity result is called after browsing for a file in the save alert dialog. + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + // Run the default commands. + super.onActivityResult(requestCode, resultCode, data); + + // Only do something if the user didn't press back from the file picker. + if (resultCode == Activity.RESULT_OK) { + // Get a handle for the save dialog fragment. + DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat)); + + // Only update the file name if the dialog still exists. + if (saveDialogFragment != null) { + // Get a handle for the save dialog. + Dialog saveDialog = saveDialogFragment.getDialog(); + + // Remove the lint warning below that the save dialog might be null. + assert saveDialog != null; + + // Get a handle for the dialog views. + EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext); + TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview); + + // Get the file name URI from the intent. + Uri fileNameUri = data.getData(); + + // Process the file name URI if it is not null. + if (fileNameUri != null) { + // Instantiate a file name helper. + FileNameHelper fileNameHelper = new FileNameHelper(); + + // Convert the file name URI to a file name path. + String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri); + + // Set the file name path as the text of the file name edit text. + fileNameEditText.setText(fileNamePath); + + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNamePath.length()); + + // Hide the file exists warning. + fileExistsWarningTextView.setVisibility(View.GONE); + } + } + } + } + private void saveLogcat(String fileNameString) { try { // Get the logcat as a string. @@ -323,6 +373,12 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo // Create a file from the file name string. File saveFile = new File(fileNameString); + // Delete the file if it already exists. + if (saveFile.exists()) { + //noinspection ResultOfMethodCallIgnored + saveFile.delete(); + } + // Create a file buffered writer. BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile))); @@ -345,58 +401,45 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo // Add the file to the list of recent files. This doesn't currently work, but maybe it will someday. MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null); - // Display a snackbar. - Snackbar.make(logcatTextView, getString(R.string.file_saved_successfully), Snackbar.LENGTH_SHORT).show(); - } catch (Exception exception) { - // Display a snackbar with the error message. - Snackbar.make(logcatTextView, getString(R.string.save_failed) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show(); - } - } + // Create a logcat saved snackbar. + Snackbar logcatSavedSnackbar = Snackbar.make(logcatTextView, getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_SHORT); - // The activity result is called after browsing for a file in the save alert dialog. - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - // Run the default commands. - super.onActivityResult(requestCode, resultCode, data); - - // Don't do anything if the user pressed back from the file picker. - if (resultCode == Activity.RESULT_OK) { - // Get a handle for the save dialog fragment. - DialogFragment saveDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_logcat)); - - // Only update the file name if the dialog still exists. - if (saveDialogFragment != null) { - // Get a handle for the save dialog. - Dialog saveDialog = saveDialogFragment.getDialog(); + // Add an open action to the snackbar. + logcatSavedSnackbar.setAction(R.string.open, (View view) -> { + // Get a file for the file name string. + File file = new File(fileNameString); - // Remove the lint warning below that the save dialog might be null. - assert saveDialog != null; + // Declare a file URI variable. + Uri fileUri; - // Get a handle for the dialog views. - EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext); - TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview); + // Get the URI for the file according to the Android version. + if (Build.VERSION.SDK_INT >= 24) { // Use a file provider. + fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file); + } else { // Get the raw file path URI. + fileUri = Uri.fromFile(file); + } - // Instantiate the file name helper. - FileNameHelper fileNameHelper = new FileNameHelper(); + // Get a handle for the content resolver. + ContentResolver contentResolver = getContentResolver(); - // Get the file name URI from the intent. - Uri fileNameUri= data.getData(); + // Create an open intent with `ACTION_VIEW`. + Intent openIntent = new Intent(Intent.ACTION_VIEW); - // Process the file name URI if it is not null. - if (fileNameUri != null) { - // Convert the file name URI to a file name path. - String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri); + // Set the URI and the MIME type. + openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); - // Set the file name path as the text of the file name edit text. - fileNameEditText.setText(fileNamePath); + // Allow the app to read the file URI. + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNamePath.length()); + // Show the chooser. + startActivity(Intent.createChooser(openIntent, getString(R.string.open))); + }); - // Hide the file exists warning. - fileExistsWarningTextView.setVisibility(View.GONE); - } - } + // Show the logcat saved snackbar. + logcatSavedSnackbar.show(); + } catch (Exception exception) { + // Display a snackbar with the error message. + Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show(); } } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 36c2edeb..600db329 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -31,6 +31,7 @@ import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -103,6 +104,7 @@ import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; @@ -136,7 +138,7 @@ import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; import com.stoutner.privacybrowser.dialogs.OpenDialog; import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog; import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog; -import com.stoutner.privacybrowser.dialogs.SaveDialog; +import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog; import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; @@ -175,7 +177,7 @@ import java.util.concurrent.Executors; public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, - PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener, + PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener, UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener { // The executor service handles background tasks. It is accessed from `ViewSourceActivity`. @@ -216,13 +218,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes. public static String proxyMode = ProxyHelper.NONE; - - // The permission result request codes are used in `onCreateContextMenu()`, `onRequestPermissionResult()`, `onSaveWebpage()`, `onCloseStoragePermissionDialog()`, and `initializeWebView()`. - private final int PERMISSION_OPEN_REQUEST_CODE = 0; - private final int PERMISSION_SAVE_URL_REQUEST_CODE = 1; - private final int PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE = 2; - private final int PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE = 3; - // Define the saved instance state constants. private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list"; private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list"; @@ -1078,7 +1073,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } @Override - // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. + // Remove Android Studio's warning about the dangers of enabling JavaScript. We know. Oh, how we know. @SuppressLint("SetJavaScriptEnabled") public boolean onOptionsItemSelected(MenuItem menuItem) { // Get the selected menu item ID. @@ -1723,18 +1718,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Consume the event. return true; - case R.id.save_as_archive: - // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_ARCHIVE, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl()); + case R.id.save_archive: + // Instantiate the save dialog. + DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null, + false); + + // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. + saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); // Consume the event. return true; - case R.id.save_as_image: - // Prepare the save dialog. The dialog will be displayed once the file size adn the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_IMAGE, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl()); + case R.id.save_image: + // Instantiate the save dialog. + DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null, + false); + + // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. + saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); // Consume the event. return true; @@ -1770,9 +1771,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Create the share intent. Intent shareIntent = new Intent(Intent.ACTION_SEND); + + // Add the share string to the intent. shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); + + // Set the MIME type. shareIntent.setType("text/plain"); + // Set the intent to open in a new task. + shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Make it so. startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url))); @@ -2416,8 +2424,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Make it so. - startActivity(emailIntent); + try { + // Make it so. + startActivity(emailIntent); + } catch (ActivityNotFoundException exception) { + // Display a snackbar. + Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); + } // Consume the event. return true; @@ -3011,7 +3024,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); } else { // Show the permission request directly. // Request the write external storage permission. The file will be opened when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE); + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.OPEN); } } } @@ -3042,14 +3055,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); break; - case StoragePermissionDialog.SAVE_AS_ARCHIVE: + case StoragePermissionDialog.SAVE_ARCHIVE: // Save the webpage archive. - currentWebView.saveWebArchive(saveWebpageFilePath); + saveWebpageArchive(); break; - case StoragePermissionDialog.SAVE_AS_IMAGE: + case StoragePermissionDialog.SAVE_IMAGE: // Save the webpage image. - new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath); + new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute(); break; } } else { // The storage permission has not been granted. @@ -3071,14 +3084,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); break; - case StoragePermissionDialog.SAVE_AS_ARCHIVE: + case StoragePermissionDialog.SAVE_ARCHIVE: // Save the webpage archive. - currentWebView.saveWebArchive(saveWebpageFilePath); + saveWebpageArchive(); break; - case StoragePermissionDialog.SAVE_AS_IMAGE: + case StoragePermissionDialog.SAVE_IMAGE: // Save the webpage image. - new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath); + new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute(); break; } } else { // The file path is in a public directory. @@ -3090,21 +3103,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Show the storage permission alert dialog. The permission will be requested when the dialog is closed. storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); } else { // Show the permission request directly. - switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - // Request the write external storage permission. The URL will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE); - - case StoragePermissionDialog.SAVE_AS_ARCHIVE: - // Request the write external storage permission. The webpage archive will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE); - break; - - case StoragePermissionDialog.SAVE_AS_IMAGE: - // Request the write external storage permission. The webpage image will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE); - break; - } + // Request the write external storage permission according to the save type. The URL will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType); } } } @@ -3112,27 +3112,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onCloseStoragePermissionDialog(int requestType) { - switch (requestType) { - case StoragePermissionDialog.OPEN: - // Request the write external storage permission. The file will be opened when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE); - break; + // Request the write external storage permission according to the request type. The file will be opened when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType); - case StoragePermissionDialog.SAVE_URL: - // Request the write external storage permission. The URL will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE); - break; - - case StoragePermissionDialog.SAVE_AS_ARCHIVE: - // Request the write external storage permission. The webpage archive will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE); - break; - - case StoragePermissionDialog.SAVE_AS_IMAGE: - // Request the write external storage permission. The webpage image will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE); - break; - } } @Override @@ -3140,7 +3122,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included). if (grantResults.length > 0) { switch (requestCode) { - case PERMISSION_OPEN_REQUEST_CODE: + case StoragePermissionDialog.OPEN: // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. // Load the file. @@ -3154,7 +3136,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook openFilePath = ""; break; - case PERMISSION_SAVE_URL_REQUEST_CODE: + case StoragePermissionDialog.SAVE_URL: // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. // Save the raw URL. @@ -3169,11 +3151,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook saveWebpageFilePath = ""; break; - case PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE: + case StoragePermissionDialog.SAVE_ARCHIVE: // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. // Save the webpage archive. - currentWebView.saveWebArchive(saveWebpageFilePath); + saveWebpageArchive(); } else { // The storage permission was not granted. // Display an error snackbar. Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); @@ -3183,11 +3165,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook saveWebpageFilePath = ""; break; - case PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE: + case StoragePermissionDialog.SAVE_IMAGE: // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. // Save the webpage image. - new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath); + new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute(); } else { // The storage permission was not granted. // Display an error snackbar. Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); @@ -4875,6 +4857,51 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook appBarLayout.setExpanded(true); } + private void saveWebpageArchive() { + // Save the webpage archive. + currentWebView.saveWebArchive(saveWebpageFilePath); + + // Display a snackbar. + Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + saveWebpageFilePath, Snackbar.LENGTH_SHORT); + + // Add an open option to the snackbar. + saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> { + // Get a file for the file name string. + File file = new File(saveWebpageFilePath); + + // Declare a file URI variable. + Uri fileUri; + + // Get the URI for the file according to the Android version. + if (Build.VERSION.SDK_INT >= 24) { // Use a file provider. + fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file); + } else { // Get the raw file path URI. + fileUri = Uri.fromFile(file); + } + + // Get a handle for the content resolver. + ContentResolver contentResolver = getContentResolver(); + + // Create an open intent with `ACTION_VIEW`. + Intent openIntent = new Intent(Intent.ACTION_VIEW); + + // Set the URI and the MIME type. + openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); + + // Allow the app to read the file URI. + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + // Show the chooser. + startActivity(Intent.createChooser(openIntent, getString(R.string.open))); + }); + + // Show the snackbar. + saveWebpageArchiveSnackbar.show(); + + // Reset the save Webpage file path. + saveWebpageFilePath = ""; + } + private void clearAndExit() { // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); @@ -5387,7 +5414,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook String fileNameString = PrepareSaveDialog.getFileNameFromContentDisposition(this, contentDisposition, downloadUrl); // Instantiate the save dialog. - DialogFragment saveDialogFragment = SaveDialog.saveUrl(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, + DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, nestedScrollWebView.getAcceptFirstPartyCookies()); // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. @@ -5722,8 +5749,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Open the email program in a new task instead of as part of Privacy Browser. emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Make it so. - startActivity(emailIntent); + try { + // Make it so. + startActivity(emailIntent); + } catch (ActivityNotFoundException exception) { + // Display a snackbar. + Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); + } + // Returning true indicates Privacy Browser is handling the URL by creating an intent. return true; @@ -5737,8 +5770,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Open the dialer in a new task instead of as part of Privacy Browser. dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Make it so. - startActivity(dialIntent); + try { + // Make it so. + startActivity(dialIntent); + } catch (ActivityNotFoundException exception) { + // Display a snackbar. + Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); + } // Returning true indicates Privacy Browser is handling the URL by creating an intent. return true; diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/AboutPagerAdapter.java b/app/src/main/java/com/stoutner/privacybrowser/adapters/AboutPagerAdapter.java index 9d4c9191..0a506709 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/adapters/AboutPagerAdapter.java +++ b/app/src/main/java/com/stoutner/privacybrowser/adapters/AboutPagerAdapter.java @@ -27,12 +27,16 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.fragments.AboutTabFragment; +import com.stoutner.privacybrowser.fragments.AboutVersionFragment; +import com.stoutner.privacybrowser.fragments.AboutWebViewFragment; + +import java.util.LinkedList; public class AboutPagerAdapter extends FragmentPagerAdapter { // Define the class variables. private Context context; private String[] blocklistVersions; + private LinkedList aboutFragmentList = new LinkedList<>(); public AboutPagerAdapter(FragmentManager fragmentManager, Context context, String[] blocklistVersions) { // Run the default commands. @@ -83,6 +87,21 @@ public class AboutPagerAdapter extends FragmentPagerAdapter { @NonNull // Setup each tab. public Fragment getItem(int tabNumber) { - return AboutTabFragment.createTab(tabNumber, blocklistVersions); + // Create the tab fragment and add it to the list. + if (tabNumber == 0){ + // Add the version tab to the list. + aboutFragmentList.add(AboutVersionFragment.createTab(blocklistVersions)); + } else { + // Add the WebView tab to the list. + aboutFragmentList.add(AboutWebViewFragment.createTab(tabNumber)); + } + + // Return the tab number fragment. + return aboutFragmentList.get(tabNumber); + } + + public Fragment getTabFragment(int tabNumber) { + // Return the tab fragment. + return aboutFragmentList.get(tabNumber); } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java index 510ac739..e6c83165 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java +++ b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java @@ -136,6 +136,7 @@ public class WebViewPagerAdapter extends FragmentPagerAdapter { } public WebViewTabFragment getPageFragment(int pageNumber) { + // Return the page fragment. return webViewFragmentsList.get(pageNumber); } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java index 9b1dcd09..5bfae1e3 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java @@ -29,7 +29,7 @@ import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentManager; import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.dialogs.SaveDialog; +import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog; import com.stoutner.privacybrowser.helpers.ProxyHelper; import java.lang.ref.WeakReference; @@ -172,7 +172,7 @@ public class PrepareSaveDialog extends AsyncTask { } // Instantiate the save dialog. - DialogFragment saveDialogFragment = SaveDialog.saveUrl(saveType, urlString, fileStringArray[0], fileStringArray[1], userAgent, cookiesEnabled); + DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(saveType, urlString, fileStringArray[0], fileStringArray[1], userAgent, cookiesEnabled); // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. saveDialogFragment.show(fragmentManager, activity.getString(R.string.save_dialog)); diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java new file mode 100644 index 00000000..eda5726c --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java @@ -0,0 +1,209 @@ +/* + * Copyright © 2020 Soren Stoutner . + * + * This file is part of Privacy Browser . + * + * Privacy Browser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser. If not, see . + */ + +package com.stoutner.privacybrowser.asynctasks; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.core.content.FileProvider; + +import com.google.android.material.snackbar.Snackbar; + +import com.stoutner.privacybrowser.R; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.lang.ref.WeakReference; + +public class SaveAboutVersionImage extends AsyncTask { + // Declare the weak references. + private WeakReference contextWeakReference; + private WeakReference activityWeakReference; + private WeakReference aboutVersionLinearLayoutWeakReference; + + // Declare the class constants. + private final String SUCCESS = "Success"; + + // Declare the class variables. + private Snackbar savingImageSnackbar; + private Bitmap aboutVersionBitmap; + private String filePathString; + + // The public constructor. + public SaveAboutVersionImage(Context context, Activity activity, String filePathString, LinearLayout aboutVersionLinearLayout) { + // Populate the weak references. + contextWeakReference = new WeakReference<>(context); + activityWeakReference = new WeakReference<>(activity); + aboutVersionLinearLayoutWeakReference = new WeakReference<>(aboutVersionLinearLayout); + + // Store the class variables. + this.filePathString = filePathString; + } + + // `onPreExecute()` operates on the UI thread. + @Override + protected void onPreExecute() { + // Get handles for the activity and the linear layout. + Activity activity = activityWeakReference.get(); + LinearLayout aboutVersionLinearLayout = aboutVersionLinearLayoutWeakReference.get(); + + // Abort if the activity or the linear layout is gone. + if ((activity == null) || activity.isFinishing() || aboutVersionLinearLayout == null) { + return; + } + + // Create a saving image snackbar. + savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image) + " " + filePathString, Snackbar.LENGTH_INDEFINITE); + + // Display the saving image snackbar. + savingImageSnackbar.show(); + + // Create the about version bitmap. This can be replaced by PixelCopy once the minimum API >= 26. + // Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The linear layout commands must be run on the UI thread. + aboutVersionBitmap = Bitmap.createBitmap(aboutVersionLinearLayout.getWidth(), aboutVersionLinearLayout.getHeight(), Bitmap.Config.ARGB_8888); + + // Create a canvas. + Canvas aboutVersionCanvas = new Canvas(aboutVersionBitmap); + + // Draw the current about version onto the bitmap. The linear layout commands must be run on the UI thread. + aboutVersionLinearLayout.draw(aboutVersionCanvas); + } + + @Override + protected String doInBackground(Void... Void) { + // Get a handle for the activity. + Activity activity = activityWeakReference.get(); + + // Abort if the activity is gone. + if (((activity == null) || activity.isFinishing())) { + return ""; + } + + // Create an about version PNG byte array output stream. + ByteArrayOutputStream aboutVersionByteArrayOutputStream = new ByteArrayOutputStream(); + + // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time. Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS. + aboutVersionBitmap.compress(Bitmap.CompressFormat.PNG, 0, aboutVersionByteArrayOutputStream); + + // Get a file for the image. + File imageFile = new File(filePathString); + + // Delete the current file if it exists. + if (imageFile.exists()) { + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + } + + // Create a file creation disposition string. + String fileCreationDisposition = SUCCESS; + + try { + // Create an image file output stream. + FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile); + + // Write the webpage image to the image file. + aboutVersionByteArrayOutputStream.writeTo(imageFileOutputStream); + + // Create a media scanner intent, which adds items like pictures to Android's recent file list. + Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + + // Add the URI to the media scanner intent. + mediaScannerIntent.setData(Uri.fromFile(imageFile)); + + // Make it so. + activity.sendBroadcast(mediaScannerIntent); + } catch (Exception exception) { + // Store the error in the file creation disposition string. + fileCreationDisposition = exception.toString(); + } + + // return the file creation disposition string. + return fileCreationDisposition; + } + + // `onPostExecute()` operates on the UI thread. + @Override + protected void onPostExecute(String fileCreationDisposition) { + // Get handles for the weak references. + Context context = contextWeakReference.get(); + Activity activity = activityWeakReference.get(); + LinearLayout aboutVersionLinearLayout = aboutVersionLinearLayoutWeakReference.get(); + + // Abort if the activity is gone. + if ((activity == null) || activity.isFinishing()) { + return; + } + + // Dismiss the saving image snackbar. + savingImageSnackbar.dismiss(); + + // Display a file creation disposition snackbar. + if (fileCreationDisposition.equals(SUCCESS)) { + // Create a file saved snackbar. + Snackbar imageSavedSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.file_saved) + " " + filePathString, Snackbar.LENGTH_SHORT); + + // Add an open action. + imageSavedSnackbar.setAction(R.string.open, (View view) -> { + // Get a file for the file path string. + File file = new File(filePathString); + + // Declare a file URI variable. + Uri fileUri; + + // Get the URI for the file according to the Android version. + if (Build.VERSION.SDK_INT >= 24) { // Use a file provider. + fileUri = FileProvider.getUriForFile(context, activity.getString(R.string.file_provider), file); + } else { // Get the raw file path URI. + fileUri = Uri.fromFile(file); + } + + // Get a handle for the content resolver. + ContentResolver contentResolver = context.getContentResolver(); + + // Create an open intent with `ACTION_VIEW`. + Intent openIntent = new Intent(Intent.ACTION_VIEW); + + // Autodetect the MIME type. + openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); + + // Allow the app to read the file URI. + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + // Show the chooser. + activity.startActivity(Intent.createChooser(openIntent, context.getString(R.string.open))); + }); + + // Show the image saved snackbar. + imageSavedSnackbar.show(); + } else { + Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.error_saving_file) + " " + fileCreationDisposition, Snackbar.LENGTH_INDEFINITE).show(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java index 6b383f4e..ad200216 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java +++ b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java @@ -20,7 +20,6 @@ package com.stoutner.privacybrowser.asynctasks; import android.app.Activity; -import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -50,7 +49,7 @@ import java.net.URL; import java.text.NumberFormat; public class SaveUrl extends AsyncTask { - // Define a weak references for the calling context and activity. + // Define a weak references. private WeakReference contextWeakReference; private WeakReference activityWeakReference; @@ -248,7 +247,7 @@ public class SaveUrl extends AsyncTask { // Check to see if a download percentage has been calculated. if (downloadPercentage[0] < 0) { // There is no download percentage. The negative number represents the raw downloaded kilobytes. // Calculate the number of bytes downloaded. When the `downloadPercentage` is negative, it is actually the raw number of kilobytes downloaded. - long numberOfBytesDownloaded = 0 - downloadPercentage[0]; + long numberOfBytesDownloaded = - downloadPercentage[0]; // Format the number of bytes downloaded. String formattedNumberOfBytesDownloaded = NumberFormat.getInstance().format(numberOfBytesDownloaded); @@ -286,11 +285,11 @@ public class SaveUrl extends AsyncTask { // Add an open action if the file is not an APK on API >= 26 (that scenario requires the REQUEST_INSTALL_PACKAGES permission). if (!(Build.VERSION.SDK_INT >= 26 && filePathString.endsWith(".apk"))) { - fileSavedSnackbar.setAction(R.string.open, (View v) -> { + fileSavedSnackbar.setAction(R.string.open, (View view) -> { // Get a file for the file path string. File file = new File(filePathString); - // Define a file URI variable + // Declare a file URI variable. Uri fileUri; // Get the URI for the file according to the Android version. @@ -316,14 +315,8 @@ public class SaveUrl extends AsyncTask { // Allow the app to read the file URI. openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Try the intent. - try { - // Show the chooser. - activity.startActivity(openIntent); - } catch (ActivityNotFoundException exception) { // There are no apps available to open the URL. - // Show a snackbar with the error. - Snackbar.make(noSwipeViewPager, activity.getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); - } + // Show the chooser. + activity.startActivity(Intent.createChooser(openIntent, context.getString(R.string.open))); }); } diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java index aeb92988..65463f35 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java +++ b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java @@ -1,5 +1,5 @@ /* - * Copyright © 2019 Soren Stoutner . + * Copyright © 2019-2020 Soren Stoutner . * * This file is part of Privacy Browser . * @@ -20,44 +20,57 @@ package com.stoutner.privacybrowser.asynctasks; import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; +import android.view.View; + +import androidx.core.content.FileProvider; import com.google.android.material.snackbar.Snackbar; import com.stoutner.privacybrowser.R; import com.stoutner.privacybrowser.views.NestedScrollWebView; -import com.stoutner.privacybrowser.views.NoSwipeViewPager; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.lang.ref.WeakReference; -public class SaveWebpageImage extends AsyncTask { - // Define the weak references. +public class SaveWebpageImage extends AsyncTask { + // Declare the weak references. + private WeakReference contextWeakReference; private WeakReference activityWeakReference; private WeakReference nestedScrollWebViewWeakReference; - // Define a success string constant. + // Declare the class constants. private final String SUCCESS = "Success"; - // Define the saving image snackbar and the webpage bitmap. + // Declare the class variables. private Snackbar savingImageSnackbar; private Bitmap webpageBitmap; + private String filePathString; // The public constructor. - public SaveWebpageImage(Activity activity, NestedScrollWebView nestedScrollWebView) { + public SaveWebpageImage(Context context, Activity activity, String filePathString, NestedScrollWebView nestedScrollWebView) { // Populate the weak references. + contextWeakReference = new WeakReference<>(context); activityWeakReference = new WeakReference<>(activity); nestedScrollWebViewWeakReference = new WeakReference<>(nestedScrollWebView); + + // Populate the class variables. + this.filePathString = filePathString; } // `onPreExecute()` operates on the UI thread. @Override protected void onPreExecute() { - // Get a handle for the activity and the nested scroll WebView. + // Get handles for the activity and the nested scroll WebView. Activity activity = activityWeakReference.get(); NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get(); @@ -67,7 +80,7 @@ public class SaveWebpageImage extends AsyncTask { } // Create a saving image snackbar. - savingImageSnackbar = Snackbar.make(nestedScrollWebView, R.string.saving_image, Snackbar.LENGTH_INDEFINITE); + savingImageSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.processing_image) + " " + filePathString, Snackbar.LENGTH_INDEFINITE); // Display the saving image snackbar. savingImageSnackbar.show(); @@ -83,7 +96,7 @@ public class SaveWebpageImage extends AsyncTask { } @Override - protected String doInBackground(String... fileName) { + protected String doInBackground(Void... Void) { // Get a handle for the activity. Activity activity = activityWeakReference.get(); @@ -95,11 +108,11 @@ public class SaveWebpageImage extends AsyncTask { // Create a webpage PNG byte array output stream. ByteArrayOutputStream webpageByteArrayOutputStream = new ByteArrayOutputStream(); - // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time. + // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time. Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS. webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream); // Get a file for the image. - File imageFile = new File(fileName[0]); + File imageFile = new File(filePathString); // Delete the current file if it exists. if (imageFile.exists()) { @@ -116,6 +129,15 @@ public class SaveWebpageImage extends AsyncTask { // Write the webpage image to the image file. webpageByteArrayOutputStream.writeTo(imageFileOutputStream); + + // Create a media scanner intent, which adds items like pictures to Android's recent file list. + Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + + // Add the URI to the media scanner intent. + mediaScannerIntent.setData(Uri.fromFile(imageFile)); + + // Make it so. + activity.sendBroadcast(mediaScannerIntent); } catch (Exception exception) { // Store the error in the file creation disposition string. fileCreationDisposition = exception.toString(); @@ -128,25 +150,60 @@ public class SaveWebpageImage extends AsyncTask { // `onPostExecute()` operates on the UI thread. @Override protected void onPostExecute(String fileCreationDisposition) { - // Get a handle for the activity. + // Get handles for the weak references. + Context context = contextWeakReference.get(); Activity activity = activityWeakReference.get(); + NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get(); // Abort if the activity is gone. if ((activity == null) || activity.isFinishing()) { return; } - // Get a handle for the no swipe view pager. - NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager); - // Dismiss the saving image snackbar. savingImageSnackbar.dismiss(); // Display a file creation disposition snackbar. if (fileCreationDisposition.equals(SUCCESS)) { - Snackbar.make(noSwipeViewPager, R.string.image_saved, Snackbar.LENGTH_SHORT).show(); + // Create a file saved snackbar. + Snackbar imageSavedSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.file_saved) + " " + filePathString, Snackbar.LENGTH_SHORT); + + // Add an open action. + imageSavedSnackbar.setAction(R.string.open, (View view) -> { + // Get a file for the file path string. + File file = new File(filePathString); + + // Declare a file URI variable. + Uri fileUri; + + // Get the URI for the file according to the Android version. + if (Build.VERSION.SDK_INT >= 24) { // Use a file provider. + fileUri = FileProvider.getUriForFile(context, activity.getString(R.string.file_provider), file); + } else { // Get the raw file path URI. + fileUri = Uri.fromFile(file); + } + + // Get a handle for the content resolver. + ContentResolver contentResolver = context.getContentResolver(); + + // Create an open intent with `ACTION_VIEW`. + Intent openIntent = new Intent(Intent.ACTION_VIEW); + + // Autodetect the MIME type. + openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); + + // Allow the app to read the file URI. + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + // Show the chooser. + activity.startActivity(Intent.createChooser(openIntent, context.getString(R.string.open))); + }); + + // Show the image saved snackbar. + imageSavedSnackbar.show(); } else { - Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_image) + " " + fileCreationDisposition, Snackbar.LENGTH_INDEFINITE).show(); + // Display the file saving error. + Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file) + " " + fileCreationDisposition, Snackbar.LENGTH_INDEFINITE).show(); } } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.java index 0730717c..2b591f7b 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.java @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2020 Soren Stoutner . + * Copyright © 2016-2020 Soren Stoutner . * * This file is part of Privacy Browser . * @@ -29,10 +29,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.text.Editable; import android.text.TextWatcher; @@ -46,56 +46,54 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; -import androidx.preference.PreferenceManager; import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.activities.MainWebViewActivity; -import com.stoutner.privacybrowser.asynctasks.GetUrlSize; import com.stoutner.privacybrowser.helpers.DownloadLocationHelper; import java.io.File; public class SaveDialog extends DialogFragment { - // Define the save webpage listener. - private SaveWebpageListener saveWebpageListener; + // Declare the save listener. + private SaveListener saveListener; // The public interface is used to send information back to the parent activity. - public interface SaveWebpageListener { - void onSaveWebpage(int saveType, DialogFragment dialogFragment); + public interface SaveListener { + void onSave(int saveType, DialogFragment dialogFragment); } - // Define the get URL size AsyncTask. This allows previous instances of the task to be cancelled if a new one is run. - private AsyncTask getUrlSize; + // Declare the class constants. + public static final int SAVE_LOGCAT = 0; + public static final int SAVE_ABOUT_VERSION_TEXT = 1; + public static final int SAVE_ABOUT_VERSION_IMAGE = 2; + private static final String SAVE_TYPE = "save_type"; + + // Declare the class variables. + String fileName; @Override public void onAttach(@NonNull Context context) { // Run the default commands. super.onAttach(context); - // Get a handle for the save webpage listener from the launching context. - saveWebpageListener = (SaveWebpageListener) context; + // Get a handle for save listener from the launching context. + saveListener = (SaveListener) context; } - public static SaveDialog saveUrl(int saveType, String urlString, String fileSizeString, String contentDispositionFileNameString, String userAgentString, boolean cookiesEnabled) { + public static SaveDialog save(int saveType) { // Create an arguments bundle. Bundle argumentsBundle = new Bundle(); // Store the arguments in the bundle. - argumentsBundle.putInt("save_type", saveType); - argumentsBundle.putString("url_string", urlString); - argumentsBundle.putString("file_size_string", fileSizeString); - argumentsBundle.putString("content_disposition_file_name_string", contentDispositionFileNameString); - argumentsBundle.putString("user_agent_string", userAgentString); - argumentsBundle.putBoolean("cookies_enabled", cookiesEnabled); + argumentsBundle.putInt(SAVE_TYPE, saveType); - // Create a new instance of the save webpage dialog. - SaveDialog saveWebpageDialog = new SaveDialog(); + // Create a new instance of the save dialog. + SaveDialog saveDialog = new SaveDialog(); - // Add the arguments bundle to the new dialog. - saveWebpageDialog.setArguments(argumentsBundle); + // Add the arguments bundle to the dialog. + saveDialog.setArguments(argumentsBundle); // Return the new dialog. - return saveWebpageDialog; + return saveDialog; } // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog. @@ -110,12 +108,7 @@ public class SaveDialog extends DialogFragment { assert arguments != null; // Get the arguments from the bundle. - int saveType = arguments.getInt("save_type"); - String urlString = arguments.getString("url_string"); - String fileSizeString = arguments.getString("file_size_string"); - String contentDispositionFileNameString = arguments.getString("content_disposition_file_name_string"); - String userAgentString = arguments.getString("user_agent_string"); - boolean cookiesEnabled = arguments.getBoolean("cookies_enabled"); + int saveType = arguments.getInt(SAVE_TYPE); // Get a handle for the activity and the context. Activity activity = requireActivity(); @@ -127,51 +120,42 @@ public class SaveDialog extends DialogFragment { // Get the current theme status. int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // Set the icon according to the theme. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) { // The night theme is enabled. - // Set the icon according to the save type. - switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - dialogBuilder.setIcon(R.drawable.copy_enabled_night); - break; - - case StoragePermissionDialog.SAVE_AS_ARCHIVE: - dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night); - break; - - case StoragePermissionDialog.SAVE_AS_IMAGE: - dialogBuilder.setIcon(R.drawable.images_enabled_night); - break; - } - } else { // The day theme is enabled. - // Set the icon according to the save type. - switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - dialogBuilder.setIcon(R.drawable.copy_enabled_day); - break; - - case StoragePermissionDialog.SAVE_AS_ARCHIVE: - dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day); - break; - - case StoragePermissionDialog.SAVE_AS_IMAGE: - dialogBuilder.setIcon(R.drawable.images_enabled_day); - break; - } - } - - // Set the title according to the type. + // Set the title and icon according to the type. switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - dialogBuilder.setTitle(R.string.save); + case SAVE_LOGCAT: + // Set the title. + dialogBuilder.setTitle(R.string.save_logcat); + + // Set the icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + dialogBuilder.setIcon(R.drawable.save_dialog_day); + } else { + dialogBuilder.setIcon(R.drawable.save_dialog_night); + } break; - case StoragePermissionDialog.SAVE_AS_ARCHIVE: - dialogBuilder.setTitle(R.string.save_archive); + case SAVE_ABOUT_VERSION_TEXT: + // Set the title. + dialogBuilder.setTitle(R.string.save_text); + + // Set the icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + dialogBuilder.setIcon(R.drawable.save_text_blue_day); + } else { + dialogBuilder.setIcon(R.drawable.save_text_blue_night); + } break; - case StoragePermissionDialog.SAVE_AS_IMAGE: + case SAVE_ABOUT_VERSION_IMAGE: + // Set the title. dialogBuilder.setTitle(R.string.save_image); + + // Set the icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + dialogBuilder.setIcon(R.drawable.images_enabled_day); + } else { + dialogBuilder.setIcon(R.drawable.images_enabled_night); + } break; } @@ -184,20 +168,20 @@ public class SaveDialog extends DialogFragment { // Set the save button listener. dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> { // Return the dialog fragment to the parent activity. - saveWebpageListener.onSaveWebpage(saveType, this); + saveListener.onSave(saveType, this); }); // Create an alert dialog from the builder. AlertDialog alertDialog = dialogBuilder.create(); - // Remove the incorrect lint warning below that the window might be null. + // Remove the incorrect lint warning below that `getWindow()` might be null. assert alertDialog.getWindow() != null; // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -208,68 +192,18 @@ public class SaveDialog extends DialogFragment { alertDialog.show(); // Get handles for the layout items. - EditText urlEditText = alertDialog.findViewById(R.id.url_edittext); EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext); Button browseButton = alertDialog.findViewById(R.id.browse_button); - TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview); TextView fileExistsWarningTextView = alertDialog.findViewById(R.id.file_exists_warning_textview); TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview); Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - // Remove the incorrect warnings that the views might be null. - assert urlEditText != null; + // Remove the incorrect lint warnings below that the views might be null. assert fileNameEditText != null; assert browseButton != null; - assert fileSizeTextView != null; assert fileExistsWarningTextView != null; assert storagePermissionTextView != null; - // Set the file size text view. - fileSizeTextView.setText(fileSizeString); - - // Modify the layout based on the save type. - if (saveType == StoragePermissionDialog.SAVE_URL) { // A URL is being saved. - // Populate the URL edit text. This must be done before the text change listener is created below so that the file size isn't requested again. - urlEditText.setText(urlString); - - // Update the file size and the status of the save button when the URL changes. - urlEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - // Do nothing. - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - // Do nothing. - } - - @Override - public void afterTextChanged(Editable editable) { - // Cancel the get URL size AsyncTask if it is running. - if ((getUrlSize != null)) { - getUrlSize.cancel(true); - } - - // Get the current URL to save. - String urlToSave = urlEditText.getText().toString(); - - // Wipe the file size text view. - fileSizeTextView.setText(""); - - // Get the file size for the current URL. - getUrlSize = new GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave); - - // Enable the save button if the URL and file name are populated. - saveButton.setEnabled(!urlToSave.isEmpty() && !fileNameEditText.getText().toString().isEmpty()); - } - }); - } else { // An archive or an image is being saved. - // Hide the URL edit text and the file size text view. - urlEditText.setVisibility(View.GONE); - fileSizeTextView.setVisibility(View.GONE); - } - // Update the status of the save button when the file name changes. fileNameEditText.addTextChangedListener(new TextWatcher() { @Override @@ -299,52 +233,42 @@ public class SaveDialog extends DialogFragment { fileExistsWarningTextView.setVisibility(View.GONE); } - // Enable the save button based on the save type. - if (saveType == StoragePermissionDialog.SAVE_URL) { // A URL is being saved. - // Enable the save button if the file name and the URL is populated. - saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty()); - } else { // An archive or an image is being saved. - // Enable the save button if the file name is populated. - saveButton.setEnabled(!fileNameString.isEmpty()); - } + // Enable the save button if the file name is populated. + saveButton.setEnabled(!fileNameString.isEmpty()); } }); - // Create a file name string. - String fileName = ""; - // Set the file name according to the type. switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - // Use the file name from the content disposition. - fileName = contentDispositionFileNameString; + case SAVE_LOGCAT: + // Use a file name ending in `.txt`. + fileName = getString(R.string.privacy_browser_logcat_txt); break; - case StoragePermissionDialog.SAVE_AS_ARCHIVE: - // Use an archive name ending in `.mht`. - fileName = getString(R.string.webpage_mht); + case SAVE_ABOUT_VERSION_TEXT: + // Use a file name ending in `.txt`. + fileName = getString(R.string.privacy_browser_version_txt); break; - case StoragePermissionDialog.SAVE_AS_IMAGE: + case SAVE_ABOUT_VERSION_IMAGE: // Use a file name ending in `.png`. - fileName = getString(R.string.webpage_png); + fileName = getString(R.string.privacy_browser_version_png); break; } - // Save the file name as the default file name. This must be final to be used in the lambda below. - final String defaultFileName = fileName; - // Instantiate the download location helper. DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper(); // Get the default file path. - String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + defaultFileName; + String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + fileName; - // Populate the file name edit text. + // Display the default file path. fileNameEditText.setText(defaultFilePath); - // Move the cursor to the end of the default file path. - fileNameEditText.setSelection(defaultFilePath.length()); + // Hide the storage permission text view if the permission has already been granted. + if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + storagePermissionTextView.setVisibility(View.GONE); + } // Handle clicks on the browse button. browseButton.setOnClickListener((View view) -> { @@ -354,8 +278,8 @@ public class SaveDialog extends DialogFragment { // Set the intent MIME type to include all files so that everything is visible. browseIntent.setType("*/*"); - // Set the initial file name according to the type. - browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName); + // Set the initial file name. + browseIntent.putExtra(Intent.EXTRA_TITLE, fileName); // Set the initial directory if the minimum API >= 26. if (Build.VERSION.SDK_INT >= 26) { @@ -365,15 +289,10 @@ public class SaveDialog extends DialogFragment { // Request a file that can be opened. browseIntent.addCategory(Intent.CATEGORY_OPENABLE); - // Start the file picker. This must be started under `activity` so that the request code is returned correctly. - activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE); + // Launch the file picker. There is only one `startActivityForResult()`, so the request code is simply set to 0, but it must be run under `activity` so the request code is correct. + activity.startActivityForResult(browseIntent, 0); }); - // Hide the storage permission text view if the permission has already been granted. - if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { - storagePermissionTextView.setVisibility(View.GONE); - } - // Return the alert dialog. return alertDialog; } diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java deleted file mode 100644 index a3cf61bf..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright © 2016-2020 Soren Stoutner . - * - * This file is part of Privacy Browser . - * - * Privacy Browser is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privacy Browser is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Privacy Browser. If not, see . - */ - -package com.stoutner.privacybrowser.dialogs; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.preference.PreferenceManager; -import android.provider.DocumentsContract; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; - -import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.helpers.DownloadLocationHelper; - -import java.io.File; - -public class SaveLogcatDialog extends DialogFragment { - // Define the save logcat listener. - private SaveLogcatListener saveLogcatListener; - - // The public interface is used to send information back to the parent activity. - public interface SaveLogcatListener { - void onSaveLogcat(DialogFragment dialogFragment); - } - - @Override - public void onAttach(@NonNull Context context) { - // Run the default commands. - super.onAttach(context); - - // Get a handle for save logcat listener from the launching context. - saveLogcatListener = (SaveLogcatListener) context; - } - - // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog. - @SuppressLint("InflateParams") - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - // Get a handle for the activity and the context. - Activity activity = requireActivity(); - Context context = requireContext(); - - // Use an alert dialog builder to create the alert dialog. - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog); - // Set the title. - dialogBuilder.setTitle(R.string.save_logcat); - - // Get the current theme status. - int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - // Set the icon according to the theme. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) { - dialogBuilder.setIcon(R.drawable.save_dialog_night); - } else { - dialogBuilder.setIcon(R.drawable.save_dialog_day); - } - - // Set the view. The parent view is null because it will be assigned by the alert dialog. - dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_logcat_dialog, null)); - - // Set the cancel button listener. - dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> { - // Do nothing. The alert dialog will close automatically. - }); - - // Set the save button listener. - dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> { - // Return the dialog fragment to the parent activity. - saveLogcatListener.onSaveLogcat(this); - }); - - // Create an alert dialog from the builder. - AlertDialog alertDialog = dialogBuilder.create(); - - // Remove the incorrect lint warning below that `getWindow()` might be null. - assert alertDialog.getWindow() != null; - - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); - - // Disable screenshots if not allowed. - if (!allowScreenshots) { - alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); - } - - // The alert dialog must be shown before items in the layout can be modified. - alertDialog.show(); - - // Get handles for the layout items. - EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext); - Button browseButton = alertDialog.findViewById(R.id.browse_button); - TextView fileExistsWarningTextView = alertDialog.findViewById(R.id.file_exists_warning_textview); - TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview); - Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - - // Remove the incorrect lint warnings below that the views might be null. - assert fileNameEditText != null; - assert browseButton != null; - assert fileExistsWarningTextView != null; - assert storagePermissionTextView != null; - - // Update the status of the save button when the file name changes. - fileNameEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing. - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing. - } - - @Override - public void afterTextChanged(Editable s) { - // Get the current file name. - String fileNameString = fileNameEditText.getText().toString(); - - // Convert the file name string to a file. - File file = new File(fileNameString); - - // Check to see if the file exists. - if (file.exists()) { - // Show the file exists warning. - fileExistsWarningTextView.setVisibility(View.VISIBLE); - } else { - // Hide the file exists warning. - fileExistsWarningTextView.setVisibility(View.GONE); - } - - // Enable the save button if the file name is populated. - saveButton.setEnabled(!fileNameString.isEmpty()); - } - }); - - // Instantiate the download location helper. - DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper(); - - // Get the default file path. - String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + getString(R.string.privacy_browser_logcat_txt); - - // Display the default file path. - fileNameEditText.setText(defaultFilePath); - - // Hide the storage permission text view if the permission has already been granted. - if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { - storagePermissionTextView.setVisibility(View.GONE); - } - - // Handle clicks on the browse button. - browseButton.setOnClickListener((View view) -> { - // Create the file picker intent. - Intent browseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - - // Set the intent MIME type to include all files so that everything is visible. - browseIntent.setType("*/*"); - - // Set the initial file name. - browseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.privacy_browser_logcat_txt)); - - // Set the initial directory if the minimum API >= 26. - if (Build.VERSION.SDK_INT >= 26) { - browseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory()); - } - - // Request a file that can be opened. - browseIntent.addCategory(Intent.CATEGORY_OPENABLE); - - // Launch the file picker. There is only one `startActivityForResult()`, so the request code is simply set to 0, but it must be run under `activity` so the request code is correct. - activity.startActivityForResult(browseIntent, 0); - }); - - // Return the alert dialog. - return alertDialog; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java new file mode 100644 index 00000000..02576db0 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java @@ -0,0 +1,383 @@ +/* + * Copyright © 2019-2020 Soren Stoutner . + * + * This file is part of Privacy Browser . + * + * Privacy Browser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser. If not, see . + */ + +package com.stoutner.privacybrowser.dialogs; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.preference.PreferenceManager; + +import com.google.android.material.textfield.TextInputLayout; +import com.stoutner.privacybrowser.R; +import com.stoutner.privacybrowser.activities.MainWebViewActivity; +import com.stoutner.privacybrowser.asynctasks.GetUrlSize; +import com.stoutner.privacybrowser.helpers.DownloadLocationHelper; + +import java.io.File; + +public class SaveWebpageDialog extends DialogFragment { + // Define the save webpage listener. + private SaveWebpageListener saveWebpageListener; + + // The public interface is used to send information back to the parent activity. + public interface SaveWebpageListener { + void onSaveWebpage(int saveType, DialogFragment dialogFragment); + } + + // Define the get URL size AsyncTask. This allows previous instances of the task to be cancelled if a new one is run. + private AsyncTask getUrlSize; + + @Override + public void onAttach(@NonNull Context context) { + // Run the default commands. + super.onAttach(context); + + // Get a handle for the save webpage listener from the launching context. + saveWebpageListener = (SaveWebpageListener) context; + } + + public static SaveWebpageDialog saveWebpage(int saveType, String urlString, String fileSizeString, String contentDispositionFileNameString, String userAgentString, boolean cookiesEnabled) { + // Create an arguments bundle. + Bundle argumentsBundle = new Bundle(); + + // Store the arguments in the bundle. + argumentsBundle.putInt("save_type", saveType); + argumentsBundle.putString("url_string", urlString); + argumentsBundle.putString("file_size_string", fileSizeString); + argumentsBundle.putString("content_disposition_file_name_string", contentDispositionFileNameString); + argumentsBundle.putString("user_agent_string", userAgentString); + argumentsBundle.putBoolean("cookies_enabled", cookiesEnabled); + + // Create a new instance of the save webpage dialog. + SaveWebpageDialog saveWebpageDialog = new SaveWebpageDialog(); + + // Add the arguments bundle to the new dialog. + saveWebpageDialog.setArguments(argumentsBundle); + + // Return the new dialog. + return saveWebpageDialog; + } + + // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog. + @SuppressLint("InflateParams") + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Get a handle for the arguments. + Bundle arguments = getArguments(); + + // Remove the incorrect lint warning that the arguments might be null. + assert arguments != null; + + // Get the arguments from the bundle. + int saveType = arguments.getInt("save_type"); + String urlString = arguments.getString("url_string"); + String fileSizeString = arguments.getString("file_size_string"); + String contentDispositionFileNameString = arguments.getString("content_disposition_file_name_string"); + String userAgentString = arguments.getString("user_agent_string"); + boolean cookiesEnabled = arguments.getBoolean("cookies_enabled"); + + // Get a handle for the activity and the context. + Activity activity = requireActivity(); + Context context = requireContext(); + + // Use an alert dialog builder to create the alert dialog. + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog); + + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Set the icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) { // The night theme is enabled. + // Set the icon according to the save type. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + dialogBuilder.setIcon(R.drawable.copy_enabled_night); + break; + + case StoragePermissionDialog.SAVE_ARCHIVE: + dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night); + break; + + case StoragePermissionDialog.SAVE_IMAGE: + dialogBuilder.setIcon(R.drawable.images_enabled_night); + break; + } + } else { // The day theme is enabled. + // Set the icon according to the save type. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + dialogBuilder.setIcon(R.drawable.copy_enabled_day); + break; + + case StoragePermissionDialog.SAVE_ARCHIVE: + dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day); + break; + + case StoragePermissionDialog.SAVE_IMAGE: + dialogBuilder.setIcon(R.drawable.images_enabled_day); + break; + } + } + + // Set the title according to the type. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + dialogBuilder.setTitle(R.string.save); + break; + + case StoragePermissionDialog.SAVE_ARCHIVE: + dialogBuilder.setTitle(R.string.save_archive); + break; + + case StoragePermissionDialog.SAVE_IMAGE: + dialogBuilder.setTitle(R.string.save_image); + break; + } + + // Set the view. The parent view is null because it will be assigned by the alert dialog. + dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_url_dialog, null)); + + // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else. + dialogBuilder.setNegativeButton(R.string.cancel, null); + + // Set the save button listener. + dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> { + // Return the dialog fragment to the parent activity. + saveWebpageListener.onSaveWebpage(saveType, this); + }); + + // Create an alert dialog from the builder. + AlertDialog alertDialog = dialogBuilder.create(); + + // Remove the incorrect lint warning below that the window might be null. + assert alertDialog.getWindow() != null; + + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + + // Get the screenshot preference. + boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + + // Disable screenshots if not allowed. + if (!allowScreenshots) { + alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + } + + // The alert dialog must be shown before items in the layout can be modified. + alertDialog.show(); + + // Get handles for the layout items. + TextInputLayout urlTextInputLayout = alertDialog.findViewById(R.id.url_textinputlayout); + EditText urlEditText = alertDialog.findViewById(R.id.url_edittext); + EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext); + Button browseButton = alertDialog.findViewById(R.id.browse_button); + TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview); + TextView fileExistsWarningTextView = alertDialog.findViewById(R.id.file_exists_warning_textview); + TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview); + Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + + // Remove the incorrect warnings that the views might be null. + assert urlTextInputLayout != null; + assert urlEditText != null; + assert fileNameEditText != null; + assert browseButton != null; + assert fileSizeTextView != null; + assert fileExistsWarningTextView != null; + assert storagePermissionTextView != null; + + // Set the file size text view. + fileSizeTextView.setText(fileSizeString); + + // Modify the layout based on the save type. + if (saveType == StoragePermissionDialog.SAVE_URL) { // A URL is being saved. + // Populate the URL edit text. This must be done before the text change listener is created below so that the file size isn't requested again. + urlEditText.setText(urlString); + + // Update the file size and the status of the save button when the URL changes. + urlEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + // Do nothing. + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + // Do nothing. + } + + @Override + public void afterTextChanged(Editable editable) { + // Cancel the get URL size AsyncTask if it is running. + if ((getUrlSize != null)) { + getUrlSize.cancel(true); + } + + // Get the current URL to save. + String urlToSave = urlEditText.getText().toString(); + + // Wipe the file size text view. + fileSizeTextView.setText(""); + + // Get the file size for the current URL. + getUrlSize = new GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave); + + // Enable the save button if the URL and file name are populated. + saveButton.setEnabled(!urlToSave.isEmpty() && !fileNameEditText.getText().toString().isEmpty()); + } + }); + } else { // An archive or an image is being saved. + // Hide the URL edit text and the file size text view. + urlTextInputLayout.setVisibility(View.GONE); + fileSizeTextView.setVisibility(View.GONE); + } + + // Update the status of the save button when the file name changes. + fileNameEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing. + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing. + } + + @Override + public void afterTextChanged(Editable s) { + // Get the current file name. + String fileNameString = fileNameEditText.getText().toString(); + + // Convert the file name string to a file. + File file = new File(fileNameString); + + // Check to see if the file exists. + if (file.exists()) { + // Show the file exists warning. + fileExistsWarningTextView.setVisibility(View.VISIBLE); + } else { + // Hide the file exists warning. + fileExistsWarningTextView.setVisibility(View.GONE); + } + + // Enable the save button based on the save type. + if (saveType == StoragePermissionDialog.SAVE_URL) { // A URL is being saved. + // Enable the save button if the file name and the URL is populated. + saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty()); + } else { // An archive or an image is being saved. + // Enable the save button if the file name is populated. + saveButton.setEnabled(!fileNameString.isEmpty()); + } + } + }); + + // Create a file name string. + String fileName = ""; + + // Set the file name according to the type. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + // Use the file name from the content disposition. + fileName = contentDispositionFileNameString; + break; + + case StoragePermissionDialog.SAVE_ARCHIVE: + // Use an archive name ending in `.mht`. + fileName = getString(R.string.webpage_mht); + break; + + case StoragePermissionDialog.SAVE_IMAGE: + // Use a file name ending in `.png`. + fileName = getString(R.string.webpage_png); + break; + } + + // Save the file name as the default file name. This must be final to be used in the lambda below. + final String defaultFileName = fileName; + + // Instantiate the download location helper. + DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper(); + + // Get the default file path. + String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + defaultFileName; + + // Populate the file name edit text. + fileNameEditText.setText(defaultFilePath); + + // Move the cursor to the end of the default file path. + fileNameEditText.setSelection(defaultFilePath.length()); + + // Handle clicks on the browse button. + browseButton.setOnClickListener((View view) -> { + // Create the file picker intent. + Intent browseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + + // Set the intent MIME type to include all files so that everything is visible. + browseIntent.setType("*/*"); + + // Set the initial file name according to the type. + browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName); + + // Set the initial directory if the minimum API >= 26. + if (Build.VERSION.SDK_INT >= 26) { + browseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory()); + } + + // Request a file that can be opened. + browseIntent.addCategory(Intent.CATEGORY_OPENABLE); + + // Start the file picker. This must be started under `activity` so that the request code is returned correctly. + activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE); + }); + + // Hide the storage permission text view if the permission has already been granted. + if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + storagePermissionTextView.setVisibility(View.GONE); + } + + // Return the alert dialog. + return alertDialog; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java index 472a5184..e1d0c409 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java @@ -38,8 +38,9 @@ public class StoragePermissionDialog extends DialogFragment { // Define the save type constants. public static final int OPEN = 0; public static final int SAVE_URL = 1; - public static final int SAVE_AS_ARCHIVE = 2; - public static final int SAVE_AS_IMAGE = 3; + public static final int SAVE_ARCHIVE = 2; + public static final int SAVE_TEXT = 3; + public static final int SAVE_IMAGE = 4; // The listener is used in `onAttach()` and `onCreateDialog()`. private StoragePermissionDialogListener storagePermissionDialogListener; diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutTabFragment.java b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutTabFragment.java deleted file mode 100644 index 9d8a2f1c..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutTabFragment.java +++ /dev/null @@ -1,663 +0,0 @@ -/* - * Copyright © 2016-2020 Soren Stoutner . - * - * This file is part of Privacy Browser . - * - * Privacy Browser is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privacy Browser is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Privacy Browser. If not, see . - */ - -package com.stoutner.privacybrowser.fragments; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ActivityManager; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.ForegroundColorSpan; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.webkit.WebViewCompat; - -import com.stoutner.privacybrowser.BuildConfig; -import com.stoutner.privacybrowser.R; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.math.BigInteger; -import java.security.Principal; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.text.DateFormat; -import java.text.NumberFormat; -import java.util.Date; - -public class AboutTabFragment extends Fragment { - // Declare the class constants. - final static String TAB_NUMBER = "tab_number"; - final static String BLOCKLIST_VERSIONS = "blocklist_versions"; - final long MEBIBYTE = 1048576; - - // Declare the class variables. - private boolean updateMemoryUsageBoolean = true; - private int tabNumber; - private String[] blocklistVersions; - private View tabLayout; - private String appConsumedMemoryLabel; - private String appAvailableMemoryLabel; - private String appTotalMemoryLabel; - private String appMaximumMemoryLabel; - private String systemConsumedMemoryLabel; - private String systemAvailableMemoryLabel; - private String systemTotalMemoryLabel; - private Runtime runtime; - private ActivityManager activityManager; - private ActivityManager.MemoryInfo memoryInfo; - private NumberFormat numberFormat; - private ForegroundColorSpan blueColorSpan; - - // Declare the class views. - private TextView appConsumedMemoryTextView; - private TextView appAvailableMemoryTextView; - private TextView appTotalMemoryTextView; - private TextView appMaximumMemoryTextView; - private TextView systemConsumedMemoryTextView; - private TextView systemAvailableMemoryTextView; - private TextView systemTotalMemoryTextView; - - public static AboutTabFragment createTab(int tabNumber, String[] blocklistVersions) { - // Create a bundle. - Bundle argumentsBundle = new Bundle(); - - // Store the tab number in the bundle. - argumentsBundle.putInt(TAB_NUMBER, tabNumber); - argumentsBundle.putStringArray(BLOCKLIST_VERSIONS, blocklistVersions); - - // Create a new instance of the tab fragment. - AboutTabFragment aboutTabFragment = new AboutTabFragment(); - - // Add the arguments bundle to the fragment. - aboutTabFragment.setArguments(argumentsBundle); - - // Return the new fragment. - return aboutTabFragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - // Run the default commands. - super.onCreate(savedInstanceState); - - // Get a handle for the arguments. - Bundle arguments = getArguments(); - - // Remove the incorrect lint warning below that arguments might be null. - assert arguments != null; - - // Store the arguments in class variables. - tabNumber = getArguments().getInt(TAB_NUMBER); - blocklistVersions = getArguments().getStringArray(BLOCKLIST_VERSIONS); - } - - @Override - public View onCreateView(@NonNull LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) { - // Get a handle for the context and assert that it isn't null. - Context context = getContext(); - assert context != null; - - // Get the current theme status. - int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - // Load the tabs. Tab numbers start at 0. - if (tabNumber == 0) { // Load the about tab. - // Setting false at the end of inflater.inflate does not attach the inflated layout as a child of container. The fragment will take care of attaching the root automatically. - tabLayout = layoutInflater.inflate(R.layout.about_tab_version, container, false); - - // Get handles for the text views. - TextView versionTextView = tabLayout.findViewById(R.id.version); - TextView brandTextView = tabLayout.findViewById(R.id.brand); - TextView manufacturerTextView = tabLayout.findViewById(R.id.manufacturer); - TextView modelTextView = tabLayout.findViewById(R.id.model); - TextView deviceTextView = tabLayout.findViewById(R.id.device); - TextView bootloaderTextView = tabLayout.findViewById(R.id.bootloader); - TextView radioTextView = tabLayout.findViewById(R.id.radio); - TextView androidTextView = tabLayout.findViewById(R.id.android); - TextView securityPatchTextView = tabLayout.findViewById(R.id.security_patch); - TextView buildTextView = tabLayout.findViewById(R.id.build); - TextView webViewProviderTextView = tabLayout.findViewById(R.id.webview_provider); - TextView webViewVersionTextView = tabLayout.findViewById(R.id.webview_version); - TextView orbotTextView = tabLayout.findViewById(R.id.orbot); - TextView i2pTextView = tabLayout.findViewById(R.id.i2p); - TextView openKeychainTextView = tabLayout.findViewById(R.id.open_keychain); - appConsumedMemoryTextView = tabLayout.findViewById(R.id.app_consumed_memory); - appAvailableMemoryTextView = tabLayout.findViewById(R.id.app_available_memory); - appTotalMemoryTextView = tabLayout.findViewById(R.id.app_total_memory); - appMaximumMemoryTextView = tabLayout.findViewById(R.id.app_maximum_memory); - systemConsumedMemoryTextView = tabLayout.findViewById(R.id.system_consumed_memory); - systemAvailableMemoryTextView = tabLayout.findViewById(R.id.system_available_memory); - systemTotalMemoryTextView = tabLayout.findViewById(R.id.system_total_memory); - TextView easyListTextView = tabLayout.findViewById(R.id.easylist); - TextView easyPrivacyTextView = tabLayout.findViewById(R.id.easyprivacy); - TextView fanboyAnnoyanceTextView = tabLayout.findViewById(R.id.fanboy_annoyance); - TextView fanboySocialTextView = tabLayout.findViewById(R.id.fanboy_social); - TextView ultraListTextView = tabLayout.findViewById(R.id.ultralist); - TextView ultraPrivacyTextView = tabLayout.findViewById(R.id.ultraprivacy); - TextView certificateIssuerDNTextView = tabLayout.findViewById(R.id.certificate_issuer_dn); - TextView certificateSubjectDNTextView = tabLayout.findViewById(R.id.certificate_subject_dn); - TextView certificateStartDateTextView = tabLayout.findViewById(R.id.certificate_start_date); - TextView certificateEndDateTextView = tabLayout.findViewById(R.id.certificate_end_date); - TextView certificateVersionTextView = tabLayout.findViewById(R.id.certificate_version); - TextView certificateSerialNumberTextView = tabLayout.findViewById(R.id.certificate_serial_number); - TextView certificateSignatureAlgorithmTextView = tabLayout.findViewById(R.id.certificate_signature_algorithm); - - // Setup the labels. - String version = getString(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + getString(R.string.version_code) + " " + BuildConfig.VERSION_CODE + ")"; - String brandLabel = getString(R.string.brand) + " "; - String manufacturerLabel = getString(R.string.manufacturer) + " "; - String modelLabel = getString(R.string.model) + " "; - String deviceLabel = getString(R.string.device) + " "; - String bootloaderLabel = getString(R.string.bootloader) + " "; - String androidLabel = getString(R.string.android) + " "; - String buildLabel = getString(R.string.build) + " "; - String webViewVersionLabel = getString(R.string.webview_version) + " "; - appConsumedMemoryLabel = getString(R.string.app_consumed_memory) + " "; - appAvailableMemoryLabel = getString(R.string.app_available_memory) + " "; - appTotalMemoryLabel = getString(R.string.app_total_memory) + " "; - appMaximumMemoryLabel = getString(R.string.app_maximum_memory) + " "; - systemConsumedMemoryLabel = getString(R.string.system_consumed_memory) + " "; - systemAvailableMemoryLabel = getString(R.string.system_available_memory) + " "; - systemTotalMemoryLabel = getString(R.string.system_total_memory) + " "; - String easyListLabel = getString(R.string.easylist_label) + " "; - String easyPrivacyLabel = getString(R.string.easyprivacy_label) + " "; - String fanboyAnnoyanceLabel = getString(R.string.fanboy_annoyance_label) + " "; - String fanboySocialLabel = getString(R.string.fanboy_social_label) + " "; - String ultraListLabel = getString(R.string.ultralist_label) + " "; - String ultraPrivacyLabel = getString(R.string.ultraprivacy_label) + " "; - String issuerDNLabel = getString(R.string.issuer_dn) + " "; - String subjectDNLabel = getString(R.string.subject_dn) + " "; - String startDateLabel = getString(R.string.start_date) + " "; - String endDateLabel = getString(R.string.end_date) + " "; - String certificateVersionLabel = getString(R.string.certificate_version) + " "; - String serialNumberLabel = getString(R.string.serial_number) + " "; - String signatureAlgorithmLabel = getString(R.string.signature_algorithm) + " "; - - // The WebView layout is only used to get the default user agent from `bare_webview`. It is not used to render content on the screen. - // Once the minimum API >= 26 this can be accomplished with the WebView package info. - View webViewLayout = layoutInflater.inflate(R.layout.bare_webview, container, false); - WebView tabLayoutWebView = webViewLayout.findViewById(R.id.bare_webview); - String userAgentString = tabLayoutWebView.getSettings().getUserAgentString(); - - // Get the device's information and store it in strings. - String brand = Build.BRAND; - String manufacturer = Build.MANUFACTURER; - String model = Build.MODEL; - String device = Build.DEVICE; - String bootloader = Build.BOOTLOADER; - String radio = Build.getRadioVersion(); - String android = Build.VERSION.RELEASE + " (" + getString(R.string.api) + " " + Build.VERSION.SDK_INT + ")"; - String build = Build.DISPLAY; - // Select the substring that begins after `Chrome/` and goes until the next ` `. - String webView = userAgentString.substring(userAgentString.indexOf("Chrome/") + 7, userAgentString.indexOf(" ", userAgentString.indexOf("Chrome/"))); - - // Get the Orbot version name if Orbot is installed. - String orbot; - try { - // Store the version name. - orbot = context.getPackageManager().getPackageInfo("org.torproject.android", 0).versionName; - } catch (PackageManager.NameNotFoundException exception) { // Orbot is not installed. - orbot = ""; - } - - // Get the I2P version name if I2P is installed. - String i2p; - try { - // Store the version name. - i2p = context.getPackageManager().getPackageInfo("net.i2p.android.router", 0).versionName; - } catch (PackageManager.NameNotFoundException exception) { // I2P is not installed. - i2p = ""; - } - - // Get the OpenKeychain version name if it is installed. - String openKeychain; - try { - // Store the version name. - openKeychain = context.getPackageManager().getPackageInfo("org.sufficientlysecure.keychain", 0).versionName; - } catch (PackageManager.NameNotFoundException exception) { // OpenKeychain is not installed. - openKeychain = ""; - } - - // Create a spannable string builder for the hardware and software text views that needs multiple colors of text. - SpannableStringBuilder brandStringBuilder = new SpannableStringBuilder(brandLabel + brand); - SpannableStringBuilder manufacturerStringBuilder = new SpannableStringBuilder(manufacturerLabel + manufacturer); - SpannableStringBuilder modelStringBuilder = new SpannableStringBuilder(modelLabel + model); - SpannableStringBuilder deviceStringBuilder = new SpannableStringBuilder(deviceLabel + device); - SpannableStringBuilder bootloaderStringBuilder = new SpannableStringBuilder(bootloaderLabel + bootloader); - SpannableStringBuilder androidStringBuilder = new SpannableStringBuilder(androidLabel + android); - SpannableStringBuilder buildStringBuilder = new SpannableStringBuilder(buildLabel + build); - SpannableStringBuilder webViewVersionStringBuilder = new SpannableStringBuilder(webViewVersionLabel + webView); - SpannableStringBuilder easyListStringBuilder = new SpannableStringBuilder(easyListLabel + blocklistVersions[0]); - SpannableStringBuilder easyPrivacyStringBuilder = new SpannableStringBuilder(easyPrivacyLabel + blocklistVersions[1]); - SpannableStringBuilder fanboyAnnoyanceStringBuilder = new SpannableStringBuilder(fanboyAnnoyanceLabel + blocklistVersions[2]); - SpannableStringBuilder fanboySocialStringBuilder = new SpannableStringBuilder(fanboySocialLabel + blocklistVersions[3]); - SpannableStringBuilder ultraListStringBuilder = new SpannableStringBuilder(ultraListLabel + blocklistVersions[4]); - SpannableStringBuilder ultraPrivacyStringBuilder = new SpannableStringBuilder(ultraPrivacyLabel + blocklistVersions[5]); - - // Set the blue color span according to the theme. The deprecated `getResources()` must be used until the minimum API >= 23. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { - blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700)); - } else { - blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.violet_500)); - } - - // Setup the spans to display the device information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. - brandStringBuilder.setSpan(blueColorSpan, brandLabel.length(), brandStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - manufacturerStringBuilder.setSpan(blueColorSpan, manufacturerLabel.length(), manufacturerStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - modelStringBuilder.setSpan(blueColorSpan, modelLabel.length(), modelStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - deviceStringBuilder.setSpan(blueColorSpan, deviceLabel.length(), deviceStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - bootloaderStringBuilder.setSpan(blueColorSpan, bootloaderLabel.length(), bootloaderStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - androidStringBuilder.setSpan(blueColorSpan, androidLabel.length(), androidStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - buildStringBuilder.setSpan(blueColorSpan, buildLabel.length(), buildStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - webViewVersionStringBuilder.setSpan(blueColorSpan, webViewVersionLabel.length(), webViewVersionStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - easyListStringBuilder.setSpan(blueColorSpan, easyListLabel.length(), easyListStringBuilder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - easyPrivacyStringBuilder.setSpan(blueColorSpan, easyPrivacyLabel.length(), easyPrivacyStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - fanboyAnnoyanceStringBuilder.setSpan(blueColorSpan, fanboyAnnoyanceLabel.length(), fanboyAnnoyanceStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - fanboySocialStringBuilder.setSpan(blueColorSpan, fanboySocialLabel.length(), fanboySocialStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - ultraListStringBuilder.setSpan(blueColorSpan, ultraListLabel.length(), ultraListStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - ultraPrivacyStringBuilder.setSpan(blueColorSpan, ultraPrivacyLabel.length(), ultraPrivacyStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - // Display the strings in the text boxes. - versionTextView.setText(version); - brandTextView.setText(brandStringBuilder); - manufacturerTextView.setText(manufacturerStringBuilder); - modelTextView.setText(modelStringBuilder); - deviceTextView.setText(deviceStringBuilder); - bootloaderTextView.setText(bootloaderStringBuilder); - androidTextView.setText(androidStringBuilder); - buildTextView.setText(buildStringBuilder); - webViewVersionTextView.setText(webViewVersionStringBuilder); - easyListTextView.setText(easyListStringBuilder); - easyPrivacyTextView.setText(easyPrivacyStringBuilder); - fanboyAnnoyanceTextView.setText(fanboyAnnoyanceStringBuilder); - fanboySocialTextView.setText(fanboySocialStringBuilder); - ultraListTextView.setText(ultraListStringBuilder); - ultraPrivacyTextView.setText(ultraPrivacyStringBuilder); - - // Only populate the radio text view if there is a radio in the device. - if (!radio.isEmpty()) { - String radioLabel = getString(R.string.radio) + " "; - SpannableStringBuilder radioStringBuilder = new SpannableStringBuilder(radioLabel + radio); - radioStringBuilder.setSpan(blueColorSpan, radioLabel.length(), radioStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - radioTextView.setText(radioStringBuilder); - } else { // This device does not have a radio. - radioTextView.setVisibility(View.GONE); - } - - // Build.VERSION.SECURITY_PATCH is only available for SDK_INT >= 23. - if (Build.VERSION.SDK_INT >= 23) { - String securityPatchLabel = getString(R.string.security_patch) + " "; - String securityPatch = Build.VERSION.SECURITY_PATCH; - SpannableStringBuilder securityPatchStringBuilder = new SpannableStringBuilder(securityPatchLabel + securityPatch); - securityPatchStringBuilder.setSpan(blueColorSpan, securityPatchLabel.length(), securityPatchStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - securityPatchTextView.setText(securityPatchStringBuilder); - } else { // The API < 23. - // Hide the security patch text view. - securityPatchTextView.setVisibility(View.GONE); - } - - // Only populate the WebView provider if the SDK >= 21. - if (Build.VERSION.SDK_INT >= 21) { - // Create the WebView provider label. - String webViewProviderLabel = getString(R.string.webview_provider) + " "; - - // Get the current WebView package info. - PackageInfo webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(context); - - // Remove the warning below that the package info might be null. - assert webViewPackageInfo != null; - - // Get the WebView provider name. - String webViewPackageName = webViewPackageInfo.packageName; - - // Create the spannable string builder. - SpannableStringBuilder webViewProviderStringBuilder = new SpannableStringBuilder(webViewProviderLabel + webViewPackageName); - - // Apply the coloration. - webViewProviderStringBuilder.setSpan(blueColorSpan, webViewProviderLabel.length(), webViewProviderStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - // Display the WebView provider. - webViewProviderTextView.setText(webViewProviderStringBuilder); - } else { // The API < 21. - // Hide the WebView provider text view. - webViewProviderTextView.setVisibility(View.GONE); - } - - // Only populate the Orbot text view if it is installed. - if (!orbot.isEmpty()) { - String orbotLabel = getString(R.string.orbot) + " "; - SpannableStringBuilder orbotStringBuilder = new SpannableStringBuilder(orbotLabel + orbot); - orbotStringBuilder.setSpan(blueColorSpan, orbotLabel.length(), orbotStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - orbotTextView.setText(orbotStringBuilder); - } else { // Orbot is not installed. - orbotTextView.setVisibility(View.GONE); - } - - // Only populate the I2P text view if it is installed. - if (!i2p.isEmpty()) { - String i2pLabel = getString(R.string.i2p) + " "; - SpannableStringBuilder i2pStringBuilder = new SpannableStringBuilder(i2pLabel + i2p); - i2pStringBuilder.setSpan(blueColorSpan, i2pLabel.length(), i2pStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - i2pTextView.setText(i2pStringBuilder); - } else { // I2P is not installed. - i2pTextView.setVisibility(View.GONE); - } - - // Only populate the OpenKeychain text view if it is installed. - if (!openKeychain.isEmpty()) { - String openKeychainLabel = getString(R.string.openkeychain) + " "; - SpannableStringBuilder openKeychainStringBuilder = new SpannableStringBuilder(openKeychainLabel + openKeychain); - openKeychainStringBuilder.setSpan(blueColorSpan, openKeychainLabel.length(), openKeychainStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - openKeychainTextView.setText(openKeychainStringBuilder); - } else { //OpenKeychain is not installed. - openKeychainTextView.setVisibility(View.GONE); - } - - // Display the package signature. - try { - // Get the first package signature. Suppress the lint warning about the need to be careful in implementing comparison of certificates for security purposes. - @SuppressLint("PackageManagerGetSignatures") Signature packageSignature = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), - PackageManager.GET_SIGNATURES).signatures[0]; - - // Convert the signature to a byte array input stream. - InputStream certificateByteArrayInputStream = new ByteArrayInputStream(packageSignature.toByteArray()); - - // Display the certificate information on the screen. - try { - // Instantiate a `CertificateFactory`. - CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); - - // Generate an `X509Certificate`. - X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(certificateByteArrayInputStream); - - // Store the individual sections of the certificate that we are interested in. - Principal issuerDNPrincipal = x509Certificate.getIssuerDN(); - Principal subjectDNPrincipal = x509Certificate.getSubjectDN(); - Date startDate = x509Certificate.getNotBefore(); - Date endDate = x509Certificate.getNotAfter(); - int certificateVersion = x509Certificate.getVersion(); - BigInteger serialNumberBigInteger = x509Certificate.getSerialNumber(); - String signatureAlgorithmNameString = x509Certificate.getSigAlgName(); - - // Create a `SpannableStringBuilder` for each `TextView` that needs multiple colors of text. - SpannableStringBuilder issuerDNStringBuilder = new SpannableStringBuilder(issuerDNLabel + issuerDNPrincipal.toString()); - SpannableStringBuilder subjectDNStringBuilder = new SpannableStringBuilder(subjectDNLabel + subjectDNPrincipal.toString()); - SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate)); - SpannableStringBuilder endDataStringBuilder = new SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate)); - SpannableStringBuilder certificateVersionStringBuilder = new SpannableStringBuilder(certificateVersionLabel + certificateVersion); - SpannableStringBuilder serialNumberStringBuilder = new SpannableStringBuilder(serialNumberLabel + serialNumberBigInteger); - SpannableStringBuilder signatureAlgorithmStringBuilder = new SpannableStringBuilder(signatureAlgorithmLabel + signatureAlgorithmNameString); - - // Setup the spans to display the device information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. - issuerDNStringBuilder.setSpan(blueColorSpan, issuerDNLabel.length(), issuerDNStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - subjectDNStringBuilder.setSpan(blueColorSpan, subjectDNLabel.length(), subjectDNStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - endDataStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDataStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - certificateVersionStringBuilder.setSpan(blueColorSpan, certificateVersionLabel.length(), certificateVersionStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - serialNumberStringBuilder.setSpan(blueColorSpan, serialNumberLabel.length(), serialNumberStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - signatureAlgorithmStringBuilder.setSpan(blueColorSpan, signatureAlgorithmLabel.length(), signatureAlgorithmStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - // Display the strings in the text boxes. - certificateIssuerDNTextView.setText(issuerDNStringBuilder); - certificateSubjectDNTextView.setText(subjectDNStringBuilder); - certificateStartDateTextView.setText(startDateStringBuilder); - certificateEndDateTextView.setText(endDataStringBuilder); - certificateVersionTextView.setText(certificateVersionStringBuilder); - certificateSerialNumberTextView.setText(serialNumberStringBuilder); - certificateSignatureAlgorithmTextView.setText(signatureAlgorithmStringBuilder); - } catch (CertificateException e) { - // Do nothing if there is a certificate error. - } - - // Get a handle for the runtime. - runtime = Runtime.getRuntime(); - - // Get a handle for the activity. - Activity activity = getActivity(); - - // Remove the incorrect lint warning below that the activity might be null. - assert activity != null; - - // Get a handle for the activity manager. - activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); - - // Remove the incorrect lint warning below that the activity manager might be null. - assert activityManager != null; - - // Instantiate a memory info variable. - memoryInfo = new ActivityManager.MemoryInfo(); - - // Define a number format. - numberFormat = NumberFormat.getInstance(); - - // Set the minimum and maximum number of fraction digits. - numberFormat.setMinimumFractionDigits(2); - numberFormat.setMaximumFractionDigits(2); - - // Update the memory usage. - updateMemoryUsage(getActivity()); - } catch (PackageManager.NameNotFoundException e) { - // Do nothing if `PackageManager` says Privacy Browser isn't installed. - } - } else { // load a WebView for all the other tabs. Tab numbers start at 0. - // Setting false at the end of inflater.inflate does not attach the inflated layout as a child of container. The fragment will take care of attaching the root automatically. - tabLayout = layoutInflater.inflate(R.layout.bare_webview, container, false); - - // Get a handle for `tabWebView`. - WebView tabWebView = (WebView) tabLayout; - - // Load the tabs according to the theme. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) { // The dark theme is applied. - // Set the background color. The deprecated `.getColor()` must be used until the minimum API >= 23. - tabWebView.setBackgroundColor(getResources().getColor(R.color.gray_850)); - - switch (tabNumber) { - case 1: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_permissions_dark.html"); - break; - - case 2: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_privacy_policy_dark.html"); - break; - - case 3: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_changelog_dark.html"); - break; - - case 4: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_licenses_dark.html"); - break; - - case 5: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_contributors_dark.html"); - break; - - case 6: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_links_dark.html"); - break; - } - } else { // The light theme is applied. - switch (tabNumber) { - case 1: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_permissions_light.html"); - break; - - case 2: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_privacy_policy_light.html"); - break; - - case 3: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_changelog_light.html"); - break; - - case 4: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_licenses_light.html"); - break; - - case 5: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_contributors_light.html"); - break; - - case 6: - tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_links_light.html"); - break; - } - } - } - - // Scroll the tab if the saved instance state is not null. - if (savedInstanceState != null) { - tabLayout.post(() -> { - tabLayout.setScrollX(savedInstanceState.getInt("scroll_x")); - tabLayout.setScrollY(savedInstanceState.getInt("scroll_y")); - }); - } - - // Return the formatted `tabLayout`. - return tabLayout; - } - - @Override - public void onPause() { - // Run the default commands. - super.onPause(); - - // Pause the updating of the memory usage. - updateMemoryUsageBoolean = false; - } - - @Override - public void onResume() { - // Run the default commands. - super.onResume(); - - // Resume the updating of the memory usage. - updateMemoryUsageBoolean = true; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { - // Run the default commands. - super.onSaveInstanceState(savedInstanceState); - - // Save the scroll positions if the tab layout is not null, which can happen if a tab is not currently selected. - if (tabLayout != null) { - savedInstanceState.putInt("scroll_x", tabLayout.getScrollX()); - savedInstanceState.putInt("scroll_y", tabLayout.getScrollY()); - } - } - - public void updateMemoryUsage(Activity activity) { - try { - // Update the memory usage if enabled. - if (updateMemoryUsageBoolean) { - // Populate the memory info variable. - activityManager.getMemoryInfo(memoryInfo); - - // Get the app memory information. - long appAvailableMemoryLong = runtime.freeMemory(); - long appTotalMemoryLong = runtime.totalMemory(); - long appMaximumMemoryLong = runtime.maxMemory(); - - // Calculate the app consumed memory. - long appConsumedMemoryLong = appTotalMemoryLong - appAvailableMemoryLong; - - // Get the system memory information. - long systemTotalMemoryLong = memoryInfo.totalMem; - long systemAvailableMemoryLong = memoryInfo.availMem; - - // Calculate the system consumed memory. - long systemConsumedMemoryLong = systemTotalMemoryLong - systemAvailableMemoryLong; - - // Convert the memory information into mebibytes. - float appConsumedMemoryFloat = (float) appConsumedMemoryLong / MEBIBYTE; - float appAvailableMemoryFloat = (float) appAvailableMemoryLong / MEBIBYTE; - float appTotalMemoryFloat = (float) appTotalMemoryLong / MEBIBYTE; - float appMaximumMemoryFloat = (float) appMaximumMemoryLong / MEBIBYTE; - float systemConsumedMemoryFloat = (float) systemConsumedMemoryLong / MEBIBYTE; - float systemAvailableMemoryFloat = (float) systemAvailableMemoryLong / MEBIBYTE; - float systemTotalMemoryFloat = (float) systemTotalMemoryLong / MEBIBYTE; - - // Get the mebibyte string. - String mebibyte = getString(R.string.mebibyte); - - // Calculate the mebibyte length. - int mebibyteLength = mebibyte.length(); - - // Create spannable string builders. - SpannableStringBuilder appConsumedMemoryStringBuilder = new SpannableStringBuilder(appConsumedMemoryLabel + numberFormat.format(appConsumedMemoryFloat) + " " + mebibyte); - SpannableStringBuilder appAvailableMemoryStringBuilder = new SpannableStringBuilder(appAvailableMemoryLabel + numberFormat.format(appAvailableMemoryFloat) + " " + mebibyte); - SpannableStringBuilder appTotalMemoryStringBuilder = new SpannableStringBuilder(appTotalMemoryLabel + numberFormat.format(appTotalMemoryFloat) + " " + mebibyte); - SpannableStringBuilder appMaximumMemoryStringBuilder = new SpannableStringBuilder(appMaximumMemoryLabel + numberFormat.format(appMaximumMemoryFloat) + " " + mebibyte); - SpannableStringBuilder systemConsumedMemoryStringBuilder = new SpannableStringBuilder(systemConsumedMemoryLabel + numberFormat.format(systemConsumedMemoryFloat) + " " + mebibyte); - SpannableStringBuilder systemAvailableMemoryStringBuilder = new SpannableStringBuilder(systemAvailableMemoryLabel + numberFormat.format(systemAvailableMemoryFloat) + " " + mebibyte); - SpannableStringBuilder systemTotalMemoryStringBuilder = new SpannableStringBuilder(systemTotalMemoryLabel + numberFormat.format(systemTotalMemoryFloat) + " " + mebibyte); - - // Setup the spans to display the memory information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. - appConsumedMemoryStringBuilder.setSpan(blueColorSpan, appConsumedMemoryLabel.length(), appConsumedMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - appAvailableMemoryStringBuilder.setSpan(blueColorSpan, appAvailableMemoryLabel.length(), appAvailableMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - appTotalMemoryStringBuilder.setSpan(blueColorSpan, appTotalMemoryLabel.length(), appTotalMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - appMaximumMemoryStringBuilder.setSpan(blueColorSpan, appMaximumMemoryLabel.length(), appMaximumMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - systemConsumedMemoryStringBuilder.setSpan(blueColorSpan, systemConsumedMemoryLabel.length(), systemConsumedMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - systemAvailableMemoryStringBuilder.setSpan(blueColorSpan, systemAvailableMemoryLabel.length(), systemAvailableMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - systemTotalMemoryStringBuilder.setSpan(blueColorSpan, systemTotalMemoryLabel.length(), systemTotalMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - // Display the string in the text boxes. - appConsumedMemoryTextView.setText(appConsumedMemoryStringBuilder); - appAvailableMemoryTextView.setText(appAvailableMemoryStringBuilder); - appTotalMemoryTextView.setText(appTotalMemoryStringBuilder); - appMaximumMemoryTextView.setText(appMaximumMemoryStringBuilder); - systemConsumedMemoryTextView.setText(systemConsumedMemoryStringBuilder); - systemAvailableMemoryTextView.setText(systemAvailableMemoryStringBuilder); - systemTotalMemoryTextView.setText(systemTotalMemoryStringBuilder); - } - - // Schedule another memory update if the activity has not been destroyed. - if (!activity.isDestroyed()) { - // Create a handler to update the memory usage. - Handler updateMemoryUsageHandler = new Handler(); - - // Create a runnable to update the memory usage. - Runnable updateMemoryUsageRunnable = () -> updateMemoryUsage(activity); - - // Update the memory usage after 1000 milliseconds - updateMemoryUsageHandler.postDelayed(updateMemoryUsageRunnable, 1000); - } - } catch (Exception exception) { - // Do nothing. - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.java b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.java new file mode 100644 index 00000000..b088a88f --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.java @@ -0,0 +1,840 @@ +/* + * Copyright © 2016-2020 Soren Stoutner . + * + * This file is part of Privacy Browser . + * + * Privacy Browser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser. If not, see . + */ + +package com.stoutner.privacybrowser.fragments; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.TextView; + +import com.google.android.material.snackbar.Snackbar; +import com.stoutner.privacybrowser.BuildConfig; +import com.stoutner.privacybrowser.R; +import com.stoutner.privacybrowser.dialogs.SaveDialog; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.NumberFormat; +import java.util.Date; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.webkit.WebViewCompat; + +public class AboutVersionFragment extends Fragment { + // Declare the class constants. + final static String BLOCKLIST_VERSIONS = "blocklist_versions"; + final long MEBIBYTE = 1048576; + + // Declare the class variables. + private boolean updateMemoryUsageBoolean = true; + private String[] blocklistVersions; + private View aboutVersionLayout; + private String appConsumedMemoryLabel; + private String appAvailableMemoryLabel; + private String appTotalMemoryLabel; + private String appMaximumMemoryLabel; + private String systemConsumedMemoryLabel; + private String systemAvailableMemoryLabel; + private String systemTotalMemoryLabel; + private Runtime runtime; + private ActivityManager activityManager; + private ActivityManager.MemoryInfo memoryInfo; + private NumberFormat numberFormat; + private ForegroundColorSpan blueColorSpan; + + // Declare the class views. + private TextView privacyBrowserTextView; + private TextView versionTextView; + private TextView hardwareTextView; + private TextView brandTextView; + private TextView manufacturerTextView; + private TextView modelTextView; + private TextView deviceTextView; + private TextView bootloaderTextView; + private TextView radioTextView; + private TextView softwareTextView; + private TextView androidTextView; + private TextView securityPatchTextView; + private TextView buildTextView; + private TextView webViewProviderTextView; + private TextView webViewVersionTextView; + private TextView orbotTextView; + private TextView i2pTextView; + private TextView openKeychainTextView; + private TextView memoryUsageTextView; + private TextView appConsumedMemoryTextView; + private TextView appAvailableMemoryTextView; + private TextView appTotalMemoryTextView; + private TextView appMaximumMemoryTextView; + private TextView systemConsumedMemoryTextView; + private TextView systemAvailableMemoryTextView; + private TextView systemTotalMemoryTextView; + private TextView blocklistsTextView; + private TextView easyListTextView; + private TextView easyPrivacyTextView; + private TextView fanboyAnnoyanceTextView; + private TextView fanboySocialTextView; + private TextView ultraListTextView; + private TextView ultraPrivacyTextView; + private TextView packageSignatureTextView; + private TextView certificateIssuerDnTextView; + private TextView certificateSubjectDnTextView; + private TextView certificateStartDateTextView; + private TextView certificateEndDateTextView; + private TextView certificateVersionTextView; + private TextView certificateSerialNumberTextView; + private TextView certificateSignatureAlgorithmTextView; + + public static AboutVersionFragment createTab(String[] blocklistVersions) { + // Create an arguments bundle. + Bundle argumentsBundle = new Bundle(); + + // Store the arguments in the bundle. + argumentsBundle.putStringArray(BLOCKLIST_VERSIONS, blocklistVersions); + + // Create a new instance of the tab fragment. + AboutVersionFragment aboutVersionFragment = new AboutVersionFragment(); + + // Add the arguments bundle to the fragment. + aboutVersionFragment.setArguments(argumentsBundle); + + // Return the new fragment. + return aboutVersionFragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + // Run the default commands. + super.onCreate(savedInstanceState); + + // Get a handle for the arguments. + Bundle arguments = getArguments(); + + // Remove the incorrect lint warning below that the arguments might be null. + assert arguments != null; + + // Store the arguments in class variables. + blocklistVersions = arguments.getStringArray(BLOCKLIST_VERSIONS); + + // Enable the options menu for this fragment. + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) { + // Get a handle for the context. + Context context = getContext(); + + // Remove the incorrect lint warning below that the context might be null. + assert context != null; + + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Inflate the layout. Setting false at the end of inflater.inflate does not attach the inflated layout as a child of container. The fragment will take care of attaching the root automatically. + aboutVersionLayout = layoutInflater.inflate(R.layout.about_version, container, false); + + // Get handles for the views. + privacyBrowserTextView = aboutVersionLayout.findViewById(R.id.privacy_browser_textview); + versionTextView = aboutVersionLayout.findViewById(R.id.version); + hardwareTextView = aboutVersionLayout.findViewById(R.id.hardware); + brandTextView = aboutVersionLayout.findViewById(R.id.brand); + manufacturerTextView = aboutVersionLayout.findViewById(R.id.manufacturer); + modelTextView = aboutVersionLayout.findViewById(R.id.model); + deviceTextView = aboutVersionLayout.findViewById(R.id.device); + bootloaderTextView = aboutVersionLayout.findViewById(R.id.bootloader); + radioTextView = aboutVersionLayout.findViewById(R.id.radio); + softwareTextView = aboutVersionLayout.findViewById(R.id.software); + androidTextView = aboutVersionLayout.findViewById(R.id.android); + securityPatchTextView = aboutVersionLayout.findViewById(R.id.security_patch); + buildTextView = aboutVersionLayout.findViewById(R.id.build); + webViewProviderTextView = aboutVersionLayout.findViewById(R.id.webview_provider); + webViewVersionTextView = aboutVersionLayout.findViewById(R.id.webview_version); + orbotTextView = aboutVersionLayout.findViewById(R.id.orbot); + i2pTextView = aboutVersionLayout.findViewById(R.id.i2p); + openKeychainTextView = aboutVersionLayout.findViewById(R.id.open_keychain); + memoryUsageTextView = aboutVersionLayout.findViewById(R.id.memory_usage); + appConsumedMemoryTextView = aboutVersionLayout.findViewById(R.id.app_consumed_memory); + appAvailableMemoryTextView = aboutVersionLayout.findViewById(R.id.app_available_memory); + appTotalMemoryTextView = aboutVersionLayout.findViewById(R.id.app_total_memory); + appMaximumMemoryTextView = aboutVersionLayout.findViewById(R.id.app_maximum_memory); + systemConsumedMemoryTextView = aboutVersionLayout.findViewById(R.id.system_consumed_memory); + systemAvailableMemoryTextView = aboutVersionLayout.findViewById(R.id.system_available_memory); + systemTotalMemoryTextView = aboutVersionLayout.findViewById(R.id.system_total_memory); + blocklistsTextView = aboutVersionLayout.findViewById(R.id.blocklists); + easyListTextView = aboutVersionLayout.findViewById(R.id.easylist); + easyPrivacyTextView = aboutVersionLayout.findViewById(R.id.easyprivacy); + fanboyAnnoyanceTextView = aboutVersionLayout.findViewById(R.id.fanboy_annoyance); + fanboySocialTextView = aboutVersionLayout.findViewById(R.id.fanboy_social); + ultraListTextView = aboutVersionLayout.findViewById(R.id.ultralist); + ultraPrivacyTextView = aboutVersionLayout.findViewById(R.id.ultraprivacy); + packageSignatureTextView = aboutVersionLayout.findViewById(R.id.package_signature); + certificateIssuerDnTextView = aboutVersionLayout.findViewById(R.id.certificate_issuer_dn); + certificateSubjectDnTextView = aboutVersionLayout.findViewById(R.id.certificate_subject_dn); + certificateStartDateTextView = aboutVersionLayout.findViewById(R.id.certificate_start_date); + certificateEndDateTextView = aboutVersionLayout.findViewById(R.id.certificate_end_date); + certificateVersionTextView = aboutVersionLayout.findViewById(R.id.certificate_version); + certificateSerialNumberTextView = aboutVersionLayout.findViewById(R.id.certificate_serial_number); + certificateSignatureAlgorithmTextView = aboutVersionLayout.findViewById(R.id.certificate_signature_algorithm); + + // Setup the labels. + String version = getString(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + getString(R.string.version_code) + " " + BuildConfig.VERSION_CODE + ")"; + String brandLabel = getString(R.string.brand) + " "; + String manufacturerLabel = getString(R.string.manufacturer) + " "; + String modelLabel = getString(R.string.model) + " "; + String deviceLabel = getString(R.string.device) + " "; + String bootloaderLabel = getString(R.string.bootloader) + " "; + String androidLabel = getString(R.string.android) + " "; + String buildLabel = getString(R.string.build) + " "; + String webViewVersionLabel = getString(R.string.webview_version) + " "; + appConsumedMemoryLabel = getString(R.string.app_consumed_memory) + " "; + appAvailableMemoryLabel = getString(R.string.app_available_memory) + " "; + appTotalMemoryLabel = getString(R.string.app_total_memory) + " "; + appMaximumMemoryLabel = getString(R.string.app_maximum_memory) + " "; + systemConsumedMemoryLabel = getString(R.string.system_consumed_memory) + " "; + systemAvailableMemoryLabel = getString(R.string.system_available_memory) + " "; + systemTotalMemoryLabel = getString(R.string.system_total_memory) + " "; + String easyListLabel = getString(R.string.easylist_label) + " "; + String easyPrivacyLabel = getString(R.string.easyprivacy_label) + " "; + String fanboyAnnoyanceLabel = getString(R.string.fanboy_annoyance_label) + " "; + String fanboySocialLabel = getString(R.string.fanboy_social_label) + " "; + String ultraListLabel = getString(R.string.ultralist_label) + " "; + String ultraPrivacyLabel = getString(R.string.ultraprivacy_label) + " "; + String issuerDNLabel = getString(R.string.issuer_dn) + " "; + String subjectDNLabel = getString(R.string.subject_dn) + " "; + String startDateLabel = getString(R.string.start_date) + " "; + String endDateLabel = getString(R.string.end_date) + " "; + String certificateVersionLabel = getString(R.string.certificate_version) + " "; + String serialNumberLabel = getString(R.string.serial_number) + " "; + String signatureAlgorithmLabel = getString(R.string.signature_algorithm) + " "; + + // The WebView layout is only used to get the default user agent from `bare_webview`. It is not used to render content on the screen. + // Once the minimum API >= 26 this can be accomplished with the WebView package info. + View webViewLayout = layoutInflater.inflate(R.layout.bare_webview, container, false); + WebView tabLayoutWebView = webViewLayout.findViewById(R.id.bare_webview); + String userAgentString = tabLayoutWebView.getSettings().getUserAgentString(); + + // Get the device's information and store it in strings. + String brand = Build.BRAND; + String manufacturer = Build.MANUFACTURER; + String model = Build.MODEL; + String device = Build.DEVICE; + String bootloader = Build.BOOTLOADER; + String radio = Build.getRadioVersion(); + String android = Build.VERSION.RELEASE + " (" + getString(R.string.api) + " " + Build.VERSION.SDK_INT + ")"; + String build = Build.DISPLAY; + // Select the substring that begins after `Chrome/` and goes until the next ` `. + String webView = userAgentString.substring(userAgentString.indexOf("Chrome/") + 7, userAgentString.indexOf(" ", userAgentString.indexOf("Chrome/"))); + + // Get the Orbot version name if Orbot is installed. + String orbot; + try { + // Store the version name. + orbot = context.getPackageManager().getPackageInfo("org.torproject.android", 0).versionName; + } catch (PackageManager.NameNotFoundException exception) { // Orbot is not installed. + orbot = ""; + } + + // Get the I2P version name if I2P is installed. + String i2p; + try { + // Store the version name. + i2p = context.getPackageManager().getPackageInfo("net.i2p.android.router", 0).versionName; + } catch (PackageManager.NameNotFoundException exception) { // I2P is not installed. + i2p = ""; + } + + // Get the OpenKeychain version name if it is installed. + String openKeychain; + try { + // Store the version name. + openKeychain = context.getPackageManager().getPackageInfo("org.sufficientlysecure.keychain", 0).versionName; + } catch (PackageManager.NameNotFoundException exception) { // OpenKeychain is not installed. + openKeychain = ""; + } + + // Create a spannable string builder for the hardware and software text views that needs multiple colors of text. + SpannableStringBuilder brandStringBuilder = new SpannableStringBuilder(brandLabel + brand); + SpannableStringBuilder manufacturerStringBuilder = new SpannableStringBuilder(manufacturerLabel + manufacturer); + SpannableStringBuilder modelStringBuilder = new SpannableStringBuilder(modelLabel + model); + SpannableStringBuilder deviceStringBuilder = new SpannableStringBuilder(deviceLabel + device); + SpannableStringBuilder bootloaderStringBuilder = new SpannableStringBuilder(bootloaderLabel + bootloader); + SpannableStringBuilder androidStringBuilder = new SpannableStringBuilder(androidLabel + android); + SpannableStringBuilder buildStringBuilder = new SpannableStringBuilder(buildLabel + build); + SpannableStringBuilder webViewVersionStringBuilder = new SpannableStringBuilder(webViewVersionLabel + webView); + SpannableStringBuilder easyListStringBuilder = new SpannableStringBuilder(easyListLabel + blocklistVersions[0]); + SpannableStringBuilder easyPrivacyStringBuilder = new SpannableStringBuilder(easyPrivacyLabel + blocklistVersions[1]); + SpannableStringBuilder fanboyAnnoyanceStringBuilder = new SpannableStringBuilder(fanboyAnnoyanceLabel + blocklistVersions[2]); + SpannableStringBuilder fanboySocialStringBuilder = new SpannableStringBuilder(fanboySocialLabel + blocklistVersions[3]); + SpannableStringBuilder ultraListStringBuilder = new SpannableStringBuilder(ultraListLabel + blocklistVersions[4]); + SpannableStringBuilder ultraPrivacyStringBuilder = new SpannableStringBuilder(ultraPrivacyLabel + blocklistVersions[5]); + + // Set the blue color span according to the theme. The deprecated `getResources()` must be used until the minimum API >= 23. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700)); + } else { + blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.violet_500)); + } + + // Setup the spans to display the device information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. + brandStringBuilder.setSpan(blueColorSpan, brandLabel.length(), brandStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + manufacturerStringBuilder.setSpan(blueColorSpan, manufacturerLabel.length(), manufacturerStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + modelStringBuilder.setSpan(blueColorSpan, modelLabel.length(), modelStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + deviceStringBuilder.setSpan(blueColorSpan, deviceLabel.length(), deviceStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + bootloaderStringBuilder.setSpan(blueColorSpan, bootloaderLabel.length(), bootloaderStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + androidStringBuilder.setSpan(blueColorSpan, androidLabel.length(), androidStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + buildStringBuilder.setSpan(blueColorSpan, buildLabel.length(), buildStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + webViewVersionStringBuilder.setSpan(blueColorSpan, webViewVersionLabel.length(), webViewVersionStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + easyListStringBuilder.setSpan(blueColorSpan, easyListLabel.length(), easyListStringBuilder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + easyPrivacyStringBuilder.setSpan(blueColorSpan, easyPrivacyLabel.length(), easyPrivacyStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + fanboyAnnoyanceStringBuilder.setSpan(blueColorSpan, fanboyAnnoyanceLabel.length(), fanboyAnnoyanceStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + fanboySocialStringBuilder.setSpan(blueColorSpan, fanboySocialLabel.length(), fanboySocialStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + ultraListStringBuilder.setSpan(blueColorSpan, ultraListLabel.length(), ultraListStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + ultraPrivacyStringBuilder.setSpan(blueColorSpan, ultraPrivacyLabel.length(), ultraPrivacyStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + // Display the strings in the text boxes. + versionTextView.setText(version); + brandTextView.setText(brandStringBuilder); + manufacturerTextView.setText(manufacturerStringBuilder); + modelTextView.setText(modelStringBuilder); + deviceTextView.setText(deviceStringBuilder); + bootloaderTextView.setText(bootloaderStringBuilder); + androidTextView.setText(androidStringBuilder); + buildTextView.setText(buildStringBuilder); + webViewVersionTextView.setText(webViewVersionStringBuilder); + easyListTextView.setText(easyListStringBuilder); + easyPrivacyTextView.setText(easyPrivacyStringBuilder); + fanboyAnnoyanceTextView.setText(fanboyAnnoyanceStringBuilder); + fanboySocialTextView.setText(fanboySocialStringBuilder); + ultraListTextView.setText(ultraListStringBuilder); + ultraPrivacyTextView.setText(ultraPrivacyStringBuilder); + + // Only populate the radio text view if there is a radio in the device. + if (!radio.isEmpty()) { + String radioLabel = getString(R.string.radio) + " "; + SpannableStringBuilder radioStringBuilder = new SpannableStringBuilder(radioLabel + radio); + radioStringBuilder.setSpan(blueColorSpan, radioLabel.length(), radioStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + radioTextView.setText(radioStringBuilder); + } else { // This device does not have a radio. + radioTextView.setVisibility(View.GONE); + } + + // Build.VERSION.SECURITY_PATCH is only available for SDK_INT >= 23. + if (Build.VERSION.SDK_INT >= 23) { + String securityPatchLabel = getString(R.string.security_patch) + " "; + String securityPatch = Build.VERSION.SECURITY_PATCH; + SpannableStringBuilder securityPatchStringBuilder = new SpannableStringBuilder(securityPatchLabel + securityPatch); + securityPatchStringBuilder.setSpan(blueColorSpan, securityPatchLabel.length(), securityPatchStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + securityPatchTextView.setText(securityPatchStringBuilder); + } else { // The API < 23. + // Hide the security patch text view. + securityPatchTextView.setVisibility(View.GONE); + } + + // Only populate the WebView provider if the SDK >= 21. + if (Build.VERSION.SDK_INT >= 21) { + // Create the WebView provider label. + String webViewProviderLabel = getString(R.string.webview_provider) + " "; + + // Get the current WebView package info. + PackageInfo webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(context); + + // Remove the warning below that the package info might be null. + assert webViewPackageInfo != null; + + // Get the WebView provider name. + String webViewPackageName = webViewPackageInfo.packageName; + + // Create the spannable string builder. + SpannableStringBuilder webViewProviderStringBuilder = new SpannableStringBuilder(webViewProviderLabel + webViewPackageName); + + // Apply the coloration. + webViewProviderStringBuilder.setSpan(blueColorSpan, webViewProviderLabel.length(), webViewProviderStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + // Display the WebView provider. + webViewProviderTextView.setText(webViewProviderStringBuilder); + } else { // The API < 21. + // Hide the WebView provider text view. + webViewProviderTextView.setVisibility(View.GONE); + } + + // Only populate the Orbot text view if it is installed. + if (!orbot.isEmpty()) { + String orbotLabel = getString(R.string.orbot) + " "; + SpannableStringBuilder orbotStringBuilder = new SpannableStringBuilder(orbotLabel + orbot); + orbotStringBuilder.setSpan(blueColorSpan, orbotLabel.length(), orbotStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + orbotTextView.setText(orbotStringBuilder); + } else { // Orbot is not installed. + orbotTextView.setVisibility(View.GONE); + } + + // Only populate the I2P text view if it is installed. + if (!i2p.isEmpty()) { + String i2pLabel = getString(R.string.i2p) + " "; + SpannableStringBuilder i2pStringBuilder = new SpannableStringBuilder(i2pLabel + i2p); + i2pStringBuilder.setSpan(blueColorSpan, i2pLabel.length(), i2pStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + i2pTextView.setText(i2pStringBuilder); + } else { // I2P is not installed. + i2pTextView.setVisibility(View.GONE); + } + + // Only populate the OpenKeychain text view if it is installed. + if (!openKeychain.isEmpty()) { + String openKeychainLabel = getString(R.string.openkeychain) + " "; + SpannableStringBuilder openKeychainStringBuilder = new SpannableStringBuilder(openKeychainLabel + openKeychain); + openKeychainStringBuilder.setSpan(blueColorSpan, openKeychainLabel.length(), openKeychainStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + openKeychainTextView.setText(openKeychainStringBuilder); + } else { //OpenKeychain is not installed. + openKeychainTextView.setVisibility(View.GONE); + } + + // Display the package signature. + try { + // Get the first package signature. Suppress the lint warning about the need to be careful in implementing comparison of certificates for security purposes. + @SuppressLint("PackageManagerGetSignatures") Signature packageSignature = context.getPackageManager().getPackageInfo(context.getPackageName(), + PackageManager.GET_SIGNATURES).signatures[0]; + + // Convert the signature to a byte array input stream. + InputStream certificateByteArrayInputStream = new ByteArrayInputStream(packageSignature.toByteArray()); + + // Display the certificate information on the screen. + try { + // Instantiate a `CertificateFactory`. + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + + // Generate an `X509Certificate`. + X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(certificateByteArrayInputStream); + + // Store the individual sections of the certificate that we are interested in. + Principal issuerDNPrincipal = x509Certificate.getIssuerDN(); + Principal subjectDNPrincipal = x509Certificate.getSubjectDN(); + Date startDate = x509Certificate.getNotBefore(); + Date endDate = x509Certificate.getNotAfter(); + int certificateVersion = x509Certificate.getVersion(); + BigInteger serialNumberBigInteger = x509Certificate.getSerialNumber(); + String signatureAlgorithmNameString = x509Certificate.getSigAlgName(); + + // Create a `SpannableStringBuilder` for each `TextView` that needs multiple colors of text. + SpannableStringBuilder issuerDNStringBuilder = new SpannableStringBuilder(issuerDNLabel + issuerDNPrincipal.toString()); + SpannableStringBuilder subjectDNStringBuilder = new SpannableStringBuilder(subjectDNLabel + subjectDNPrincipal.toString()); + SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate)); + SpannableStringBuilder endDataStringBuilder = new SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate)); + SpannableStringBuilder certificateVersionStringBuilder = new SpannableStringBuilder(certificateVersionLabel + certificateVersion); + SpannableStringBuilder serialNumberStringBuilder = new SpannableStringBuilder(serialNumberLabel + serialNumberBigInteger); + SpannableStringBuilder signatureAlgorithmStringBuilder = new SpannableStringBuilder(signatureAlgorithmLabel + signatureAlgorithmNameString); + + // Setup the spans to display the device information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. + issuerDNStringBuilder.setSpan(blueColorSpan, issuerDNLabel.length(), issuerDNStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + subjectDNStringBuilder.setSpan(blueColorSpan, subjectDNLabel.length(), subjectDNStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + endDataStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDataStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + certificateVersionStringBuilder.setSpan(blueColorSpan, certificateVersionLabel.length(), certificateVersionStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + serialNumberStringBuilder.setSpan(blueColorSpan, serialNumberLabel.length(), serialNumberStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + signatureAlgorithmStringBuilder.setSpan(blueColorSpan, signatureAlgorithmLabel.length(), signatureAlgorithmStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + // Display the strings in the text boxes. + certificateIssuerDnTextView.setText(issuerDNStringBuilder); + certificateSubjectDnTextView.setText(subjectDNStringBuilder); + certificateStartDateTextView.setText(startDateStringBuilder); + certificateEndDateTextView.setText(endDataStringBuilder); + certificateVersionTextView.setText(certificateVersionStringBuilder); + certificateSerialNumberTextView.setText(serialNumberStringBuilder); + certificateSignatureAlgorithmTextView.setText(signatureAlgorithmStringBuilder); + } catch (CertificateException e) { + // Do nothing if there is a certificate error. + } + + // Get a handle for the runtime. + runtime = Runtime.getRuntime(); + + // Get a handle for the activity. + Activity activity = getActivity(); + + // Remove the incorrect lint warning below that the activity might be null. + assert activity != null; + + // Get a handle for the activity manager. + activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); + + // Remove the incorrect lint warning below that the activity manager might be null. + assert activityManager != null; + + // Instantiate a memory info variable. + memoryInfo = new ActivityManager.MemoryInfo(); + + // Define a number format. + numberFormat = NumberFormat.getInstance(); + + // Set the minimum and maximum number of fraction digits. + numberFormat.setMinimumFractionDigits(2); + numberFormat.setMaximumFractionDigits(2); + + // Update the memory usage. + updateMemoryUsage(getActivity()); + } catch (PackageManager.NameNotFoundException e) { + // Do nothing if `PackageManager` says Privacy Browser isn't installed. + } + + // Scroll the tab if the saved instance state is not null. + if (savedInstanceState != null) { + aboutVersionLayout.post(() -> { + aboutVersionLayout.setScrollX(savedInstanceState.getInt("scroll_x")); + aboutVersionLayout.setScrollY(savedInstanceState.getInt("scroll_y")); + }); + } + + // Return the tab layout. + return aboutVersionLayout; + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { + // Inflate the about version menu. + menuInflater.inflate(R.menu.about_version_options_menu, menu); + + // Run the default commands. + super.onCreateOptionsMenu(menu, menuInflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem menuItem) { + // Get the ID of the menu item that was selected. + int menuItemId = menuItem.getItemId(); + + // Remove the warning below that `getActivity()` might be null. + assert getActivity() != null; + + // Run the appropriate commands. + switch (menuItemId) { + case R.id.copy: + // Get the about version string. + String aboutVersionString = getAboutVersionString(); + + // Get a handle for the clipboard manager. + ClipboardManager clipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + + // Remove the incorrect lint error below that the clipboard manager might be null. + assert clipboardManager != null; + + // Save the about version string in a clip data. + ClipData aboutVersionClipData = ClipData.newPlainText(getString(R.string.about), aboutVersionString); + + // Place the clip data on the clipboard. + clipboardManager.setPrimaryClip(aboutVersionClipData); + + // Display a snackbar. + Snackbar.make(aboutVersionLayout, R.string.version_info_copied, Snackbar.LENGTH_SHORT).show(); + + // Consume the event. + return true; + + case R.id.share: + // Get the about version string. + String aboutString = getAboutVersionString(); + + // Create an email intent. + Intent emailIntent = new Intent(Intent.ACTION_SEND); + + // Add the about version string to the intent. + emailIntent.putExtra(Intent.EXTRA_TEXT, aboutString); + + // Set the MIME type. + emailIntent.setType("text/plain"); + + // Set the intent to open in a new task. + emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Make it so. + startActivity(Intent.createChooser(emailIntent, getString(R.string.share))); + + // Consume the event. + return true; + + case R.id.save_text: + // Instantiate the save alert dialog. + DialogFragment saveTextDialogFragment = SaveDialog.save(SaveDialog.SAVE_ABOUT_VERSION_TEXT); + + // Show the save alert dialog. + saveTextDialogFragment.show(getActivity().getSupportFragmentManager(), getString(R.string.save_dialog)); + + // Consume the event. + return true; + + case R.id.save_image: + // Instantiate the save alert dialog. + DialogFragment saveImageDialogFragment = SaveDialog.save(SaveDialog.SAVE_ABOUT_VERSION_IMAGE); + + // Show the save alert dialog. + saveImageDialogFragment.show(getActivity().getSupportFragmentManager(), getString(R.string.save_dialog)); + + // Consume the event. + return true; + + default: + // Don't consume the event. + return super.onOptionsItemSelected(menuItem); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { + // Run the default commands. + super.onSaveInstanceState(savedInstanceState); + + // Save the scroll positions if the layout is not null, which can happen if a tab is not currently selected. + if (aboutVersionLayout != null) { + savedInstanceState.putInt("scroll_x", aboutVersionLayout.getScrollX()); + savedInstanceState.putInt("scroll_y", aboutVersionLayout.getScrollY()); + } + } + + @Override + public void onPause() { + // Run the default commands. + super.onPause(); + + // Pause the updating of the memory usage. + updateMemoryUsageBoolean = false; + } + + @Override + public void onResume() { + // Run the default commands. + super.onResume(); + + // Resume the updating of the memory usage. + updateMemoryUsageBoolean = true; + } + + public void updateMemoryUsage(Activity activity) { + try { + // Update the memory usage if enabled. + if (updateMemoryUsageBoolean) { + // Populate the memory info variable. + activityManager.getMemoryInfo(memoryInfo); + + // Get the app memory information. + long appAvailableMemoryLong = runtime.freeMemory(); + long appTotalMemoryLong = runtime.totalMemory(); + long appMaximumMemoryLong = runtime.maxMemory(); + + // Calculate the app consumed memory. + long appConsumedMemoryLong = appTotalMemoryLong - appAvailableMemoryLong; + + // Get the system memory information. + long systemTotalMemoryLong = memoryInfo.totalMem; + long systemAvailableMemoryLong = memoryInfo.availMem; + + // Calculate the system consumed memory. + long systemConsumedMemoryLong = systemTotalMemoryLong - systemAvailableMemoryLong; + + // Convert the memory information into mebibytes. + float appConsumedMemoryFloat = (float) appConsumedMemoryLong / MEBIBYTE; + float appAvailableMemoryFloat = (float) appAvailableMemoryLong / MEBIBYTE; + float appTotalMemoryFloat = (float) appTotalMemoryLong / MEBIBYTE; + float appMaximumMemoryFloat = (float) appMaximumMemoryLong / MEBIBYTE; + float systemConsumedMemoryFloat = (float) systemConsumedMemoryLong / MEBIBYTE; + float systemAvailableMemoryFloat = (float) systemAvailableMemoryLong / MEBIBYTE; + float systemTotalMemoryFloat = (float) systemTotalMemoryLong / MEBIBYTE; + + // Get the mebibyte string. + String mebibyte = getString(R.string.mebibyte); + + // Calculate the mebibyte length. + int mebibyteLength = mebibyte.length(); + + // Create spannable string builders. + SpannableStringBuilder appConsumedMemoryStringBuilder = new SpannableStringBuilder(appConsumedMemoryLabel + numberFormat.format(appConsumedMemoryFloat) + " " + mebibyte); + SpannableStringBuilder appAvailableMemoryStringBuilder = new SpannableStringBuilder(appAvailableMemoryLabel + numberFormat.format(appAvailableMemoryFloat) + " " + mebibyte); + SpannableStringBuilder appTotalMemoryStringBuilder = new SpannableStringBuilder(appTotalMemoryLabel + numberFormat.format(appTotalMemoryFloat) + " " + mebibyte); + SpannableStringBuilder appMaximumMemoryStringBuilder = new SpannableStringBuilder(appMaximumMemoryLabel + numberFormat.format(appMaximumMemoryFloat) + " " + mebibyte); + SpannableStringBuilder systemConsumedMemoryStringBuilder = new SpannableStringBuilder(systemConsumedMemoryLabel + numberFormat.format(systemConsumedMemoryFloat) + " " + mebibyte); + SpannableStringBuilder systemAvailableMemoryStringBuilder = new SpannableStringBuilder(systemAvailableMemoryLabel + numberFormat.format(systemAvailableMemoryFloat) + " " + mebibyte); + SpannableStringBuilder systemTotalMemoryStringBuilder = new SpannableStringBuilder(systemTotalMemoryLabel + numberFormat.format(systemTotalMemoryFloat) + " " + mebibyte); + + // Setup the spans to display the memory information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. + appConsumedMemoryStringBuilder.setSpan(blueColorSpan, appConsumedMemoryLabel.length(), appConsumedMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + appAvailableMemoryStringBuilder.setSpan(blueColorSpan, appAvailableMemoryLabel.length(), appAvailableMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + appTotalMemoryStringBuilder.setSpan(blueColorSpan, appTotalMemoryLabel.length(), appTotalMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + appMaximumMemoryStringBuilder.setSpan(blueColorSpan, appMaximumMemoryLabel.length(), appMaximumMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + systemConsumedMemoryStringBuilder.setSpan(blueColorSpan, systemConsumedMemoryLabel.length(), systemConsumedMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + systemAvailableMemoryStringBuilder.setSpan(blueColorSpan, systemAvailableMemoryLabel.length(), systemAvailableMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + systemTotalMemoryStringBuilder.setSpan(blueColorSpan, systemTotalMemoryLabel.length(), systemTotalMemoryStringBuilder.length() - mebibyteLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + // Display the string in the text boxes. + appConsumedMemoryTextView.setText(appConsumedMemoryStringBuilder); + appAvailableMemoryTextView.setText(appAvailableMemoryStringBuilder); + appTotalMemoryTextView.setText(appTotalMemoryStringBuilder); + appMaximumMemoryTextView.setText(appMaximumMemoryStringBuilder); + systemConsumedMemoryTextView.setText(systemConsumedMemoryStringBuilder); + systemAvailableMemoryTextView.setText(systemAvailableMemoryStringBuilder); + systemTotalMemoryTextView.setText(systemTotalMemoryStringBuilder); + } + + // Schedule another memory update if the activity has not been destroyed. + if (!activity.isDestroyed()) { + // Create a handler to update the memory usage. + Handler updateMemoryUsageHandler = new Handler(); + + // Create a runnable to update the memory usage. + Runnable updateMemoryUsageRunnable = () -> updateMemoryUsage(activity); + + // Update the memory usage after 1000 milliseconds + updateMemoryUsageHandler.postDelayed(updateMemoryUsageRunnable, 1000); + } + } catch (Exception exception) { + // Do nothing. + } + } + + public String getAboutVersionString() { + // Initialize an about version string builder. + StringBuilder aboutVersionStringBuilder = new StringBuilder(); + + // Populate the about version string builder. + aboutVersionStringBuilder.append(privacyBrowserTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(versionTextView.getText()); + aboutVersionStringBuilder.append("\n\n"); + aboutVersionStringBuilder.append(hardwareTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(brandTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(manufacturerTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(modelTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(deviceTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(bootloaderTextView.getText()); + aboutVersionStringBuilder.append("\n"); + if (radioTextView.getVisibility() == View.VISIBLE) { + aboutVersionStringBuilder.append(radioTextView.getText()); + aboutVersionStringBuilder.append("\n"); + } + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(softwareTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(androidTextView.getText()); + aboutVersionStringBuilder.append("\n"); + if (securityPatchTextView.getVisibility() == View.VISIBLE) { + aboutVersionStringBuilder.append(securityPatchTextView.getText()); + aboutVersionStringBuilder.append("\n"); + } + aboutVersionStringBuilder.append(buildTextView.getText()); + aboutVersionStringBuilder.append("\n"); + if (webViewProviderTextView.getVisibility() == View.VISIBLE) { + aboutVersionStringBuilder.append(webViewProviderTextView.getText()); + aboutVersionStringBuilder.append("\n"); + } + aboutVersionStringBuilder.append(webViewVersionTextView.getText()); + aboutVersionStringBuilder.append("\n"); + if (orbotTextView.getVisibility() == View.VISIBLE) { + aboutVersionStringBuilder.append(orbotTextView.getText()); + aboutVersionStringBuilder.append("\n"); + } + if (i2pTextView.getVisibility() == View.VISIBLE) { + aboutVersionStringBuilder.append(i2pTextView.getText()); + aboutVersionStringBuilder.append("\n"); + } + if (openKeychainTextView.getVisibility() == View.VISIBLE) { + aboutVersionStringBuilder.append(openKeychainTextView.getText()); + aboutVersionStringBuilder.append("\n"); + } + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(memoryUsageTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(appConsumedMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(appAvailableMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(appTotalMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(appMaximumMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(systemConsumedMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(systemAvailableMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(systemTotalMemoryTextView.getText()); + aboutVersionStringBuilder.append("\n\n"); + aboutVersionStringBuilder.append(blocklistsTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(easyListTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(easyPrivacyTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(fanboyAnnoyanceTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(fanboySocialTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(ultraListTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(ultraPrivacyTextView.getText()); + aboutVersionStringBuilder.append("\n\n"); + aboutVersionStringBuilder.append(packageSignatureTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateIssuerDnTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateSubjectDnTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateStartDateTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateEndDateTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateVersionTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateSerialNumberTextView.getText()); + aboutVersionStringBuilder.append("\n"); + aboutVersionStringBuilder.append(certificateSignatureAlgorithmTextView.getText()); + + // Return the string. + return aboutVersionStringBuilder.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutWebViewFragment.java b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutWebViewFragment.java new file mode 100644 index 00000000..307512b2 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutWebViewFragment.java @@ -0,0 +1,169 @@ +/* + * Copyright © 2016-2020 Soren Stoutner . + * + * This file is part of Privacy Browser . + * + * Privacy Browser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser. If not, see . + */ + +package com.stoutner.privacybrowser.fragments; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.stoutner.privacybrowser.R; + +public class AboutWebViewFragment extends Fragment { + // Declare the class constants. + final static String TAB_NUMBER = "tab_number"; + + // Declare the class variables. + private int tabNumber; + + // Declare the class views. + private View aboutWebViewLayout; + + public static AboutWebViewFragment createTab(int tabNumber) { + // Create an arguments bundle. + Bundle argumentsBundle = new Bundle(); + + // Store the arguments in the bundle. + argumentsBundle.putInt(TAB_NUMBER, tabNumber); + + // Create a new instance of the tab fragment. + AboutWebViewFragment aboutWebViewFragment = new AboutWebViewFragment(); + + // Add the arguments bundle to the fragment. + aboutWebViewFragment.setArguments(argumentsBundle); + + // Return the new fragment. + return aboutWebViewFragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + // Run the default commands. + super.onCreate(savedInstanceState); + + // Get a handle for the arguments. + Bundle arguments = getArguments(); + + // Remove the incorrect lint warning below that arguments might be null. + assert arguments != null; + + // Store the arguments in class variables. + tabNumber = arguments.getInt(TAB_NUMBER); + } + + @Override + public View onCreateView(@NonNull LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) { + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Inflate the layout. Setting false at the end of inflater.inflate does not attach the inflated layout as a child of container. The fragment will take care of attaching the root automatically. + aboutWebViewLayout = layoutInflater.inflate(R.layout.bare_webview, container, false); + + // Get a handle for tab WebView. + WebView tabWebView = (WebView) aboutWebViewLayout; + + // Load the tabs according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The light theme is applied. + switch (tabNumber) { + case 1: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_permissions_light.html"); + break; + + case 2: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_privacy_policy_light.html"); + break; + + case 3: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_changelog_light.html"); + break; + + case 4: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_licenses_light.html"); + break; + + case 5: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_contributors_light.html"); + break; + + case 6: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_links_light.html"); + break; + } + } else { // The dark theme is applied. + // Set the background color. The deprecated `.getColor()` must be used until the minimum API >= 23. + tabWebView.setBackgroundColor(getResources().getColor(R.color.gray_850)); + + // Tab numbers start at 0, with the WebView tabs starting at 1. + switch (tabNumber) { + case 1: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_permissions_dark.html"); + break; + + case 2: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_privacy_policy_dark.html"); + break; + + case 3: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_changelog_dark.html"); + break; + + case 4: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_licenses_dark.html"); + break; + + case 5: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_contributors_dark.html"); + break; + + case 6: + tabWebView.loadUrl("file:///android_asset/" + getString(R.string.android_asset_path) + "/about_links_dark.html"); + break; + } + } + + // Scroll the tab if the saved instance state is not null. + if (savedInstanceState != null) { + aboutWebViewLayout.post(() -> { + aboutWebViewLayout.setScrollX(savedInstanceState.getInt("scroll_x")); + aboutWebViewLayout.setScrollY(savedInstanceState.getInt("scroll_y")); + }); + } + + // Return the tab layout. + return aboutWebViewLayout; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { + // Run the default commands. + super.onSaveInstanceState(savedInstanceState); + + // Save the scroll positions if the layout is not null, which can happen if a tab is not currently selected. + if (aboutWebViewLayout != null) { + savedInstanceState.putInt("scroll_x", aboutWebViewLayout.getScrollX()); + savedInstanceState.putInt("scroll_y", aboutWebViewLayout.getScrollY()); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/images_options_day.xml b/app/src/main/res/drawable/images_options_day.xml new file mode 100644 index 00000000..04b2929b --- /dev/null +++ b/app/src/main/res/drawable/images_options_day.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/images_options_night.xml b/app/src/main/res/drawable/images_options_night.xml new file mode 100644 index 00000000..840a8bee --- /dev/null +++ b/app/src/main/res/drawable/images_options_night.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/save_text_blue_day.xml b/app/src/main/res/drawable/save_text_blue_day.xml new file mode 100644 index 00000000..69b71ea7 --- /dev/null +++ b/app/src/main/res/drawable/save_text_blue_day.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/save_text_blue_night.xml b/app/src/main/res/drawable/save_text_blue_night.xml new file mode 100644 index 00000000..3dbcd7d2 --- /dev/null +++ b/app/src/main/res/drawable/save_text_blue_night.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/save_text_day.xml b/app/src/main/res/drawable/save_text_day.xml new file mode 100644 index 00000000..7d5d4680 --- /dev/null +++ b/app/src/main/res/drawable/save_text_day.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/save_text_night.xml b/app/src/main/res/drawable/save_text_night.xml new file mode 100644 index 00000000..ed5dc429 --- /dev/null +++ b/app/src/main/res/drawable/save_text_night.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/share_day.xml b/app/src/main/res/drawable/share_day.xml new file mode 100644 index 00000000..ee2f0e21 --- /dev/null +++ b/app/src/main/res/drawable/share_day.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/share_night.xml b/app/src/main/res/drawable/share_night.xml new file mode 100644 index 00000000..3d3652bf --- /dev/null +++ b/app/src/main/res/drawable/share_night.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/layout/about_tab_version.xml b/app/src/main/res/layout/about_tab_version.xml deleted file mode 100644 index ef773d5c..00000000 --- a/app/src/main/res/layout/about_tab_version.xml +++ /dev/null @@ -1,336 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/about_version.xml b/app/src/main/res/layout/about_version.xml new file mode 100644 index 00000000..d9ee5ae7 --- /dev/null +++ b/app/src/main/res/layout/about_version.xml @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/save_dialog.xml b/app/src/main/res/layout/save_dialog.xml index 1519c9be..57e6d293 100644 --- a/app/src/main/res/layout/save_dialog.xml +++ b/app/src/main/res/layout/save_dialog.xml @@ -31,34 +31,11 @@ android:layout_marginStart="10dp" android:layout_marginEnd="10dp" > - - - - - - - - - - + android:orientation="horizontal" > - - - - - - - - - - - - - - - - - -