-/*
- * Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * SPDX-FileCopyrightText: 2016-2025 Soren Stoutner <soren@stoutner.com>
*
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
*
- * Privacy Browser Android 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.
+ * This program 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 Android 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.
+ * This program 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 Android. If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.stoutner.privacybrowser.fragments
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.MenuHost
+import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
-import androidx.webkit.WebViewCompat
+import androidx.lifecycle.Lifecycle
import com.google.android.material.snackbar.Snackbar
import kotlin.text.StringBuilder
// Define the class constants.
-private const val FILTERLISTS_VERSIONS = "filterlists_versions"
+private const val FILTERLISTS_VERSIONS = "A"
+private const val SCROLL_Y = "B"
private const val MEBIBYTE = 1048576
class AboutVersionFragment : Fragment() {
private val saveAboutVersionTextActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri ->
// Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
if (fileUri != null) {
- // Initialize the file name string from the file URI last path segment.
- var fileNameString = fileUri.lastPathSegment
+ // Get a cursor from the content resolver.
+ val contentResolverCursor = requireActivity().contentResolver.query(fileUri, null, null, null)!!
- // Query the exact file name if the API >= 26.
- if (Build.VERSION.SDK_INT >= 26) {
- // Get a cursor from the content resolver.
- val contentResolverCursor = requireActivity().contentResolver.query(fileUri, null, null, null)!!
+ // Move to the first row.
+ contentResolverCursor.moveToFirst()
- // Move to the first row.
- contentResolverCursor.moveToFirst()
+ // Get the file name from the cursor.
+ val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
- // Get the file name from the cursor.
- fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
-
- // Close the cursor.
- contentResolverCursor.close()
- }
+ // Close the cursor.
+ contentResolverCursor.close()
try {
// Get the about version string.
// Store the arguments in class variables.
filterListsVersions = requireArguments().getStringArray(FILTERLISTS_VERSIONS)!!
-
- // Enable the options menu for this fragment.
- setHasOptionsMenu(true)
}
override fun onCreateView(layoutInflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ // Add an options menu.
+ (requireActivity() as MenuHost).addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ // Inflate the about version menu.
+ menuInflater.inflate(R.menu.about_version_options_menu, menu)
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ // Run the appropriate commands.
+ when (menuItem.itemId) {
+ R.id.copy -> { // Copy.
+ // Get the about version string.
+ val aboutVersionString = getAboutVersionString()
+
+ // Get a handle for the clipboard manager.
+ val clipboardManager = (requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
+
+ // Place the about version string in a clip data.
+ val aboutVersionClipData = ClipData.newPlainText(getString(R.string.about), aboutVersionString)
+
+ // Place the clip data on the clipboard.
+ clipboardManager.setPrimaryClip(aboutVersionClipData)
+
+ // Display a snackbar if the API <= 32 (Android 12L). Beginning in Android 13 the OS displays a notification that covers up the snackbar.
+ if (Build.VERSION.SDK_INT <= 32)
+ Snackbar.make(aboutVersionLayout, R.string.version_info_copied, Snackbar.LENGTH_SHORT).show()
+
+ // Consume the event.
+ return true
+ }
+
+ R.id.share -> { // Share.
+ // Get the about version string.
+ val aboutString = getAboutVersionString()
+
+ // Create a share intent.
+ val shareIntent = Intent(Intent.ACTION_SEND)
+
+ // Add the about version string to the intent.
+ shareIntent.putExtra(Intent.EXTRA_TEXT, aboutString)
+
+ // Set the MIME type.
+ shareIntent.type = "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)))
+
+ // Consume the event.
+ return true
+ }
+
+ R.id.save_text -> { // Save text.
+ // Open the file picker.
+ saveAboutVersionTextActivityResultLauncher.launch(getString(R.string.privacy_browser_version_txt, BuildConfig.VERSION_NAME))
+
+ // Consume the event.
+ return true
+ }
+
+ R.id.save_image -> { // Save image.
+ // Open the file picker.
+ saveAboutVersionImageActivityResultLauncher.launch(getString(R.string.privacy_browser_version_png, BuildConfig.VERSION_NAME))
+
+ // Consume the event.
+ return true
+ }
+
+ else -> { // The home button was selected.
+ // Do not consume the event.
+ return false
+ }
+ }
+ }
+
+ }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+
// 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_scrollview, container, false)
val deviceLabel = getString(R.string.device)
val bootloaderLabel = getString(R.string.bootloader)
val androidLabel = getString(R.string.android)
+ val securityPatchLabel = getString(R.string.security_patch)
val buildLabel = getString(R.string.build)
val kernelLabel = getString(R.string.kernel)
+ val webViewProviderLabel = getString(R.string.webview_provider)
val webViewVersionLabel = getString(R.string.webview_version)
appConsumedMemoryLabel = getString(R.string.app_consumed_memory)
appAvailableMemoryLabel = getString(R.string.app_available_memory)
val serialNumberLabel = getString(R.string.serial_number)
val 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.
- val webViewLayout = layoutInflater.inflate(R.layout.bare_webview, container, false)
- val tabLayoutWebView = webViewLayout.findViewById<WebView>(R.id.bare_webview)
- val userAgentString = tabLayoutWebView.settings.userAgentString
+ // Get the current WebView package info.
+ val webViewPackageInfo = WebView.getCurrentWebViewPackage()!!
// Get the device's information and store it in strings.
val brand = Build.BRAND
val bootloader = Build.BOOTLOADER
val radio = Build.getRadioVersion()
val android = getString(R.string.api, Build.VERSION.RELEASE, Build.VERSION.SDK_INT)
+ val securityPatch = Build.VERSION.SECURITY_PATCH
val build = Build.DISPLAY
val kernel = System.getProperty("os.version")
-
- // Get the WebView version, selecting the substring that begins after `Chrome/` and goes until the next ` `.
- val webView = userAgentString.substring(userAgentString.indexOf("Chrome/") + 7, userAgentString.indexOf(" ", userAgentString.indexOf("Chrome/")))
+ val webViewPackageName = webViewPackageInfo.packageName
+ val webViewVersion = webViewPackageInfo.versionName
// Get the Orbot version name if Orbot is installed.
val orbot: String = try {
- // Store the version name. The newer `getPackageInfo()` may be used once the minimum API >= 33.
- @Suppress("DEPRECATION")
- requireContext().packageManager.getPackageInfo("org.torproject.android", 0).versionName
- } catch (exception: PackageManager.NameNotFoundException) { // Orbot is not installed.
+ // If the safe call (`?.`) is null, the Elvis operator (`?"`) returns the following value instead, which is an empty string.
+ requireContext().packageManager.getPackageInfo("org.torproject.android", 0).versionName ?: ""
+ } catch (exception: PackageManager.NameNotFoundException) {
// Store an empty string.
""
}
// Get the I2P version name if I2P is installed.
val i2p: String = try {
- // Check to see if the F-Droid flavor is installed. The newer `getPackageInfo()` may be used once the minimum API >= 33.
- @Suppress("DEPRECATION")
- requireContext().getString(R.string.fdroid_flavor, requireContext().packageManager.getPackageInfo("net.i2p.android.router", 0).versionName)
+ // Check to see if the F-Droid flavor is installed.
+ getString(R.string.fdroid_flavor, requireContext().packageManager.getPackageInfo("net.i2p.android.router", 0).versionName)
} catch (exception: PackageManager.NameNotFoundException) { // The F-Droid flavor is not installed.
try {
- // Check to see if the F-Droid flavor is installed. The newer `getPackageInfo()` may be used once the minimum API >= 33.
- @Suppress("DEPRECATION")
- requireContext().getString(R.string.google_play_flavor, requireContext().packageManager.getPackageInfo("net.i2p.android", 0).versionName)
+ // Check to see if the F-Droid flavor is installed.
+ getString(R.string.google_play_flavor, requireContext().packageManager.getPackageInfo("net.i2p.android", 0).versionName)
} catch (exception: PackageManager.NameNotFoundException) { // The Google Play flavor is not installed either.
// Store an empty string.
""
// Get the OpenKeychain version name if it is installed.
val openKeychain: String = try {
- // Store the version name. The newer `getPackageInfo()` may be used once the minimum API >= 33.
- @Suppress("DEPRECATION")
- requireContext().packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName
- } catch (exception: PackageManager.NameNotFoundException) { // OpenKeychain is not installed.
+ // If the safe call (`?.`) is null, the Elvis operator (`?"`) returns the following value instead, which is an empty string.
+ requireContext().packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName ?: ""
+ } catch (exception: PackageManager.NameNotFoundException) {
// Store an empty string.
""
}
val deviceStringBuilder = SpannableStringBuilder(deviceLabel + device)
val bootloaderStringBuilder = SpannableStringBuilder(bootloaderLabel + bootloader)
val androidStringBuilder = SpannableStringBuilder(androidLabel + android)
+ val securityPatchStringBuilder = SpannableStringBuilder(securityPatchLabel + securityPatch)
val buildStringBuilder = SpannableStringBuilder(buildLabel + build)
val kernelStringBuilder = SpannableStringBuilder(kernelLabel + kernel)
- val webViewVersionStringBuilder = SpannableStringBuilder(webViewVersionLabel + webView)
+ val webViewProviderStringBuilder = SpannableStringBuilder(webViewProviderLabel + webViewPackageName)
+ val webViewVersionStringBuilder = SpannableStringBuilder(webViewVersionLabel + webViewVersion)
val easyListStringBuilder = SpannableStringBuilder(easyListLabel + filterListsVersions[0])
val easyPrivacyStringBuilder = SpannableStringBuilder(easyPrivacyLabel + filterListsVersions[1])
val fanboyAnnoyanceStringBuilder = SpannableStringBuilder(fanboyAnnoyanceLabel + filterListsVersions[2])
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)
+ securityPatchStringBuilder.setSpan(blueColorSpan, securityPatchLabel.length, securityPatchStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
buildStringBuilder.setSpan(blueColorSpan, buildLabel.length, buildStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
kernelStringBuilder.setSpan(blueColorSpan, kernelLabel.length, kernelStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+ webViewProviderStringBuilder.setSpan(blueColorSpan, webViewProviderLabel.length, webViewProviderStringBuilder.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)
deviceTextView.text = deviceStringBuilder
bootloaderTextView.text = bootloaderStringBuilder
androidTextView.text = androidStringBuilder
+ securityPatchTextView.text = securityPatchStringBuilder
buildTextView.text = buildStringBuilder
kernelTextView.text = kernelStringBuilder
+ webViewProviderTextView.text = webViewProviderStringBuilder
webViewVersionTextView.text = webViewVersionStringBuilder
easyListTextView.text = easyListStringBuilder
easyPrivacyTextView.text = easyPrivacyStringBuilder
radioTextView.visibility = View.GONE
}
- // Setup the label.
- val securityPatchLabel = getString(R.string.security_patch)
-
- // Get the security patch version.
- val securityPatch = Build.VERSION.SECURITY_PATCH
-
- // Create a spannable string builder.
- val securityPatchStringBuilder = SpannableStringBuilder(securityPatchLabel + securityPatch)
-
- // Set the span to display the security patch version in blue.
- securityPatchStringBuilder.setSpan(blueColorSpan, securityPatchLabel.length, securityPatchStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-
- // Display the string in the text view.
- securityPatchTextView.text = securityPatchStringBuilder
-
- // Create the WebView provider label.
- val webViewProviderLabel = getString(R.string.webview_provider)
-
- // Get the current WebView package info.
- val webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(requireContext())!!
-
- // Get the WebView provider name.
- val webViewPackageName = webViewPackageInfo.packageName
-
- // Create the spannable string builder.
- val webViewProviderStringBuilder = SpannableStringBuilder(webViewProviderLabel + webViewPackageName)
-
- // Apply the coloration.
- webViewProviderStringBuilder.setSpan(blueColorSpan, webViewProviderLabel.length, webViewProviderStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
-
- // Display the WebView provider.
- webViewProviderTextView.text = webViewProviderStringBuilder
-
// Only populate the Orbot text view if it is installed.
if (orbot.isNotEmpty()) {
// Setup the label.
// Get the first package signature. Suppress the lint warning about the need to be careful in implementing comparison of certificates for security purposes.
// Once the minimum API >= 28, `GET_SIGNING_CERTIFICATES` can be used instead. Once the minimum API >= 33, the newer `getPackageInfo()` may be used.
@Suppress("DEPRECATION")
- @SuppressLint("PackageManagerGetSignatures") val packageSignature = requireContext().packageManager.getPackageInfo(requireContext().packageName,PackageManager.GET_SIGNATURES)
- .signatures[0]
+ @SuppressLint("PackageManagerGetSignatures") val packageSignature = requireContext().packageManager.getPackageInfo(requireContext().packageName,PackageManager.GET_SIGNATURES).signatures!![0]
// Convert the signature to a byte array input stream.
val certificateByteArrayInputStream: InputStream = ByteArrayInputStream(packageSignature.toByteArray())
// Scroll the tab if the saved instance state is not null.
if (savedInstanceState != null) {
aboutVersionLayout.post {
- aboutVersionLayout.scrollX = savedInstanceState.getInt("scroll_x")
- aboutVersionLayout.scrollY = savedInstanceState.getInt("scroll_y")
+ aboutVersionLayout.scrollY = savedInstanceState.getInt(SCROLL_Y)
}
}
return aboutVersionLayout
}
- override fun onCreateOptionsMenu(menu: Menu, 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 fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
- // Run the appropriate commands.
- when (menuItem.itemId) {
- R.id.copy -> { // Copy.
- // Get the about version string.
- val aboutVersionString = getAboutVersionString()
-
- // Get a handle for the clipboard manager.
- val clipboardManager = (requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
-
- // Save the about version string in a clip data.
- val 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
- }
-
- R.id.share -> { // Share.
- // Get the about version string.
- val aboutString = getAboutVersionString()
-
- // Create an email intent.
- val emailIntent = Intent(Intent.ACTION_SEND)
-
- // Add the about version string to the intent.
- emailIntent.putExtra(Intent.EXTRA_TEXT, aboutString)
-
- // Set the MIME type.
- emailIntent.type = "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
- }
-
- R.id.save_text -> { // Save text.
- // Open the file picker.
- saveAboutVersionTextActivityResultLauncher.launch(getString(R.string.privacy_browser_version_txt, BuildConfig.VERSION_NAME))
-
- // Consume the event.
- return true
- }
-
- R.id.save_image -> { // Save image.
- // Open the file picker.
- saveAboutVersionImageActivityResultLauncher.launch(getString(R.string.privacy_browser_version_png, BuildConfig.VERSION_NAME))
-
- // Consume the event.
- return true
- }
- else -> { // The home button was selected.
- // Run the parents class on return.
- return super.onOptionsItemSelected(menuItem)
- }
- }
- }
-
override fun onSaveInstanceState(savedInstanceState: Bundle) {
// Run the default commands.
super.onSaveInstanceState(savedInstanceState)
- // Save the scroll positions.
- savedInstanceState.putInt("scroll_x", aboutVersionLayout.scrollX)
- savedInstanceState.putInt("scroll_y", aboutVersionLayout.scrollY)
+ // Save the scroll position.
+ savedInstanceState.putInt(SCROLL_Y, aboutVersionLayout.scrollY)
}
override fun onPause() {