From cd609c9888924549c03cd6509ed889cdb052d5bb Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Thu, 20 May 2021 12:51:00 -0700 Subject: [PATCH] Add the option to move the app bar to the bottom of the screen. https://redmine.stoutner.com/issues/143 --- .gitignore | 4 +- .idea/assetWizardSettings.xml | 5 +- .../free/res/layout/adview_bottom_appbar.xml | 30 +++ .../{adview.xml => adview_top_appbar.xml} | 0 app/src/main/assets/de/about_licenses.html | 1 + app/src/main/assets/en/about_licenses.html | 1 + app/src/main/assets/es/about_licenses.html | 1 + app/src/main/assets/fr/about_licenses.html | 1 + .../main/assets/fr/guide_tracking_ids.html | 5 +- app/src/main/assets/it/about_licenses.html | 1 + .../main/assets/pt-rBR/about_licenses.html | 1 + app/src/main/assets/ru/about_licenses.html | 1 + .../assets/shared_images/call_to_action.svg | 32 +++ app/src/main/assets/tr/about_licenses.html | 1 + .../activities/AboutActivity.kt | 9 +- .../activities/BookmarksActivity.java | 28 +- .../BookmarksDatabaseViewActivity.java | 19 +- .../activities/DomainsActivity.java | 13 +- .../activities/GuideActivity.java | 17 +- .../activities/ImportExportActivity.java | 15 +- .../activities/LogcatActivity.java | 9 +- .../activities/MainWebViewActivity.java | 222 +++++++++------- .../activities/RequestsActivity.java | 11 +- .../activities/ViewSourceActivity.kt | 9 +- .../dialogs/ViewSslCertificateDialog.kt | 1 + .../fragments/SettingsFragment.java | 89 +++++-- .../helpers/ImportExportDatabaseHelper.java | 27 +- .../res/drawable/app_bar_disabled_day.xml | 8 +- .../res/drawable/app_bar_disabled_night.xml | 8 +- .../main/res/drawable/app_bar_enabled_day.xml | 2 +- .../res/drawable/app_bar_enabled_night.xml | 2 +- .../drawable/bottom_app_bar_disabled_day.xml | 13 + .../bottom_app_bar_disabled_night.xml | 13 + .../drawable/bottom_app_bar_enabled_day.xml | 13 + .../drawable/bottom_app_bar_enabled_night.xml | 13 + .../bookmarks_drawer_bottom_appbar.xml | 107 ++++++++ ...er.xml => bookmarks_drawer_top_appbar.xml} | 0 .../res/layout-w900dp/domains_fragments.xml | 7 +- .../about_coordinatorlayout_bottom_appbar.xml | 63 +++++ ...=> about_coordinatorlayout_top_appbar.xml} | 0 .../{adview.xml => adview_bottom_appbar.xml} | 0 app/src/main/res/layout/adview_top_appbar.xml | 27 ++ ...kmarks_coordinatorlayout_bottom_appbar.xml | 82 ++++++ ...ookmarks_coordinatorlayout_top_appbar.xml} | 8 +- ...seview_coordinatorlayout_bottom_appbar.xml | 58 ++++ ...baseview_coordinatorlayout_top_appbar.xml} | 0 .../layout/bookmarks_drawer_bottom_appbar.xml | 107 ++++++++ ...er.xml => bookmarks_drawer_top_appbar.xml} | 0 ...omains_coordinatorlayout_bottom_appbar.xml | 62 +++++ ... domains_coordinatorlayout_top_appbar.xml} | 3 +- app/src/main/res/layout/domains_fragments.xml | 7 +- .../guide_coordinatorlayout_bottom_appbar.xml | 63 +++++ ...=> guide_coordinatorlayout_top_appbar.xml} | 14 +- ...export_coordinatorlayout_bottom_appbar.xml | 242 +++++++++++++++++ ...t_export_coordinatorlayout_top_appbar.xml} | 0 ...logcat_coordinatorlayout_bottom_appbar.xml | 69 +++++ ...> logcat_coordinatorlayout_top_appbar.xml} | 6 +- .../layout/main_framelayout_bottom_appbar.xml | 249 ++++++++++++++++++ ...ut.xml => main_framelayout_top_appbar.xml} | 12 +- ...quests_coordinatorlayout_bottom_appbar.xml | 56 ++++ ...requests_coordinatorlayout_top_appbar.xml} | 4 +- ...source_coordinatorlayout_bottom_appbar.xml | 162 ++++++++++++ ...w_source_coordinatorlayout_top_appbar.xml} | 0 app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values-night-v23/styles.xml | 1 - app/src/main/res/values-night-v27/styles.xml | 1 - app/src/main/res/values-night/styles.xml | 3 - app/src/main/res/values-pt-rBR/strings.xml | 2 + app/src/main/res/values-v23/styles.xml | 1 - app/src/main/res/values-v27/styles.xml | 1 - app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 3 - app/src/main/res/xml/preferences.xml | 6 + 73 files changed, 1855 insertions(+), 205 deletions(-) create mode 100644 app/src/free/res/layout/adview_bottom_appbar.xml rename app/src/free/res/layout/{adview.xml => adview_top_appbar.xml} (100%) create mode 100644 app/src/main/assets/shared_images/call_to_action.svg create mode 100644 app/src/main/res/drawable/bottom_app_bar_disabled_day.xml create mode 100644 app/src/main/res/drawable/bottom_app_bar_disabled_night.xml create mode 100644 app/src/main/res/drawable/bottom_app_bar_enabled_day.xml create mode 100644 app/src/main/res/drawable/bottom_app_bar_enabled_night.xml create mode 100644 app/src/main/res/layout-w900dp/bookmarks_drawer_bottom_appbar.xml rename app/src/main/res/layout-w900dp/{bookmarks_drawer.xml => bookmarks_drawer_top_appbar.xml} (100%) create mode 100644 app/src/main/res/layout/about_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{about_coordinatorlayout.xml => about_coordinatorlayout_top_appbar.xml} (100%) rename app/src/main/res/layout/{adview.xml => adview_bottom_appbar.xml} (100%) create mode 100644 app/src/main/res/layout/adview_top_appbar.xml create mode 100644 app/src/main/res/layout/bookmarks_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{bookmarks_coordinatorlayout.xml => bookmarks_coordinatorlayout_top_appbar.xml} (92%) create mode 100644 app/src/main/res/layout/bookmarks_databaseview_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{bookmarks_databaseview_coordinatorlayout.xml => bookmarks_databaseview_coordinatorlayout_top_appbar.xml} (100%) create mode 100644 app/src/main/res/layout/bookmarks_drawer_bottom_appbar.xml rename app/src/main/res/layout/{bookmarks_drawer.xml => bookmarks_drawer_top_appbar.xml} (100%) create mode 100644 app/src/main/res/layout/domains_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{domains_coordinatorlayout.xml => domains_coordinatorlayout_top_appbar.xml} (95%) create mode 100644 app/src/main/res/layout/guide_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{guide_coordinatorlayout.xml => guide_coordinatorlayout_top_appbar.xml} (94%) create mode 100644 app/src/main/res/layout/import_export_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{import_export_coordinatorlayout.xml => import_export_coordinatorlayout_top_appbar.xml} (100%) create mode 100644 app/src/main/res/layout/logcat_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{logcat_coordinatorlayout.xml => logcat_coordinatorlayout_top_appbar.xml} (90%) create mode 100644 app/src/main/res/layout/main_framelayout_bottom_appbar.xml rename app/src/main/res/layout/{main_framelayout.xml => main_framelayout_top_appbar.xml} (98%) create mode 100644 app/src/main/res/layout/requests_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{requests_coordinatorlayout.xml => requests_coordinatorlayout_top_appbar.xml} (94%) create mode 100644 app/src/main/res/layout/view_source_coordinatorlayout_bottom_appbar.xml rename app/src/main/res/layout/{view_source_coordinatorlayout.xml => view_source_coordinatorlayout_top_appbar.xml} (100%) diff --git a/.gitignore b/.gitignore index c8173b9e..ed7caad3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .gradle /local.properties -/.idea/caches -/.idea/workspace.xml -/.idea/libraries +/.idea .DS_Store /build /captures diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index d11ce2c7..5f0c8ec2 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -68,7 +68,7 @@ @@ -78,8 +78,7 @@ diff --git a/app/src/free/res/layout/adview_bottom_appbar.xml b/app/src/free/res/layout/adview_bottom_appbar.xml new file mode 100644 index 00000000..bd037ad3 --- /dev/null +++ b/app/src/free/res/layout/adview_bottom_appbar.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/app/src/free/res/layout/adview.xml b/app/src/free/res/layout/adview_top_appbar.xml similarity index 100% rename from app/src/free/res/layout/adview.xml rename to app/src/free/res/layout/adview_top_appbar.xml diff --git a/app/src/main/assets/de/about_licenses.html b/app/src/main/assets/de/about_licenses.html index d8df339a..ae520ff1 100644 --- a/app/src/main/assets/de/about_licenses.html +++ b/app/src/main/assets/de/about_licenses.html @@ -108,6 +108,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/en/about_licenses.html b/app/src/main/assets/en/about_licenses.html index 4ee960e2..10ef8059 100644 --- a/app/src/main/assets/en/about_licenses.html +++ b/app/src/main/assets/en/about_licenses.html @@ -106,6 +106,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/es/about_licenses.html b/app/src/main/assets/es/about_licenses.html index d8159635..10421f67 100644 --- a/app/src/main/assets/es/about_licenses.html +++ b/app/src/main/assets/es/about_licenses.html @@ -111,6 +111,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/fr/about_licenses.html b/app/src/main/assets/fr/about_licenses.html index e8f1f866..23e3d7f9 100644 --- a/app/src/main/assets/fr/about_licenses.html +++ b/app/src/main/assets/fr/about_licenses.html @@ -113,6 +113,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/fr/guide_tracking_ids.html b/app/src/main/assets/fr/guide_tracking_ids.html index 0e091841..0833f9cf 100644 --- a/app/src/main/assets/fr/guide_tracking_ids.html +++ b/app/src/main/assets/fr/guide_tracking_ids.html @@ -34,9 +34,8 @@

