2 * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.dialogs
22 import android.app.Dialog
23 import android.content.Context
24 import android.content.DialogInterface
25 import android.os.Bundle
26 import android.view.WindowManager
28 import androidx.appcompat.app.AlertDialog
29 import androidx.fragment.app.DialogFragment
30 import androidx.preference.PreferenceManager
31 import androidx.viewpager.widget.ViewPager
33 import com.google.android.material.tabs.TabLayout
35 import com.stoutner.privacybrowser.R
36 import com.stoutner.privacybrowser.activities.MainWebViewActivity
37 import com.stoutner.privacybrowser.adapters.PinnedMismatchPagerAdapter
38 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
39 import com.stoutner.privacybrowser.views.NestedScrollWebView
41 // Define the class constants.
42 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
44 class PinnedMismatchDialog : DialogFragment() {
46 fun displayDialog(webViewFragmentId: Long): PinnedMismatchDialog {
47 // Create an arguments bundle.
48 val argumentsBundle = Bundle()
50 // Store the WebView fragment ID in the bundle.
51 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
53 // Create a new instance of the pinned mismatch dialog.
54 val pinnedMismatchDialog = PinnedMismatchDialog()
56 // Add the arguments bundle to the new instance.
57 pinnedMismatchDialog.arguments = argumentsBundle
60 return pinnedMismatchDialog
64 // Declare the class variables.
65 private lateinit var pinnedMismatchListener: PinnedMismatchListener
67 // The public interface is used to send information back to the parent activity.
68 interface PinnedMismatchListener {
69 fun pinnedErrorGoBack()
72 override fun onAttach(context: Context) {
73 // Run the default commands.
74 super.onAttach(context)
76 // Get a handle for the listener from the launching context.
77 pinnedMismatchListener = context as PinnedMismatchListener
80 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
81 // Try to create the dialog. This will fail if the app was restarted while the dialog is shown because the WebView view pager will not have been populated yet.
83 // Get the WebView fragment ID.
84 val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
86 // Get the current position of this WebView fragment.
87 val webViewPosition = MainWebViewActivity.webViewStateAdapter!!.getPositionForId(webViewFragmentId)
89 // Get the WebView tab fragment.
90 val webViewTabFragment = MainWebViewActivity.webViewStateAdapter!!.getPageFragment(webViewPosition)
92 // Get the fragment view.
93 val fragmentView = webViewTabFragment.requireView()
95 // Get a handle for the current WebView.
96 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)!!
98 // Use an alert dialog builder to create the alert dialog.
99 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
102 dialogBuilder.setIcon(R.drawable.ssl_certificate)
105 dialogBuilder.setTitle(R.string.pinned_mismatch)
108 dialogBuilder.setView(R.layout.pinned_mismatch_linearlayout)
110 // Set the update button listener.
111 dialogBuilder.setNeutralButton(R.string.update) { _: DialogInterface?, _: Int ->
112 // Get the current SSL certificate.
113 val currentSslCertificate = nestedScrollWebView.certificate!!
115 // Get the dates from the certificate.
116 val currentSslStartDate = currentSslCertificate.validNotBeforeDate
117 val currentSslEndDate = currentSslCertificate.validNotAfterDate
119 // Convert the dates into longs. If the date is null, a long value of `0` will be stored in the domains database entry.
120 val currentSslStartDateLong: Long = currentSslStartDate?.time ?: 0
121 val currentSslEndDateLong: Long = currentSslEndDate?.time ?: 0
123 // Initialize the database handler.
124 val domainsDatabaseHelper = DomainsDatabaseHelper(requireContext())
126 // Update the SSL certificate if it is pinned.
127 if (nestedScrollWebView.hasPinnedSslCertificate()) {
128 // Update the pinned SSL certificate in the domain database.
129 domainsDatabaseHelper.updatePinnedSslCertificate(nestedScrollWebView.domainSettingsDatabaseId, currentSslCertificate.issuedTo.cName, currentSslCertificate.issuedTo.oName,
130 currentSslCertificate.issuedTo.uName, currentSslCertificate.issuedBy.cName, currentSslCertificate.issuedBy.oName, currentSslCertificate.issuedBy.uName, currentSslStartDateLong,
131 currentSslEndDateLong)
133 // Update the pinned SSL certificate in the nested scroll WebView.
134 nestedScrollWebView.setPinnedSslCertificate(currentSslCertificate.issuedTo.cName, currentSslCertificate.issuedTo.oName, currentSslCertificate.issuedTo.uName,
135 currentSslCertificate.issuedBy.cName, currentSslCertificate.issuedBy.oName, currentSslCertificate.issuedBy.uName, currentSslStartDate, currentSslEndDate)
138 // Update the IP addresses if they are pinned.
139 if (nestedScrollWebView.pinnedIpAddresses != "") {
140 // Update the pinned IP addresses in the domain database.
141 domainsDatabaseHelper.updatePinnedIpAddresses(nestedScrollWebView.domainSettingsDatabaseId, nestedScrollWebView.currentIpAddresses)
143 // Update the pinned IP addresses in the nested scroll WebView.
144 nestedScrollWebView.pinnedIpAddresses = nestedScrollWebView.currentIpAddresses
148 // Set the back button listener.
149 dialogBuilder.setNegativeButton(R.string.back) { _: DialogInterface?, _: Int ->
150 if (nestedScrollWebView.canGoBack()) { // There is a back page in the history.
151 // Invoke the navigate history listener in the calling activity. These commands cannot be run here because they need access to `applyDomainSettings()`.
152 pinnedMismatchListener.pinnedErrorGoBack()
153 } else { // There are no pages to go back to.
155 nestedScrollWebView.loadUrl("")
159 // Set the proceed button listener.
160 dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int ->
161 // Do not check the pinned information for this domain again until the domain changes.
162 nestedScrollWebView.ignorePinnedDomainInformation = true
165 // Create an alert dialog from the alert dialog builder.
166 val alertDialog = dialogBuilder.create()
168 // Get a handle for the shared preferences.
169 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
171 // Get the screenshot preference.
172 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
174 // Disable screenshots if not allowed.
175 if (!allowScreenshots) {
176 // Disable screenshots.
177 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
180 // The alert dialog must be shown before items in the layout can be modified.
183 // Get handles for the views.
184 val viewPager = alertDialog.findViewById<ViewPager>(R.id.pinned_ssl_certificate_mismatch_viewpager)!!
185 val tabLayout = alertDialog.findViewById<TabLayout>(R.id.pinned_ssl_certificate_mismatch_tablayout)!!
187 // Initialize the pinned mismatch pager adapter.
188 val pinnedMismatchPagerAdapter = PinnedMismatchPagerAdapter(requireContext(), layoutInflater, webViewFragmentId)
190 // Set the view pager adapter.
191 viewPager.adapter = pinnedMismatchPagerAdapter
193 // Connect the tab layout to the view pager.
194 tabLayout.setupWithViewPager(viewPager)
196 // Return the alert dialog.
198 } catch (exception: Exception) { // The app was restarted while the dialog was displayed.
199 // Dismiss this new instance of the dialog. Amazingly, the old instance will be restored by Android and, even more amazingly, will be fully functional.
202 // Use an alert dialog builder to create an empty alert dialog.
203 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
205 // Return the empty alert dialog.
206 return dialogBuilder.create()