2 * Copyright 2017-2022 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.activities
22 import android.app.Activity
23 import android.content.Context
24 import android.database.Cursor
25 import android.os.Bundle
26 import android.os.Handler
27 import android.view.Menu
28 import android.view.MenuItem
29 import android.view.View
30 import android.view.ViewGroup
31 import android.view.WindowManager
32 import android.widget.EditText
33 import android.widget.ListView
34 import android.widget.RadioButton
35 import android.widget.ScrollView
36 import android.widget.Spinner
37 import android.widget.TextView
38 import androidx.activity.OnBackPressedCallback
40 import androidx.appcompat.app.AppCompatActivity
41 import androidx.appcompat.widget.SwitchCompat
42 import androidx.appcompat.widget.Toolbar
43 import androidx.core.app.NavUtils
44 import androidx.cursoradapter.widget.CursorAdapter
45 import androidx.fragment.app.DialogFragment
46 import androidx.preference.PreferenceManager
48 import com.google.android.material.floatingactionbutton.FloatingActionButton
49 import com.google.android.material.snackbar.Snackbar
51 import com.stoutner.privacybrowser.R
52 import com.stoutner.privacybrowser.dialogs.AddDomainDialog.AddDomainListener
53 import com.stoutner.privacybrowser.dialogs.AddDomainDialog.Companion.addDomain
54 import com.stoutner.privacybrowser.fragments.DomainSettingsFragment
55 import com.stoutner.privacybrowser.fragments.DomainsListFragment
56 import com.stoutner.privacybrowser.fragments.DomainsListFragment.DismissSnackbarInterface
57 import com.stoutner.privacybrowser.fragments.DomainsListFragment.SaveDomainSettingsInterface
58 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
60 // Define the class constants.
61 private const val DOMAIN_SETTINGS_DATABASE_ID = "domain_settings_database_id"
62 private const val DOMAIN_SETTINGS_DISPLAYED = "domain_settings_displayed"
63 private const val DOMAIN_SETTINGS_SCROLL_Y = "domain_settings_scroll_y"
64 private const val LISTVIEW_POSITION = "listview_position"
66 class DomainsActivity : AppCompatActivity(), AddDomainListener, DismissSnackbarInterface, SaveDomainSettingsInterface {
68 // Define the public constants.
69 const val CLOSE_ON_BACK = "close_on_back"
70 const val CURRENT_IP_ADDRESSES = "current_ip_addresses"
71 const val CURRENT_URL = "current_url"
72 const val LOAD_DOMAIN = "load_domain"
73 const val SSL_END_DATE = "ssl_end_date"
74 const val SSL_ISSUED_BY_CNAME = "ssl_issued_by_cname"
75 const val SSL_ISSUED_BY_ONAME = "ssl_issued_by_oname"
76 const val SSL_ISSUED_BY_UNAME = "ssl_issued_by_uname"
77 const val SSL_ISSUED_TO_CNAME = "ssl_issued_to_cname"
78 const val SSL_ISSUED_TO_ONAME = "ssl_issued_to_oname"
79 const val SSL_ISSUED_TO_UNAME = "ssl_issued_to_uname"
80 const val SSL_START_DATE = "ssl_start_date"
82 // Define the public variables.
83 var currentDomainDatabaseId = 0 // Used in `DomainsListFragment`.
84 var dismissingSnackbar = false // Used in `DomainsListFragment`.
85 var domainsListViewPosition = 0 // Used in `DomainsListFragment`.
86 var sslEndDateLong: Long = 0 // Used in `DomainsSettingsFragment`.
87 var sslStartDateLong: Long = 0 // Used in `DomainSettingsFragment`.
88 var twoPanedMode = false // Used in `DomainsListFragment`.
90 // Declare the public views. They are used in `DomainsListFragment`.
91 lateinit var deleteMenuItem: MenuItem
93 // Declare the SSL certificate and IP address strings.
94 var currentIpAddresses: String? = null // Used in `DomainSettingsFragment`.
95 var sslIssuedToCName: String? = null // Used in `DomainSettingsFragment`.
96 var sslIssuedToOName: String? = null // Used in `DomainSettingsFragment`.
97 var sslIssuedToUName: String? = null // Used in `DomainSettingsFragment`.
98 var sslIssuedByCName: String? = null // Used in `DomainSettingsFragment`.
99 var sslIssuedByOName: String? = null // Used in `DomainSettingsFragment`.
100 var sslIssuedByUName: String? = null // Used in `DomainSettingsFragment`.
103 // Declare the class views.
104 private lateinit var addDomainFAB: FloatingActionButton
105 private lateinit var coordinatorLayout: View
106 private var domainsListView: ListView? = null
107 private var undoDeleteSnackbar: Snackbar? = null
109 // Declare the class variables.
110 private lateinit var domainsDatabaseHelper: DomainsDatabaseHelper
112 // Define the class variables.
113 private var appRestarted = false
114 private var closeActivityAfterDismissingSnackbar = false
115 private var closeOnBack = false
116 private var deletedDomainPosition = 0
117 private var domainSettingsDatabaseIdBeforeRestart = 0
118 private var domainSettingsDisplayedBeforeRestart = false
119 private var domainSettingsScrollY = 0
120 private var goDirectlyToDatabaseId = -1
122 override fun onCreate(savedInstanceState: Bundle?) {
123 // Get a handle for the shared preferences.
124 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
126 // Get the preferences.
127 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
128 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
130 // Disable screenshots if not allowed.
131 if (!allowScreenshots) {
132 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
135 // Run the default commands.
136 super.onCreate(savedInstanceState)
138 // Process the saved instance state if it is not null.
139 if (savedInstanceState != null) {
140 // Extract the values from the saved instance state if it is not null.
141 domainsListViewPosition = savedInstanceState.getInt(LISTVIEW_POSITION)
142 domainSettingsDisplayedBeforeRestart = savedInstanceState.getBoolean(DOMAIN_SETTINGS_DISPLAYED)
143 domainSettingsDatabaseIdBeforeRestart = savedInstanceState.getInt(DOMAIN_SETTINGS_DATABASE_ID)
144 domainSettingsScrollY = savedInstanceState.getInt(DOMAIN_SETTINGS_SCROLL_Y)
146 // Set the app restarted flag.
150 // Get the launching intent
153 // Extract the domain to load if there is one. `-1` is the default value.
154 goDirectlyToDatabaseId = intent.getIntExtra(LOAD_DOMAIN, -1)
156 // Get the status of close-on-back, which is true when the domains activity is called from the options menu.
157 closeOnBack = intent.getBooleanExtra(CLOSE_ON_BACK, false)
159 // Get the current URL.
160 val currentUrl = intent.getStringExtra(CURRENT_URL)
162 // Store the current SSL certificate information in class variables.
163 sslIssuedToCName = intent.getStringExtra(SSL_ISSUED_TO_CNAME)
164 sslIssuedToOName = intent.getStringExtra(SSL_ISSUED_TO_ONAME)
165 sslIssuedToUName = intent.getStringExtra(SSL_ISSUED_TO_UNAME)
166 sslIssuedByCName = intent.getStringExtra(SSL_ISSUED_BY_CNAME)
167 sslIssuedByOName = intent.getStringExtra(SSL_ISSUED_BY_ONAME)
168 sslIssuedByUName = intent.getStringExtra(SSL_ISSUED_BY_UNAME)
169 sslStartDateLong = intent.getLongExtra(SSL_START_DATE, 0)
170 sslEndDateLong = intent.getLongExtra(SSL_END_DATE, 0)
171 currentIpAddresses = intent.getStringExtra(CURRENT_IP_ADDRESSES)
173 // Set the content view.
175 setContentView(R.layout.domains_bottom_appbar)
177 setContentView(R.layout.domains_top_appbar)
179 // Get handles for the views.
180 coordinatorLayout = findViewById(R.id.domains_coordinatorlayout)
181 val toolbar = findViewById<Toolbar>(R.id.domains_toolbar)
182 addDomainFAB = findViewById(R.id.add_domain_fab)
184 // Set the support action bar.
185 setSupportActionBar(toolbar)
187 // Get a handle for the action bar.
188 val actionBar = supportActionBar!!
190 // Set the back arrow on the action bar.
191 actionBar.setDisplayHomeAsUpEnabled(true)
193 // Initialize the database handler.
194 domainsDatabaseHelper = DomainsDatabaseHelper(this)
196 // Determine if the device is in two pane mode. `domain_settings_fragment_container` does not exist on devices with a width less than 900dp.
197 twoPanedMode = (findViewById<View?>(R.id.domain_settings_fragment_container) != null)
199 // Configure the add domain floating action button.
200 addDomainFAB.setOnClickListener {
201 // Create an add domain dialog.
202 val addDomainDialog: DialogFragment = addDomain(currentUrl)
204 // Show the add domain dialog.
205 addDomainDialog.show(supportFragmentManager, resources.getString(R.string.add_domain))
208 // Control what the system back command does.
209 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
210 override fun handleOnBackPressed() {
211 if (twoPanedMode) { // The device is in two-paned mode.
212 // Save the current domain settings if the domain settings fragment is displayed.
213 if (findViewById<View?>(R.id.domain_settings_scrollview) != null)
214 saveDomainSettings(coordinatorLayout)
216 // Dismiss the undo delete snackbar if it is shown.
217 if (undoDeleteSnackbar != null && undoDeleteSnackbar!!.isShown) {
218 // Set the close flag.
219 closeActivityAfterDismissingSnackbar = true
221 // Dismiss the snackbar.
222 undoDeleteSnackbar!!.dismiss()
227 } else if (closeOnBack) { // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
228 // Save the current domain settings.
229 saveDomainSettings(coordinatorLayout)
233 } else if (findViewById<View?>(R.id.domain_settings_scrollview) != null) { // The device is in single-paned mode and domain settings fragment is displayed.
234 // Save the current domain settings.
235 saveDomainSettings(coordinatorLayout)
237 // Instantiate a new domains list fragment.
238 val domainsListFragment = DomainsListFragment()
240 // Display the domains list fragment.
241 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commitNow()
243 // Populate the list of domains. `-1` highlights the first domain if in two-paned mode. It has no effect in single-paned mode.
244 populateDomainsListView(-1, domainsListViewPosition)
246 // Show the add domain floating action button.
249 // Hide the delete menu item.
250 deleteMenuItem.isVisible = false
251 } else { // The device is in single-paned mode and the domain list fragment is displayed.
252 // Dismiss the undo delete SnackBar if it is shown.
253 if (undoDeleteSnackbar != null && undoDeleteSnackbar!!.isShown) {
254 // Set the close flag.
255 closeActivityAfterDismissingSnackbar = true
257 // Dismiss the snackbar.
258 undoDeleteSnackbar!!.dismiss()
267 // Register the on back pressed callback.
268 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
271 override fun onCreateOptionsMenu(menu: Menu): Boolean {
273 menuInflater.inflate(R.menu.domains_options_menu, menu)
275 // Get a handle for the delete menu item.
276 deleteMenuItem = menu.findItem(R.id.delete_domain)
278 // Only display the delete menu item (initially) in two-paned mode.
279 deleteMenuItem.isVisible = twoPanedMode
281 // Display the fragments. This must be done from `onCreateOptionsMenu()` instead of `onCreate()` because `populateDomainsListView()` needs `deleteMenuItem` to be inflated.
282 if (appRestarted && domainSettingsDisplayedBeforeRestart) { // The app was restarted, possibly because the device was rotated, and domain settings were displayed previously.
283 // Reset the app restarted flag.
286 if (twoPanedMode) { // The device is in two-paned mode.
287 // Initialize the domains list fragment.
288 val domainsListFragment = DomainsListFragment()
290 // Display the domains list fragment.
291 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commitNow()
293 // Populate the list of domains and highlight the domain that was highlighted before the restart.
294 populateDomainsListView(domainSettingsDatabaseIdBeforeRestart, domainsListViewPosition)
295 } else { // The device is in single-paned mode.
296 // Store the current domain database ID.
297 currentDomainDatabaseId = domainSettingsDatabaseIdBeforeRestart
299 // Create an arguments bundle.
300 val argumentsBundle = Bundle()
302 // Add the domain settings arguments.
303 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId)
304 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY)
306 // Instantiate a new domain settings fragment.
307 val domainSettingsFragment = DomainSettingsFragment()
309 // Add the arguments bundle to the domain settings fragment.
310 domainSettingsFragment.arguments = argumentsBundle
312 // Show the delete menu item.
313 deleteMenuItem.isVisible = true
315 // Hide the add domain floating action button.
318 // Display the domain settings fragment.
319 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commitNow()
321 } else { // The device was not restarted or, if it was, domain settings were not displayed previously.
322 if (goDirectlyToDatabaseId >= 0) { // Load the indicated domain settings.
323 // Store the current domain database ID.
324 currentDomainDatabaseId = goDirectlyToDatabaseId
326 // Check if the device is in two-paned mode.
327 if (twoPanedMode) { // The device is in two-paned mode.
328 // Instantiate a new domains list fragment.
329 val domainsListFragment = DomainsListFragment()
331 // Display the domains list fragment.
332 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commitNow()
334 // Populate the list of domains.
335 populateDomainsListView(goDirectlyToDatabaseId, domainsListViewPosition)
336 } else { // The device is in single-paned mode.
337 // Create an arguments bundle.
338 val argumentsBundle = Bundle()
340 // Add the domain settings to arguments bundle.
341 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, goDirectlyToDatabaseId)
342 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY)
344 // Instantiate a new domain settings fragment.
345 val domainSettingsFragment = DomainSettingsFragment()
347 // Add the arguments bundle to the domain settings fragment.
348 domainSettingsFragment.arguments = argumentsBundle
350 // Show the delete menu item.
351 deleteMenuItem.isVisible = true
353 // Hide the add domain floating action button.
356 // Display the domain settings fragment.
357 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commitNow()
359 } else { // Highlight the first domain.
360 // Instantiate a new domains list fragment.
361 val domainsListFragment = DomainsListFragment()
363 // Display the domain list fragment.
364 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commitNow()
366 // Populate the list of domains. `-1` highlights the first domain.
367 populateDomainsListView(-1, domainsListViewPosition)
375 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
376 // Run the command according to the selected menu item.
377 when (menuItem.itemId) {
378 android.R.id.home -> { // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
379 // Check if the device is in two-paned mode.
380 if (twoPanedMode) { // The device is in two-paned mode.
381 // Save the current domain settings if the domain settings fragment is displayed.
382 if (findViewById<View?>(R.id.domain_settings_scrollview) != null)
383 saveDomainSettings(coordinatorLayout)
385 // Dismiss the undo delete snackbar if it is shown.
386 if (undoDeleteSnackbar != null && undoDeleteSnackbar!!.isShown) {
387 // Set the close flag.
388 closeActivityAfterDismissingSnackbar = true
390 // Dismiss the snackbar.
391 undoDeleteSnackbar!!.dismiss()
396 } else if (closeOnBack) { // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
397 // Save the current domain settings.
398 saveDomainSettings(coordinatorLayout)
402 } else if (findViewById<View?>(R.id.domain_settings_scrollview) != null) { // The device is in single-paned mode and the domain settings fragment is displayed.
403 // Save the current domain settings.
404 saveDomainSettings(coordinatorLayout)
406 // Instantiate a new domains list fragment.
407 val domainsListFragment = DomainsListFragment()
409 // Display the domains list fragment.
410 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commitNow()
412 // Populate the list of domains. `-1` highlights the first domain if in two-paned mode. It has no effect in single-paned mode.
413 populateDomainsListView(-1, domainsListViewPosition)
415 // Show the add domain floating action button.
418 // Hide the delete menu item.
419 deleteMenuItem.isVisible = false
420 } else { // The device is in single-paned mode and domains list fragment is displayed.
421 // Dismiss the undo delete snackbar if it is shown.
422 if (undoDeleteSnackbar != null && undoDeleteSnackbar!!.isShown) {
423 // Set the close flag.
424 closeActivityAfterDismissingSnackbar = true
426 // Dismiss the snackbar.
427 undoDeleteSnackbar!!.dismiss()
435 R.id.delete_domain -> { // Delete.
436 // Get a handle for the activity (used in an inner class below).
437 val activity: Activity = this
439 // Check to see if the domain settings were loaded directly for editing of this app in single-paned mode.
440 if (closeOnBack && !twoPanedMode) { // The activity should delete the domain settings and exit straight to the the main WebView activity.
441 // Delete the selected domain.
442 domainsDatabaseHelper.deleteDomain(currentDomainDatabaseId)
445 NavUtils.navigateUpFromSameTask(activity)
446 } else { // A snackbar should be shown before deleting the domain settings.
447 // Reset close-on-back, which otherwise can cause errors if the system attempts to save settings for a domain that no longer exists.
450 // Store a copy of the current domain database ID because it could change while the snackbar is displayed.
451 val databaseIdToDelete = currentDomainDatabaseId
453 // Update the fragments and menu items.
454 if (twoPanedMode) { // Two-paned mode.
455 // Store the deleted domain position, which is needed if undo is selected in the snackbar.
456 deletedDomainPosition = domainsListView!!.checkedItemPosition
458 // Disable the delete menu item.
459 deleteMenuItem.isEnabled = false
461 // Remove the domain settings fragment.
462 supportFragmentManager.beginTransaction().remove(supportFragmentManager.findFragmentById(R.id.domain_settings_fragment_container)!!).commitNow()
463 } else { // Single-paned mode.
464 // Instantiate a new domains list fragment.
465 val domainsListFragment = DomainsListFragment()
467 // Display the domains list fragment.
468 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commitNow()
470 // Show the add domain floating action button.
473 // Hide the delete menu item.
474 deleteMenuItem.isVisible = false
477 // Get a cursor that does not show the domain to be deleted.
478 val domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete)
480 // Setup the domains pending delete cursor adapter.
481 val domainsPendingDeleteCursorAdapter: CursorAdapter = object : CursorAdapter(this, domainsPendingDeleteCursor, false) {
482 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
483 // Inflate the individual item layout.
484 return layoutInflater.inflate(R.layout.domain_name_linearlayout, parent, false)
487 override fun bindView(view: View, context: Context, cursor: Cursor) {
488 // Get the domain name string.
489 val domainNameString = cursor.getString(cursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME))
491 // Get a handle for the domain name text view.
492 val domainNameTextView = view.findViewById<TextView>(R.id.domain_name_textview)
494 // Display the domain name.
495 domainNameTextView.text = domainNameString
499 // Update the handle for the current domains list view.
500 domainsListView = findViewById(R.id.domains_listview)
502 // Update the list view.
503 domainsListView!!.adapter = domainsPendingDeleteCursorAdapter
505 // Display a snackbar.
506 undoDeleteSnackbar = Snackbar.make(domainsListView!!, R.string.domain_deleted, Snackbar.LENGTH_LONG)
507 .setAction(R.string.undo) {} // Do nothing because everything will be handled by `onDismissed()` below.
508 .addCallback(object : Snackbar.Callback() {
509 override fun onDismissed(snackbar: Snackbar, event: Int) {
510 // Run commands based on the event.
511 if (event == DISMISS_EVENT_ACTION) { // The user pushed the `Undo` button.
512 // Create an arguments bundle.
513 val argumentsBundle = Bundle()
515 // Store the domains settings in the arguments bundle.
516 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete)
517 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY)
519 // Instantiate a new domain settings fragment.
520 val domainSettingsFragment = DomainSettingsFragment()
522 // Add the arguments bundle to the domain settings fragment.
523 domainSettingsFragment.arguments = argumentsBundle
525 // Display the correct fragments.
526 if (twoPanedMode) { // The device in in two-paned mode.
527 // Get a cursor with the current contents of the domains database.
528 val undoDeleteDomainsCursor = domainsDatabaseHelper.domainNameCursorOrderedByDomain
530 // Setup the domains cursor adapter.
531 val undoDeleteDomainsCursorAdapter: CursorAdapter = object : CursorAdapter(applicationContext, undoDeleteDomainsCursor, false) {
532 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
533 // Inflate the individual item layout.
534 return layoutInflater.inflate(R.layout.domain_name_linearlayout, parent, false)
537 override fun bindView(view: View, context: Context, cursor: Cursor) {
538 /// Get the domain name string.
539 val domainNameString = cursor.getString(cursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME))
541 // Get a handle for the domain name text view.
542 val domainNameTextView = view.findViewById<TextView>(R.id.domain_name_textview)
544 // Display the domain name.
545 domainNameTextView.text = domainNameString
549 // Update the domains list view.
550 domainsListView!!.adapter = undoDeleteDomainsCursorAdapter
552 // Select the previously deleted domain in the list view.
553 domainsListView!!.setItemChecked(deletedDomainPosition, true)
555 // Display the domain settings fragment.
556 supportFragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commitNow()
558 // Enable the delete menu item.
559 deleteMenuItem.isEnabled = true
560 } else { // The device in in one-paned mode.
561 // Display the domain settings fragment.
562 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commitNow()
564 // Hide the add domain floating action button.
567 // Show and enable the delete menu item.
568 deleteMenuItem.isVisible = true
570 // Display the domain settings fragment.
571 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commitNow()
573 } else { // The snackbar was dismissed without the undo button being pushed.
574 // Delete the selected domain.
575 domainsDatabaseHelper.deleteDomain(databaseIdToDelete)
577 // Enable the delete menu item if the system was waiting for a snackbar to be dismissed.
578 if (dismissingSnackbar) {
579 // Create a runnable to enable the delete menu item.
580 val enableDeleteMenuItemRunnable = Runnable {
581 // Enable or show the delete menu item according to the display mode.
583 deleteMenuItem.isEnabled = true
585 deleteMenuItem.isVisible = true
587 // Reset the dismissing snackbar tracker.
588 dismissingSnackbar = false
591 // Instantiate a handler running the main looper.
592 val handler = Handler(mainLooper)
594 // Enable or show the delete menu icon after 100 milliseconds to make sure that the previous domain has been deleted from the database.
595 handler.postDelayed(enableDeleteMenuItemRunnable, 100)
598 // Close the activity if back was pressed.
599 if (closeActivityAfterDismissingSnackbar)
600 NavUtils.navigateUpFromSameTask(activity)
605 // Show the Snackbar.
606 undoDeleteSnackbar!!.show()
611 // Consume the event.
615 override fun onSaveInstanceState(savedInstanceState: Bundle) {
616 // Run the default commands.
617 super.onSaveInstanceState(savedInstanceState)
619 // Get a handle for the domain settings scrollview.
620 val domainSettingsScrollView = findViewById<ScrollView>(R.id.domain_settings_scrollview)
622 // Check to see if the domain settings scrollview exists.
623 if (domainSettingsScrollView == null) { // The domain settings are not displayed.
624 // Store the domain settings status in the bundle.
625 savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, false)
626 savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, -1)
627 savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, 0)
628 } else { // The domain settings are displayed.
629 // Save any changes that have been made to the domain settings.
630 saveDomainSettings(coordinatorLayout)
632 // Get the domain settings scroll Y.
633 val domainSettingsScrollY = domainSettingsScrollView.scrollY
635 // Store the domain settings status in the bundle.
636 savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, true)
637 savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, DomainSettingsFragment.databaseId)
638 savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, domainSettingsScrollY)
641 // Check to see if the domains listview exists.
642 if (domainsListView != null) {
643 // Get the domains listview position.
644 val domainsListViewPosition = domainsListView!!.firstVisiblePosition
646 // Store the listview position in the bundle.
647 savedInstanceState.putInt(LISTVIEW_POSITION, domainsListViewPosition)
651 override fun onAddDomain(dialogFragment: DialogFragment) {
652 // Dismiss the undo delete snackbar if it is currently displayed.
653 if (undoDeleteSnackbar != null && undoDeleteSnackbar!!.isShown)
654 undoDeleteSnackbar!!.dismiss()
656 // Get a handle for the domain name edit text.
657 val domainNameEditText = dialogFragment.dialog!!.findViewById<EditText>(R.id.domain_name_edittext)
659 // Get the domain name string.
660 val domainNameString = domainNameEditText.text.toString()
662 // Create the domain and store the database ID in the current domain database ID.
663 currentDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString)
665 // Display the newly created domain.
666 if (twoPanedMode) { // The device in in two-paned mode.
667 populateDomainsListView(currentDomainDatabaseId, 0)
668 } else { // The device is in single-paned mode.
669 // Hide the add domain floating action button.
672 // Show and enable the delete menu item.
673 deleteMenuItem.isVisible = true
675 // Create an arguments bundle.
676 val argumentsBundle = Bundle()
678 // Add the domain settings to the arguments bundle. The scroll Y should always be `0` on a new domain.
679 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId)
680 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, 0)
682 // Instantiate a new domain settings fragment.
683 val domainSettingsFragment = DomainSettingsFragment()
685 // Add the arguments bundle to the domain setting fragment.
686 domainSettingsFragment.arguments = argumentsBundle
688 // Display the domain settings fragment.
689 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commitNow()
693 override fun saveDomainSettings(view: View) {
694 // Get handles for the domain settings.
695 val domainNameEditText = view.findViewById<EditText>(R.id.domain_settings_name_edittext)
696 val javaScriptSwitch = view.findViewById<SwitchCompat>(R.id.javascript_switch)
697 val cookiesSwitch = view.findViewById<SwitchCompat>(R.id.cookies_switch)
698 val domStorageSwitch = view.findViewById<SwitchCompat>(R.id.dom_storage_switch)
699 val formDataSwitch = view.findViewById<SwitchCompat>(R.id.form_data_switch) // Form data can be removed once the minimum API >= 26.
700 val easyListSwitch = view.findViewById<SwitchCompat>(R.id.easylist_switch)
701 val easyPrivacySwitch = view.findViewById<SwitchCompat>(R.id.easyprivacy_switch)
702 val fanboysAnnoyanceSwitch = view.findViewById<SwitchCompat>(R.id.fanboys_annoyance_list_switch)
703 val fanboysSocialBlockingSwitch = view.findViewById<SwitchCompat>(R.id.fanboys_social_blocking_list_switch)
704 val ultraListSwitch = view.findViewById<SwitchCompat>(R.id.ultralist_switch)
705 val ultraPrivacySwitch = view.findViewById<SwitchCompat>(R.id.ultraprivacy_switch)
706 val blockAllThirdPartyRequestsSwitch = view.findViewById<SwitchCompat>(R.id.block_all_third_party_requests_switch)
707 val userAgentSpinner = view.findViewById<Spinner>(R.id.user_agent_spinner)
708 val customUserAgentEditText = view.findViewById<EditText>(R.id.custom_user_agent_edittext)
709 val xRequestedWithHeaderSpinner = view.findViewById<Spinner>(R.id.x_requested_with_header_spinner)
710 val fontSizeSpinner = view.findViewById<Spinner>(R.id.font_size_spinner)
711 val customFontSizeEditText = view.findViewById<EditText>(R.id.custom_font_size_edittext)
712 val swipeToRefreshSpinner = view.findViewById<Spinner>(R.id.swipe_to_refresh_spinner)
713 val webViewThemeSpinner = view.findViewById<Spinner>(R.id.webview_theme_spinner)
714 val wideViewportSpinner = view.findViewById<Spinner>(R.id.wide_viewport_spinner)
715 val displayWebpageImagesSpinner = view.findViewById<Spinner>(R.id.display_webpage_images_spinner)
716 val pinnedSslCertificateSwitch = view.findViewById<SwitchCompat>(R.id.pinned_ssl_certificate_switch)
717 val currentWebsiteCertificateRadioButton = view.findViewById<RadioButton>(R.id.current_website_certificate_radiobutton)
718 val pinnedIpAddressesSwitch = view.findViewById<SwitchCompat>(R.id.pinned_ip_addresses_switch)
719 val currentIpAddressesRadioButton = view.findViewById<RadioButton>(R.id.current_ip_addresses_radiobutton)
721 // Extract the data for the domain settings.
722 val domainNameString = domainNameEditText.text.toString()
723 val javaScript = javaScriptSwitch.isChecked
724 val cookies = cookiesSwitch.isChecked
725 val domStorage = domStorageSwitch.isChecked
726 val formData = formDataSwitch.isChecked // Form data can be removed once the minimum API >= 26.
727 val easyList = easyListSwitch.isChecked
728 val easyPrivacy = easyPrivacySwitch.isChecked
729 val fanboysAnnoyance = fanboysAnnoyanceSwitch.isChecked
730 val fanboysSocialBlocking = fanboysSocialBlockingSwitch.isChecked
731 val ultraList = ultraListSwitch.isChecked
732 val ultraPrivacy = ultraPrivacySwitch.isChecked
733 val blockAllThirdPartyRequests = blockAllThirdPartyRequestsSwitch.isChecked
734 val userAgentSwitchPosition = userAgentSpinner.selectedItemPosition
735 val xRequestedWithHeaderSwitchInt = xRequestedWithHeaderSpinner.selectedItemPosition
736 val fontSizeSwitchPosition = fontSizeSpinner.selectedItemPosition
737 val swipeToRefreshInt = swipeToRefreshSpinner.selectedItemPosition
738 val webViewThemeInt = webViewThemeSpinner.selectedItemPosition
739 val wideViewportInt = wideViewportSpinner.selectedItemPosition
740 val displayWebpageImagesInt = displayWebpageImagesSpinner.selectedItemPosition
741 val pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked
742 val pinnedIpAddress = pinnedIpAddressesSwitch.isChecked
744 // Get the user agent name.
745 val userAgentName: String = when (userAgentSwitchPosition) {
746 MainWebViewActivity.DOMAINS_SYSTEM_DEFAULT_USER_AGENT -> resources.getString(R.string.system_default_user_agent) // Set the user agent name to be `System default user agent`.
747 MainWebViewActivity.DOMAINS_CUSTOM_USER_AGENT -> customUserAgentEditText.text.toString() // Set the user agent name to be the custom user agent.
749 // Get the array of user agent names.
750 val userAgentNameArray = resources.getStringArray(R.array.user_agent_names)
752 // Set the user agent name from the array. The domain spinner has one more entry than the name array, so the position must be decremented.
753 userAgentNameArray[userAgentSwitchPosition - 1]
757 // Initialize the font size integer. `0` indicates the system default font size.
760 // Use a custom font size if it is selected.
761 if (fontSizeSwitchPosition == 1)
762 fontSizeInt = customFontSizeEditText.text.toString().toInt()
764 // Save the domain settings.
765 domainsDatabaseHelper.updateDomain(currentDomainDatabaseId, domainNameString, javaScript, cookies, domStorage, formData, easyList, easyPrivacy, fanboysAnnoyance, fanboysSocialBlocking, ultraList,
766 ultraPrivacy, blockAllThirdPartyRequests, userAgentName, xRequestedWithHeaderSwitchInt, fontSizeInt, swipeToRefreshInt, webViewThemeInt, wideViewportInt, displayWebpageImagesInt,
767 pinnedSslCertificate, pinnedIpAddress)
769 // Update the pinned SSL certificate if a new one is checked.
770 if (currentWebsiteCertificateRadioButton.isChecked)
771 domainsDatabaseHelper.updatePinnedSslCertificate(currentDomainDatabaseId, sslIssuedToCName!!, sslIssuedToOName!!, sslIssuedToUName!!, sslIssuedByCName!!, sslIssuedByOName!!, sslIssuedByUName!!,
772 sslStartDateLong, sslEndDateLong)
774 // Update the pinned IP addresses if new ones are checked.
775 if (currentIpAddressesRadioButton.isChecked)
776 domainsDatabaseHelper.updatePinnedIpAddresses(currentDomainDatabaseId, currentIpAddresses!!)
779 private fun populateDomainsListView(highlightedDomainDatabaseId: Int, domainsListViewPosition: Int) {
780 // Get a cursor with the current contents of the domains database.
781 val domainsCursor = domainsDatabaseHelper.domainNameCursorOrderedByDomain
783 // Setup the domains cursor adapter.
784 val domainsCursorAdapter: CursorAdapter = object : CursorAdapter(applicationContext, domainsCursor, false) {
785 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
786 // Inflate the individual item layout.
787 return layoutInflater.inflate(R.layout.domain_name_linearlayout, parent, false)
790 override fun bindView(view: View, context: Context, cursor: Cursor) {
791 // Get a handle for the domain name text view.
792 val domainNameTextView = view.findViewById<TextView>(R.id.domain_name_textview)
794 // Get the domain name string.
795 val domainNameString = cursor.getString(cursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME))
797 // Set the domain name.
798 domainNameTextView.text = domainNameString
802 // get a handle for the current domains listview.
803 domainsListView = findViewById(R.id.domains_listview)
805 // Update the list view.
806 domainsListView!!.adapter = domainsCursorAdapter
808 // Restore the scroll position.
809 domainsListView!!.setSelection(domainsListViewPosition)
811 // Display the domain settings in the second pane if operating in two pane mode and the database contains at least one domain.
812 if (twoPanedMode && domainsCursor.count > 0) { // Two-paned mode is enabled and there is at least one domain.
813 // Initialize the highlighted domain position tracker.
814 var highlightedDomainPosition = 0
816 // Get the cursor position for the highlighted domain.
817 for (i in 0 until domainsCursor.count) {
818 // Move to position `i` in the cursor.
819 domainsCursor.moveToPosition(i)
821 // Get the database ID for this position.
822 val currentDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID))
824 // Set the highlighted domain position if the database ID for this matches the highlighted domain database ID.
825 if (highlightedDomainDatabaseId == currentDatabaseId)
826 highlightedDomainPosition = i
829 // Select the highlighted domain.
830 domainsListView!!.setItemChecked(highlightedDomainPosition, true)
832 // Move to the highlighted domain.
833 domainsCursor.moveToPosition(highlightedDomainPosition)
835 // Get the database ID for the highlighted domain.
836 currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID))
838 // Create an arguments bundle.
839 val argumentsBundle = Bundle()
841 // Store the domain settings in the arguments bundle.
842 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId)
843 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY)
845 // Instantiate a new domain settings fragment.
846 val domainSettingsFragment = DomainSettingsFragment()
848 // Add the arguments bundle to the domain settings fragment.
849 domainSettingsFragment.arguments = argumentsBundle
851 // Display the domain settings fragment.
852 supportFragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit()
854 // Enable the delete options menu items.
855 deleteMenuItem.isEnabled = true
856 } else if (twoPanedMode) { // Two-paned mode is enabled but there are no domains.
857 // Disable the delete menu item.
858 deleteMenuItem.isEnabled = false
862 override fun dismissSnackbar() {
863 // Dismiss the undo delete snackbar if it is shown.
864 if (undoDeleteSnackbar != null && undoDeleteSnackbar!!.isShown) {
865 // Dismiss the snackbar.
866 undoDeleteSnackbar!!.dismiss()
870 public override fun onDestroy() {
871 // Close the domains database helper.
872 domainsDatabaseHelper.close()
874 // Run the default commands.