Il y a quelques années, le W3C (Consortium World Wide Web) a créé un mécanisme permettant aux navigateurs d'informer les serveurs Web qu'ils ne voudraient pas être suivis. Ceci est réalisé en incluant un en-tête DNT (Ne pas suivre) avec les requêtes Web.

-

L'en-tête DNT ne fournit pas vraiment de confidentialité car la plupart des serveurs Web l'ignorent. - Par exemple, Yahoo, Google, Microsoft et Facebook ignorent tous au moins certains en-têtes DNT. - Beginning with version 3.8, Privacy Browser no longer has the option to send a DNT header.

+

L'en-tête DNT ne fournit pas vraiment de confidentialité car la plupart des serveurs Web l'ignorent. Par exemple, Yahoo, Google, Microsoft et Facebook ignorent tous au moins certains en-têtes DNT. + À partir de la version 3.8, Privacy Browser n'a plus l'option d'envoyer un en-tête DNT.

Modification d'URLs

diff --git a/app/src/main/assets/it/about_licenses.html b/app/src/main/assets/it/about_licenses.html index ea13c25e..2731abc7 100644 --- a/app/src/main/assets/it/about_licenses.html +++ b/app/src/main/assets/it/about_licenses.html @@ -112,6 +112,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/pt-rBR/about_licenses.html b/app/src/main/assets/pt-rBR/about_licenses.html index 4ee960e2..10ef8059 100644 --- a/app/src/main/assets/pt-rBR/about_licenses.html +++ b/app/src/main/assets/pt-rBR/about_licenses.html @@ -106,6 +106,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/ru/about_licenses.html b/app/src/main/assets/ru/about_licenses.html index 23fa3d5e..63c5b761 100644 --- a/app/src/main/assets/ru/about_licenses.html +++ b/app/src/main/assets/ru/about_licenses.html @@ -106,6 +106,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/assets/shared_images/call_to_action.svg b/app/src/main/assets/shared_images/call_to_action.svg new file mode 100644 index 00000000..feb17923 --- /dev/null +++ b/app/src/main/assets/shared_images/call_to_action.svg @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/tr/about_licenses.html b/app/src/main/assets/tr/about_licenses.html index 7406a093..b6adf418 100644 --- a/app/src/main/assets/tr/about_licenses.html +++ b/app/src/main/assets/tr/about_licenses.html @@ -107,6 +107,7 @@

arrow_forward.

bookmarks.

bug_report.

+

call_to_action.

camera_enhance.

chrome_reader_mode.

close.

diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.kt index 94023b37..bd5b3b68 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.kt @@ -60,8 +60,9 @@ class AboutActivity : AppCompatActivity(), SaveListener { // Get a handle for the shared preferences. val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) - // Get the screenshot preference. + // Get the preferences. val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false) + val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false) // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -81,7 +82,11 @@ class AboutActivity : AppCompatActivity(), SaveListener { val blocklistVersions = launchingIntent.getStringArrayExtra(BLOCKLIST_VERSIONS)!! // Set the content view. - setContentView(R.layout.about_coordinatorlayout) + if (bottomAppBar) { + setContentView(R.layout.about_coordinatorlayout_bottom_appbar) + } else { + setContentView(R.layout.about_coordinatorlayout_top_appbar) + } // Get handles for the views. val toolbar = findViewById(R.id.about_toolbar) diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.java index 2b46600e..81f175f8 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.java @@ -39,6 +39,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.view.WindowManager; import android.widget.AbsListView; import android.widget.CursorAdapter; @@ -123,8 +124,9 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + // Get the preferences. + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -160,11 +162,22 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Convert the favorite icon byte array to a bitmap. Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length); - // Set the content view. - setContentView(R.layout.bookmarks_coordinatorlayout); + // Set the content according to the app bar position. + if (bottomAppBar) { + // Set the content view. + setContentView(R.layout.bookmarks_coordinatorlayout_bottom_appbar); + } else { + // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set. + supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY); + + // Set the content view. + setContentView(R.layout.bookmarks_coordinatorlayout_top_appbar); + } - // The AndroidX toolbar must be used until the minimum API is >= 21. + // Get a handle for the toolbar. final Toolbar toolbar = findViewById(R.id.bookmarks_toolbar); + + // Set the support action bar. setSupportActionBar(toolbar); // Get handles for the views. @@ -695,6 +708,11 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma bookmarksListView.setItemChecked(i, true); } } else if (menuItemId == R.id.bookmarks_database_view) { + // Close the contextual action bar if it is displayed. This can happen if the bottom app bar is enabled. + if (contextualActionMode != null) { + contextualActionMode.finish(); + } + // Create an intent to launch the bookmarks database view activity. Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class); diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.java index 6a230c07..d084d5c6 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.java @@ -41,6 +41,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.view.WindowManager; import android.widget.AbsListView; import android.widget.AdapterView; @@ -97,8 +98,9 @@ public class BookmarksDatabaseViewActivity extends AppCompatActivity implements // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + // Get the preferences. + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -123,8 +125,17 @@ public class BookmarksDatabaseViewActivity extends AppCompatActivity implements // Convert the favorite icon byte array to a bitmap and store it in a class variable. Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length); - // Set the content view. - setContentView(R.layout.bookmarks_databaseview_coordinatorlayout); + // Set the view according to the theme. + if (bottomAppBar) { + // Set the content view. + setContentView(R.layout.bookmarks_databaseview_coordinatorlayout_bottom_appbar); + } else { + // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set. + supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY); + + // Set the content view. + setContentView(R.layout.bookmarks_databaseview_coordinatorlayout_top_appbar); + } // Get a handle for the toolbar. Toolbar toolbar = findViewById(R.id.bookmarks_databaseview_toolbar); 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 8d460b0c..188875b4 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java @@ -129,8 +129,9 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + // Get the preferences. + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -178,8 +179,12 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo sslEndDateLong = intent.getLongExtra("ssl_end_date", 0); currentIpAddresses = intent.getStringExtra("current_ip_addresses"); - // Set the content view. - setContentView(R.layout.domains_coordinatorlayout); + // Set the view. + if (bottomAppBar) { + setContentView(R.layout.domains_coordinatorlayout_bottom_appbar); + } else { + setContentView(R.layout.domains_coordinatorlayout_top_appbar); + } // Populate the class variables. coordinatorLayout = findViewById(R.id.domains_coordinatorlayout); diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/GuideActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/GuideActivity.java index 54c87a87..91ef0fd3 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/GuideActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/GuideActivity.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2020 Soren Stoutner . + * Copyright © 2016-2021 Soren Stoutner . * * This file is part of Privacy Browser . * @@ -40,8 +40,9 @@ public class GuideActivity extends AppCompatActivity { // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + // Get the preferences. + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -55,10 +56,16 @@ public class GuideActivity extends AppCompatActivity { super.onCreate(savedInstanceState); // Set the content view. - setContentView(R.layout.guide_coordinatorlayout); + if (bottomAppBar) { + setContentView(R.layout.guide_coordinatorlayout_bottom_appbar); + } else { + setContentView(R.layout.guide_coordinatorlayout_top_appbar); + } - // The AndroidX toolbar must be used until the minimum API is >= 21. + // Get a handle for the toolbar. Toolbar toolbar = findViewById(R.id.guide_toolbar); + + // Set the support action bar. setSupportActionBar(toolbar); // Get a handle for the action bar. diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java index b9acb8c5..e3324295 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java @@ -117,8 +117,9 @@ public class ImportExportActivity extends AppCompatActivity { // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + // Get the preferences. + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -132,10 +133,16 @@ public class ImportExportActivity extends AppCompatActivity { super.onCreate(savedInstanceState); // Set the content view. - setContentView(R.layout.import_export_coordinatorlayout); + if (bottomAppBar) { + setContentView(R.layout.import_export_coordinatorlayout_bottom_appbar); + } else { + setContentView(R.layout.import_export_coordinatorlayout_top_appbar); + } - // Set the support action bar. + // Get a handle for the toolbar. Toolbar toolbar = findViewById(R.id.import_export_toolbar); + + // Set the support action bar. setSupportActionBar(toolbar); // Get a handle for the action bar. 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 8e54bb43..78d203da 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java @@ -72,8 +72,9 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. + // Get the preferences. boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -87,7 +88,11 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save super.onCreate(savedInstanceState); // Set the content view. - setContentView(R.layout.logcat_coordinatorlayout); + if (bottomAppBar) { + setContentView(R.layout.logcat_coordinatorlayout_bottom_appbar); + } else { + setContentView(R.layout.logcat_coordinatorlayout_top_appbar); + } // Get handles for the views. Toolbar toolbar = findViewById(R.id.logcat_toolbar); 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 4861ae3a..d08f7c63 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -158,12 +158,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; + import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; + import java.text.NumberFormat; + import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -256,6 +260,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private boolean downloadWithExternalApp; private boolean hideAppBar; private boolean scrollAppBar; + private boolean bottomAppBar; private boolean loadingNewIntent; private boolean reapplyDomainSettingsOnRestart; private boolean reapplyAppSettingsOnRestart; @@ -296,6 +301,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private boolean sanitizeFacebookClickIds; private boolean sanitizeTwitterAmpRedirects; + // Define the class variables. + private long lastScrollUpdate = 0; + // Declare the class views. private FrameLayout rootFrameLayout; private DrawerLayout drawerLayout; @@ -385,9 +393,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. + // Get the preferences. String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value)); boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Get the theme entry values string array. String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values); @@ -423,7 +432,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook setTheme(R.style.PrivacyBrowser); // Set the content view. - setContentView(R.layout.main_framelayout); + if (bottomAppBar) { + setContentView(R.layout.main_framelayout_bottom_appbar); + } else { + setContentView(R.layout.main_framelayout_top_appbar); + } // Get handles for the views. rootFrameLayout = findViewById(R.id.root_framelayout); @@ -3395,7 +3408,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook }); // Implement swipe to refresh. - swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); + swipeRefreshLayout.setOnRefreshListener(() -> { + // Check the visibility of the bottom app bar. Sometimes it is hidden if the WebView is the same size as the visible screen. + if (bottomAppBar && scrollAppBar && (appBarLayout.getVisibility() == View.GONE)) { // The bottom app bar is currently hidden. + // Show the app bar. + appBarLayout.setVisibility(View.VISIBLE); + + // Disable the refreshing animation. + swipeRefreshLayout.setRefreshing(false); + } else { // A bottom app bar is not currently hidden. + // Reload the website. + currentWebView.reload(); + } + }); // Store the default progress view offsets for use later in `initializeWebView()`. defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); @@ -3587,51 +3612,48 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Apply the proxy. applyProxy(false); - // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); - AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); - AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); - - // Add the scrolling behavior to the layout parameters. - if (scrollAppBar) { - // Enable scrolling of the app bar. - swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); - toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - } else { - // Disable scrolling of the app bar. - swipeRefreshLayoutParams.setBehavior(null); - toolbarLayoutParams.setScrollFlags(0); - findOnPageLayoutParams.setScrollFlags(0); - tabsLayoutParams.setScrollFlags(0); - - // Expand the app bar if it is currently collapsed. - appBarLayout.setExpanded(true); - } - - // Apply the modified layout parameters. - swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams); - toolbar.setLayoutParams(toolbarLayoutParams); - findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams); - tabsLinearLayout.setLayoutParams(tabsLayoutParams); + // Adjust the layout and scrolling parameters if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); + AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); + AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); + + // Add the scrolling behavior to the layout parameters. + if (scrollAppBar) { + // Enable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + } else { + // Disable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(null); + toolbarLayoutParams.setScrollFlags(0); + findOnPageLayoutParams.setScrollFlags(0); + tabsLayoutParams.setScrollFlags(0); + + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } - // Set the app bar scrolling for each WebView. - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Set the app bar scrolling for each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Only modify the WebViews if they exist. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Only modify the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Set the app bar scrolling. - nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + // Set the app bar scrolling. + nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + } } } @@ -5238,19 +5260,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Hide the action bar. actionBar.hide(); - // Check to see if the app bar is normally scrolled. - if (scrollAppBar) { // The app bar is scrolled when it is displayed. - // Get the swipe refresh layout parameters. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - - // Remove the off-screen scrolling layout. - swipeRefreshLayoutParams.setBehavior(null); - } else { // The app bar is not scrolled when it is displayed. - // Remove the padding from the top of the swipe refresh layout. - swipeRefreshLayout.setPadding(0, 0, 0, 0); - - // The swipe refresh circle must be moved above the now removed status bar location. - swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset); + // Set layout and scrolling parameters if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Check to see if the app bar is normally scrolled. + if (scrollAppBar) { // The app bar is scrolled when it is displayed. + // Get the swipe refresh layout parameters. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + + // Remove the off-screen scrolling layout. + swipeRefreshLayoutParams.setBehavior(null); + } else { // The app bar is not scrolled when it is displayed. + // Remove the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + + // The swipe refresh circle must be moved above the now removed status bar location. + swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset); + } } } @@ -5280,19 +5305,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Show the action bar. actionBar.show(); - // Check to see if the app bar is normally scrolled. - if (scrollAppBar) { // The app bar is scrolled when it is displayed. - // Get the swipe refresh layout parameters. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - - // Add the off-screen scrolling layout. - swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); - } else { // The app bar is not scrolled when it is displayed. - // The swipe refresh layout must be manually moved below the app bar layout. - swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + // Set layout and scrolling parameters if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Check to see if the app bar is normally scrolled. + if (scrollAppBar) { // The app bar is scrolled when it is displayed. + // Get the swipe refresh layout parameters. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + + // Add the off-screen scrolling layout. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + } else { // The app bar is not scrolled when it is displayed. + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } } } @@ -5396,7 +5424,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. Also reinforce full screen browsing mode. // On API < 23, `getViewTreeObserver().addOnScrollChangedListener()` must be used, but it is a little bit buggy and appears to get garbage collected from time to time. if (Build.VERSION.SDK_INT >= 23) { - nestedScrollWebView.setOnScrollChangeListener((view, i, i1, i2, i3) -> { + nestedScrollWebView.setOnScrollChangeListener((view, scrollX, scrollY, oldScrollX, oldScrollY) -> { + // Set the swipe to refresh status. if (nestedScrollWebView.getSwipeToRefresh()) { // Only enable swipe to refresh if the WebView is scrolled to the top. swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0); @@ -5405,6 +5434,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook swipeRefreshLayout.setEnabled(false); } + // Set the visibility of the bottom app bar. + if (bottomAppBar && scrollAppBar && (Calendar.getInstance().getTimeInMillis() - lastScrollUpdate > 100)) { + if (scrollY - oldScrollY > 25) { // The WebView was scrolled down. + appBarLayout.setVisibility(View.GONE); + } else if (scrollY - oldScrollY < -25) { // The WebView was scrolled up. + appBarLayout.setVisibility(View.VISIBLE); + } + + // Update the last scroll update variable. This prevents the app bar from flashing on and off at the bottom of the screen. + lastScrollUpdate = Calendar.getInstance().getTimeInMillis(); + } + // Reinforce the system UI visibility flags if in full screen browsing mode. // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard. if (inFullScreenBrowsingMode) { @@ -5428,7 +5469,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook swipeRefreshLayout.setEnabled(false); } - // Reinforce the system UI visibility flags if in full screen browsing mode. // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard. if (inFullScreenBrowsingMode) { @@ -6060,25 +6100,25 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - // Get the preferences. - boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); - - // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. This can't be done in `appAppSettings()` because the app bar is not yet populated there. - if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { - // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior. - swipeRefreshLayout.setPadding(0, 0, 0, 0); - - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset); - } else { - // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there. - appBarHeight = appBarLayout.getHeight(); - - // The swipe refresh layout must be manually moved below the app bar layout. - swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + // Set the padding and layout settings if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. This can't be done in `appAppSettings()` because the app bar is not yet populated there. + if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { + // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset); + } else { + // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there. + appBarHeight = appBarLayout.getHeight(); + + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } } // Reset the list of resource requests. diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/RequestsActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/RequestsActivity.java index dcb9d66c..bcbb9c1a 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/RequestsActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/RequestsActivity.java @@ -64,8 +64,9 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - // Get the screenshot preference. - boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + // Get the preferences. + boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -85,7 +86,11 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi boolean blockAllThirdPartyRequests = intent.getBooleanExtra("block_all_third_party_requests", false); // Set the content view. - setContentView(R.layout.requests_coordinatorlayout); + if (bottomAppBar) { + setContentView(R.layout.requests_coordinatorlayout_bottom_appbar); + } else { + setContentView(R.layout.requests_coordinatorlayout_top_appbar); + } // Use the AndroidX toolbar until the minimum API is >= 21. Toolbar toolbar = findViewById(R.id.requests_toolbar); diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt index eb8ba87c..b2d50e7c 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt @@ -82,8 +82,9 @@ class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener { // Get a handle for the shared preferences. val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) - // Get the screenshot preference. + // Get the preferences. val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false) + val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false) // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -104,7 +105,11 @@ class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener { val userAgent = intent.getStringExtra(USER_AGENT)!! // Set the content view. - setContentView(R.layout.view_source_coordinatorlayout) + if (bottomAppBar) { + setContentView(R.layout.view_source_coordinatorlayout_bottom_appbar) + } else { + setContentView(R.layout.view_source_coordinatorlayout_top_appbar) + } // Get a handle for the toolbar. val toolbar = findViewById(R.id.view_source_toolbar) diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt index 3c971dc6..b5b0dd27 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt @@ -41,6 +41,7 @@ import androidx.preference.PreferenceManager import com.stoutner.privacybrowser.R import com.stoutner.privacybrowser.activities.MainWebViewActivity import com.stoutner.privacybrowser.views.NestedScrollWebView + import java.io.ByteArrayOutputStream import java.text.DateFormat diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java b/app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java index c6a1493e..44617594 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java @@ -94,6 +94,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { private Preference swipeToRefreshPreference; private Preference downloadWithExternalAppPreference; private Preference scrollAppBarPreference; + private Preference bottomAppBarPreference; private Preference displayAdditionalAppBarIconsPreference; private Preference appThemePreference; private Preference webViewThemePreference; @@ -157,6 +158,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { swipeToRefreshPreference = findPreference("swipe_to_refresh"); downloadWithExternalAppPreference = findPreference(getString(R.string.download_with_external_app_key)); scrollAppBarPreference = findPreference("scroll_app_bar"); + bottomAppBarPreference = findPreference(getString(R.string.bottom_app_bar_key)); displayAdditionalAppBarIconsPreference = findPreference(getString(R.string.display_additional_app_bar_icons_key)); appThemePreference = findPreference("app_theme"); webViewThemePreference = findPreference("webview_theme"); @@ -200,6 +202,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { assert swipeToRefreshPreference != null; assert downloadWithExternalAppPreference != null; assert scrollAppBarPreference != null; + assert bottomAppBarPreference != null; assert displayAdditionalAppBarIconsPreference != null; assert appThemePreference != null; assert webViewThemePreference != null; @@ -888,6 +891,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { } } + // Set the bottom app bar preference icon. + if (sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)) { + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_enabled_day); + } else { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_enabled_night); + } + } else { + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_disabled_day); + } else { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_disabled_night); + } + } + // Set the display additional app bar icons preference icon. if (sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false)) { if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { @@ -1174,29 +1192,8 @@ public class SettingsFragment extends PreferenceFragmentCompat { } } - // Create an intent to restart Privacy Browser. - Intent allowScreenshotsRestartIntent = requireActivity().getParentActivityIntent(); - - // Assert that the intent is not null to remove the lint error below. - assert allowScreenshotsRestartIntent != null; - - // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack. It requires `Intent.FLAG_ACTIVITY_NEW_TASK`. - allowScreenshotsRestartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - - // Create a handler to restart the activity. - Handler allowScreenshotsRestartHandler = new Handler(Looper.getMainLooper()); - - // Create a runnable to restart the activity. - Runnable allowScreenshotsRestartRunnable = () -> { - // Restart the activity. - startActivity(allowScreenshotsRestartIntent); - - // Kill this instance of Privacy Browser. Otherwise, the app exhibits sporadic behavior after the restart. - System.exit(0); - }; - - // Restart the activity after 150 milliseconds, so that the app has enough time to save the change to the preference. - allowScreenshotsRestartHandler.postDelayed(allowScreenshotsRestartRunnable, 150); + // Restart Privacy Browser. + restartPrivacyBrowser(); break; case "easylist": @@ -1793,6 +1790,26 @@ public class SettingsFragment extends PreferenceFragmentCompat { } break; + case "bottom_app_bar": + // Update the icon. + if (sharedPreferences.getBoolean(context.getString(R.string.bottom_app_bar_key), false)) { + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_enabled_day); + } else { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_enabled_night); + } + } else { + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_disabled_day); + } else { + bottomAppBarPreference.setIcon(R.drawable.bottom_app_bar_disabled_night); + } + } + + // Restart Privacy Browser. + restartPrivacyBrowser(); + break; + case "display_additional_app_bar_icons": // Update the icon. if (sharedPreferences.getBoolean(context.getString(R.string.display_additional_app_bar_icons_key), false)) { @@ -1932,4 +1949,30 @@ public class SettingsFragment extends PreferenceFragmentCompat { } }; } + + private void restartPrivacyBrowser() { + // Create an intent to restart Privacy Browser. + Intent restartIntent = requireActivity().getParentActivityIntent(); + + // Assert that the intent is not null to remove the lint error below. + assert restartIntent != null; + + // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack. It requires `Intent.FLAG_ACTIVITY_NEW_TASK`. + restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + // Create a handler to restart the activity. + Handler restartHandler = new Handler(Looper.getMainLooper()); + + // Create a runnable to restart the activity. + Runnable restartRunnable = () -> { + // Restart the activity. + startActivity(restartIntent); + + // Kill this instance of Privacy Browser. Otherwise, the app exhibits sporadic behavior after the restart. + System.exit(0); + }; + + // Restart the activity after 200 milliseconds, so that the app has enough time to save the change to the preference. + restartHandler.postDelayed(restartRunnable, 200); + } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java b/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java index 42cd1c7a..c8638e08 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java @@ -82,6 +82,7 @@ public class ImportExportDatabaseHelper { private static final String SWIPE_TO_REFRESH = "swipe_to_refresh"; private static final String DOWNLOAD_WITH_EXTERNAL_APP = "download_with_external_app"; private static final String SCROLL_APP_BAR = "scroll_app_bar"; + private static final String BOTTOM_APP_BAR = "bottom_app_bar"; private static final String DISPLAY_ADDITIONAL_APP_BAR_ICONS = "display_additional_app_bar_icons"; private static final String APP_THEME = "app_theme"; private static final String WEBVIEW_THEME = "webview_theme"; @@ -319,7 +320,7 @@ public class ImportExportDatabaseHelper { // Get the current clear logcat value. boolean clearLogcat = sharedPreferences.getBoolean(CLEAR_LOGCAT, true); - // Populate the preference table with the current clear logcat value. + // Populate the preferences table with the current clear logcat value. if (clearLogcat) { importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + CLEAR_LOGCAT + " = " + 1); } else { @@ -346,8 +347,27 @@ public class ImportExportDatabaseHelper { importDatabase.execSQL("UPDATE " + DomainsDatabaseHelper.DOMAINS_TABLE + " SET " + DomainsDatabaseHelper.COOKIES + " = enablefirstpartycookies"); importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + COOKIES + " = first_party_cookies"); - // Create the download with external app column. + // Create the new columns. importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + DOWNLOAD_WITH_EXTERNAL_APP + " BOOLEAN"); + importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + BOTTOM_APP_BAR + " BOOLEAN"); + + // Get the current values for the new columns. + boolean downloadWithExternalApp = sharedPreferences.getBoolean(DOWNLOAD_WITH_EXTERNAL_APP, false); + boolean bottomAppBar = sharedPreferences.getBoolean(BOTTOM_APP_BAR, false); + + // Populate the preferences table with the current download with external app value. + if (downloadWithExternalApp) { + importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + DOWNLOAD_WITH_EXTERNAL_APP + " = " + 1); + } else { + importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + DOWNLOAD_WITH_EXTERNAL_APP + " = " + 0); + } + + // Populate the preferences table with the current bottom app bar value. + if (bottomAppBar) { + importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + BOTTOM_APP_BAR + " = " + 1); + } else { + importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + BOTTOM_APP_BAR + " = " + 0); + } } } @@ -499,6 +519,7 @@ public class ImportExportDatabaseHelper { .putBoolean(SWIPE_TO_REFRESH, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(SWIPE_TO_REFRESH)) == 1) .putBoolean(DOWNLOAD_WITH_EXTERNAL_APP, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DOWNLOAD_WITH_EXTERNAL_APP)) == 1) .putBoolean(SCROLL_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(SCROLL_APP_BAR)) == 1) + .putBoolean(BOTTOM_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(BOTTOM_APP_BAR)) == 1) .putBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DISPLAY_ADDITIONAL_APP_BAR_ICONS)) == 1) .putString(APP_THEME, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(APP_THEME))) .putString(WEBVIEW_THEME, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(WEBVIEW_THEME))) @@ -667,6 +688,7 @@ public class ImportExportDatabaseHelper { SWIPE_TO_REFRESH + " BOOLEAN, " + DOWNLOAD_WITH_EXTERNAL_APP + " BOOLEAN, " + SCROLL_APP_BAR + " BOOLEAN, " + + BOTTOM_APP_BAR + " BOOLEAN, " + DISPLAY_ADDITIONAL_APP_BAR_ICONS + " BOOLEAN, " + APP_THEME + " TEXT, " + WEBVIEW_THEME + " TEXT, " + @@ -717,6 +739,7 @@ public class ImportExportDatabaseHelper { preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean(SWIPE_TO_REFRESH, true)); preferencesContentValues.put(DOWNLOAD_WITH_EXTERNAL_APP, sharedPreferences.getBoolean(DOWNLOAD_WITH_EXTERNAL_APP, false)); preferencesContentValues.put(SCROLL_APP_BAR, sharedPreferences.getBoolean(SCROLL_APP_BAR, true)); + preferencesContentValues.put(BOTTOM_APP_BAR, sharedPreferences.getBoolean(BOTTOM_APP_BAR, false)); preferencesContentValues.put(DISPLAY_ADDITIONAL_APP_BAR_ICONS, sharedPreferences.getBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, false)); preferencesContentValues.put(APP_THEME, sharedPreferences.getString(APP_THEME, context.getString(R.string.app_theme_default_value))); preferencesContentValues.put(WEBVIEW_THEME, sharedPreferences.getString(WEBVIEW_THEME, context.getString(R.string.webview_theme_default_value))); diff --git a/app/src/main/res/drawable/app_bar_disabled_day.xml b/app/src/main/res/drawable/app_bar_disabled_day.xml index 3de4608f..52c9ec06 100644 --- a/app/src/main/res/drawable/app_bar_disabled_day.xml +++ b/app/src/main/res/drawable/app_bar_disabled_day.xml @@ -1,13 +1,13 @@ + android:width="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0" > - + \ No newline at end of file diff --git a/app/src/main/res/drawable/app_bar_disabled_night.xml b/app/src/main/res/drawable/app_bar_disabled_night.xml index cfd29e1d..6feeaf83 100644 --- a/app/src/main/res/drawable/app_bar_disabled_night.xml +++ b/app/src/main/res/drawable/app_bar_disabled_night.xml @@ -1,13 +1,13 @@ + android:width="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0" > - + \ No newline at end of file diff --git a/app/src/main/res/drawable/app_bar_enabled_day.xml b/app/src/main/res/drawable/app_bar_enabled_day.xml index ae11037d..8bdfb69d 100644 --- a/app/src/main/res/drawable/app_bar_enabled_day.xml +++ b/app/src/main/res/drawable/app_bar_enabled_day.xml @@ -10,4 +10,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/app_bar_enabled_night.xml b/app/src/main/res/drawable/app_bar_enabled_night.xml index 5eea93c1..0f60fbad 100644 --- a/app/src/main/res/drawable/app_bar_enabled_night.xml +++ b/app/src/main/res/drawable/app_bar_enabled_night.xml @@ -10,4 +10,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_app_bar_disabled_day.xml b/app/src/main/res/drawable/bottom_app_bar_disabled_day.xml new file mode 100644 index 00000000..735a435e --- /dev/null +++ b/app/src/main/res/drawable/bottom_app_bar_disabled_day.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_app_bar_disabled_night.xml b/app/src/main/res/drawable/bottom_app_bar_disabled_night.xml new file mode 100644 index 00000000..0a891e62 --- /dev/null +++ b/app/src/main/res/drawable/bottom_app_bar_disabled_night.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_app_bar_enabled_day.xml b/app/src/main/res/drawable/bottom_app_bar_enabled_day.xml new file mode 100644 index 00000000..fbe36d36 --- /dev/null +++ b/app/src/main/res/drawable/bottom_app_bar_enabled_day.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_app_bar_enabled_night.xml b/app/src/main/res/drawable/bottom_app_bar_enabled_night.xml new file mode 100644 index 00000000..50344376 --- /dev/null +++ b/app/src/main/res/drawable/bottom_app_bar_enabled_night.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w900dp/bookmarks_drawer_bottom_appbar.xml b/app/src/main/res/layout-w900dp/bookmarks_drawer_bottom_appbar.xml new file mode 100644 index 00000000..89e51677 --- /dev/null +++ b/app/src/main/res/layout-w900dp/bookmarks_drawer_bottom_appbar.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w900dp/bookmarks_drawer.xml b/app/src/main/res/layout-w900dp/bookmarks_drawer_top_appbar.xml similarity index 100% rename from app/src/main/res/layout-w900dp/bookmarks_drawer.xml rename to app/src/main/res/layout-w900dp/bookmarks_drawer_top_appbar.xml diff --git a/app/src/main/res/layout-w900dp/domains_fragments.xml b/app/src/main/res/layout-w900dp/domains_fragments.xml index 15a55f43..37a0d5fd 100644 --- a/app/src/main/res/layout-w900dp/domains_fragments.xml +++ b/app/src/main/res/layout-w900dp/domains_fragments.xml @@ -1,7 +1,7 @@ - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/about_coordinatorlayout.xml b/app/src/main/res/layout/about_coordinatorlayout_top_appbar.xml similarity index 100% rename from app/src/main/res/layout/about_coordinatorlayout.xml rename to app/src/main/res/layout/about_coordinatorlayout_top_appbar.xml diff --git a/app/src/main/res/layout/adview.xml b/app/src/main/res/layout/adview_bottom_appbar.xml similarity index 100% rename from app/src/main/res/layout/adview.xml rename to app/src/main/res/layout/adview_bottom_appbar.xml diff --git a/app/src/main/res/layout/adview_top_appbar.xml b/app/src/main/res/layout/adview_top_appbar.xml new file mode 100644 index 00000000..9230b60c --- /dev/null +++ b/app/src/main/res/layout/adview_top_appbar.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bookmarks_coordinatorlayout_bottom_appbar.xml b/app/src/main/res/layout/bookmarks_coordinatorlayout_bottom_appbar.xml new file mode 100644 index 00000000..48633763 --- /dev/null +++ b/app/src/main/res/layout/bookmarks_coordinatorlayout_bottom_appbar.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bookmarks_coordinatorlayout.xml b/app/src/main/res/layout/bookmarks_coordinatorlayout_top_appbar.xml similarity index 92% rename from app/src/main/res/layout/bookmarks_coordinatorlayout.xml rename to app/src/main/res/layout/bookmarks_coordinatorlayout_top_appbar.xml index 555e1853..e5ac9712 100644 --- a/app/src/main/res/layout/bookmarks_coordinatorlayout.xml +++ b/app/src/main/res/layout/bookmarks_coordinatorlayout_top_appbar.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bookmarks_databaseview_coordinatorlayout.xml b/app/src/main/res/layout/bookmarks_databaseview_coordinatorlayout_top_appbar.xml similarity index 100% rename from app/src/main/res/layout/bookmarks_databaseview_coordinatorlayout.xml rename to app/src/main/res/layout/bookmarks_databaseview_coordinatorlayout_top_appbar.xml diff --git a/app/src/main/res/layout/bookmarks_drawer_bottom_appbar.xml b/app/src/main/res/layout/bookmarks_drawer_bottom_appbar.xml new file mode 100644 index 00000000..1d11be0a --- /dev/null +++ b/app/src/main/res/layout/bookmarks_drawer_bottom_appbar.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bookmarks_drawer.xml b/app/src/main/res/layout/bookmarks_drawer_top_appbar.xml similarity index 100% rename from app/src/main/res/layout/bookmarks_drawer.xml rename to app/src/main/res/layout/bookmarks_drawer_top_appbar.xml diff --git a/app/src/main/res/layout/domains_coordinatorlayout_bottom_appbar.xml b/app/src/main/res/layout/domains_coordinatorlayout_bottom_appbar.xml new file mode 100644 index 00000000..4af84907 --- /dev/null +++ b/app/src/main/res/layout/domains_coordinatorlayout_bottom_appbar.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/domains_coordinatorlayout.xml b/app/src/main/res/layout/domains_coordinatorlayout_top_appbar.xml similarity index 95% rename from app/src/main/res/layout/domains_coordinatorlayout.xml rename to app/src/main/res/layout/domains_coordinatorlayout_top_appbar.xml index 60d14cb8..6efd1d7b 100644 --- a/app/src/main/res/layout/domains_coordinatorlayout.xml +++ b/app/src/main/res/layout/domains_coordinatorlayout_top_appbar.xml @@ -54,5 +54,6 @@ android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@drawable/add" - android:tint="?attr/fabIconTintColor" /> + android:tint="?attr/fabIconTintColor" + android:contentDescription="@string/add_domain" /> \ No newline at end of file diff --git a/app/src/main/res/layout/domains_fragments.xml b/app/src/main/res/layout/domains_fragments.xml index b706f2eb..7271f48f 100644 --- a/app/src/main/res/layout/domains_fragments.xml +++ b/app/src/main/res/layout/domains_fragments.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/guide_coordinatorlayout.xml b/app/src/main/res/layout/guide_coordinatorlayout_top_appbar.xml similarity index 94% rename from app/src/main/res/layout/guide_coordinatorlayout.xml rename to app/src/main/res/layout/guide_coordinatorlayout_top_appbar.xml index d3ac643c..9724e44d 100644 --- a/app/src/main/res/layout/guide_coordinatorlayout.xml +++ b/app/src/main/res/layout/guide_coordinatorlayout_top_appbar.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +