Implement a night mode option. https://redmine.stoutner.com/issues/145.
authorSoren Stoutner <soren@stoutner.com>
Wed, 13 Sep 2017 22:09:05 +0000 (15:09 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 13 Sep 2017 22:09:05 +0000 (15:09 -0700)
32 files changed:
.idea/dictionaries/soren.xml
app/src/free/res/layout/main_webview.xml
app/src/main/assets/de/about_licenses.html
app/src/main/assets/en/about_licenses.html
app/src/main/assets/en/images/night_mode.png [new file with mode: 0644]
app/src/main/assets/es/about_licenses.html
app/src/main/assets/it/about_licenses.html
app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/fragments/DomainSettingsFragment.java
app/src/main/java/com/stoutner/privacybrowser/fragments/DomainsListFragment.java
app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java
app/src/main/java/com/stoutner/privacybrowser/helpers/DomainsDatabaseHelper.java
app/src/main/res/drawable/add_dark.xml
app/src/main/res/drawable/add_light.xml
app/src/main/res/drawable/night_mode_disabled_dark.xml [new file with mode: 0644]
app/src/main/res/drawable/night_mode_disabled_light.xml [new file with mode: 0644]
app/src/main/res/drawable/night_mode_enabled_dark.xml [new file with mode: 0644]
app/src/main/res/drawable/night_mode_enabled_light.xml [new file with mode: 0644]
app/src/main/res/drawable/privacy_mode.xml
app/src/main/res/drawable/refresh_disabled_dark.xml
app/src/main/res/drawable/refresh_disabled_light.xml
app/src/main/res/drawable/refresh_enabled_dark.xml
app/src/main/res/drawable/refresh_enabled_light.xml
app/src/main/res/layout/domain_settings_fragment.xml
app/src/main/res/layout/main_webview.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values/strings.xml
app/src/main/res/xml/preferences.xml
fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch - it.png [deleted file]
fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch.png [new file with mode: 0644]

index 25a0801..859d212 100644 (file)
@@ -72,6 +72,7 @@
       <w>mitm</w>
       <w>mozilla</w>
       <w>navigationview</w>
+      <w>nightmode</w>
       <w>nojs</w>
       <w>oname</w>
       <w>orbot</w>
       <w>subfolders</w>
       <w>tablayout</w>
       <w>techrepublic</w>
+      <w>textarea</w>
       <w>textview</w>
       <w>theverge</w>
       <w>torproject</w>
index c58d242..fb80533 100644 (file)
@@ -19,6 +19,7 @@
   along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>. -->
 
 <!-- `android:layout_weight="1"` sets the `RelativeLayout` to fill the rest of the screen because it is encapsulated in a `LinearLayout` with `android:orientation="vertical"`.-->
+<!-- `android:background=@color/gray_900` sets the background color that is displayed when a website is loading in night mode. -->
 <RelativeLayout
     android:id="@+id/main_webview_relativelayout"
     xmlns:android="http://schemas.android.com/apk/res/android"
@@ -27,8 +28,9 @@
     android:layout_width="match_parent"
     android:layout_height="0dp"
     android:layout_weight="1"
+    android:background="@color/gray_900"
     tools:context="com.stoutner.privacybrowser.activities.MainWebViewActivity"
-    tools:showIn="@layout/main_drawerlayout">
+    tools:showIn="@layout/main_drawerlayout" >
 
     <com.google.android.gms.ads.AdView
         android:id="@+id/adview"
         android:layout_centerHorizontal="true"
         android:layout_alignParentBottom="true"
         ads:adSize="SMART_BANNER"
-        ads:adUnitId="@string/ad_id">
+        ads:adUnitId="@string/ad_id" >
     </com.google.android.gms.ads.AdView>
 
     <android.support.v4.widget.SwipeRefreshLayout
         android:id="@+id/swipe_refreshlayout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_above="@id/adview">
+        android:layout_above="@id/adview" >
 
         <!-- Google does not currently want to support hiding the AppBar on scroll for a WebView child with the Support Toolbar.  https://code.google.com/p/android/issues/detail?id=200394 -->
         <WebView
index 23c09ef..9f480fe 100644 (file)
@@ -63,6 +63,9 @@
         <p><img class="left" src="../en/images/clear_and_exit.png">
             is derived from ic_exit_to_app, which is part of the <a href="https://material.io/icons/">Android Material icon set</a> and is released under the <a href ="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a>.
             The full text of the license is below. Modifications copyright © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. The resulting image is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3+ license</a>.</p>
+        <p><img class="left" src="../en/images/night_mode.png">
+            is derived from ic_compare, which is part of the <a href="https://material.io/icons/">Android Material icon set</a> and is released under the <a href ="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a>.
+            The full text of the license is below. Modifications copyright © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. The resulting image is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3+ license</a>.</p>
         <p><img class="left" src="../en/images/orbot.png"> orbot is a modified version of <a href="https://gitweb.torproject.org/orbot.git/tree/app/src/main/res/drawable-xxxhdpi/ic_stat_tor.png">the status icon from the Orbot project</a>, which is copyright
             2009-2010 Nathan Freitas, The Guardian Project. It is released under the <a href="https://gitweb.torproject.org/orbot.git/tree/LICENSE">3-clause BSD license</a>.
             The full text of the license is below. Modifications copyright © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. The resulting image is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3+ license</a>.</p>
index 295123d..54025ad 100644 (file)
@@ -58,6 +58,9 @@
         <p><img class="left" src="images/clear_and_exit.png">
             is derived from ic_exit_to_app, which is part of the <a href="https://material.io/icons/">Android Material icon set</a> and is released under the <a href ="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a>.
             The full text of the license is below. Modifications copyright © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. The resulting image is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3+ license</a>.</p>
+        <p><img class="left" src="images/night_mode.png">
+            is derived from ic_compare, which is part of the <a href="https://material.io/icons/">Android Material icon set</a> and is released under the <a href ="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a>.
+            The full text of the license is below. Modifications copyright © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. The resulting image is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3+ license</a>.</p>
         <p><img class="left" src="images/orbot.png"> orbot is a modified version of <a href="https://gitweb.torproject.org/orbot.git/tree/app/src/main/res/drawable-xxxhdpi/ic_stat_tor.png">the status icon from the Orbot project</a>, which is copyright
             2009-2010 Nathan Freitas, The Guardian Project. It is released under the <a href="https://gitweb.torproject.org/orbot.git/tree/LICENSE">3-clause BSD license</a>.
             The full text of the license is below. Modifications copyright © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. The resulting image is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GPLv3+ license</a>.</p>
diff --git a/app/src/main/assets/en/images/night_mode.png b/app/src/main/assets/en/images/night_mode.png
new file mode 100644 (file)
index 0000000..5e6cf45
Binary files /dev/null and b/app/src/main/assets/en/images/night_mode.png differ
index 633afd4..940d643 100644 (file)
             <a href="https://www.gnu.org/licenses/gpl-3.0.html">licencia GPLv3+</a>.</p>
         <p><img class="left" src="../en/images/clear_and_exit.png">
             deriva de ic_exit_to_app, que es parte del <a href="https://material.io/icons/">conjunto de iconos Android Material</a> y es liberado bajo la <a href ="https://www.apache.org/licenses/LICENSE-2.0">Licencia Apache 2.0</a>.
-            El texto completo de la licencia se encuentra debajo. Copyright de modificaciones © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>. La imagen resultante se libera bajo la
-            <a href="https://www.gnu.org/licenses/gpl-3.0.html">licencia GPLv3+</a>.</p>
+            El texto completo de la licencia se encuentra debajo. Copyright de modificaciones © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>.
+            La imagen resultante se libera bajo la <a href="https://www.gnu.org/licenses/gpl-3.0.html">licencia GPLv3+</a>.</p>
+        <p><img class="left" src="../en/images/night_mode.png">
+            deriva de ic_exit_to_app, que es parte del <a href="https://material.io/icons/">conjunto de iconos Android Material</a> y es liberado bajo la <a href ="https://www.apache.org/licenses/LICENSE-2.0">Licencia Apache 2.0</a>.
+            El texto completo de la licencia se encuentra debajo. Copyright de modificaciones © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>.
+            La imagen resultante se libera bajo la <a href="https://www.gnu.org/licenses/gpl-3.0.html">licencia GPLv3+</a>.</p>
         <p><img class="left" src="../en/images/orbot.png"> orbot es una versión modificada del <a href="https://gitweb.torproject.org/orbot.git/tree/app/src/main/res/drawable-xxxhdpi/ic_stat_tor.png">icono de estado del proyecto Orbot</a>,
             que tiene copyright 2009-2010 por Nathan Freitas, The Guardian Project. Es liberado bajo la <a href="https://gitweb.torproject.org/orbot.git/tree/LICENSE">licencia BSD modificada (de 3 cláusulas)</a>.
             El texto completo de la licencia se encuentra debajo. Copyright de modificaciones © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>.
index 7e38996..77986b3 100644 (file)
             è stata derivata da ic_exit_to_app, che fa parte dell'<a href="https://material.io/icons/">Android Material icon set</a> ed è stata rilasciata sotto <a href="https://www.apache.org/licenses/LICENSE-2.0">Licenza Apache 2.0</a>.
             Il testo completo della licenza è riportato di seguito. Copyright delle modifiche © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>.
             L'immagine risultante è rilasciata sotto <a href="https://www.gnu.org/licenses/gpl-3.0.html">Licenza GPLv3+</a>.</p>
+        <p><img class="left" src="../en/images/night_mode.png">
+            è stata derivata da ic_compare, che fa parte dell'<a href="https://material.io/icons/">Android Material icon set</a> ed è stata rilasciata sotto <a href="https://www.apache.org/licenses/LICENSE-2.0">Licenza Apache 2.0</a>.
+            Il testo completo della licenza è riportato di seguito. Copyright delle modifiche © 2017 <a href="mailto:soren@stoutner.com">Soren Stoutner</a>.
+            L'immagine risultante è rilasciata sotto <a href="https://www.gnu.org/licenses/gpl-3.0.html">Licenza GPLv3+</a>.</p>
         <p><img class="left" src="../en/images/orbot.png"> orbot è una versione modificata della <a href="https://gitweb.torproject.org/orbot.git/tree/app/src/main/res/drawable-xxxhdpi/ic_stat_tor.png">icona di stato del progetto Orbot</a>, il cui copyright
             è 2009-2010 Nathan Freitas, The Guardian Project. E' rilasciata sotto <a href="https://gitweb.torproject.org/orbot.git/tree/LICENSE">3-clause BSD license</a>. Il testo completo della Licenza è riportato di seguito.
             Copyright delle modifiche © 2017<a href="mailto:soren@stoutner.com">Soren Stoutner</a>. L'immagine risultante è rilasciata sotto <a href="https://www.gnu.org/licenses/gpl-3.0.html">Licenza GPLv3+</a>.</p>
index f71a138..de68623 100644 (file)
@@ -520,6 +520,7 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
         EditText customUserAgentEditText = (EditText) findViewById(R.id.domain_settings_custom_user_agent_edittext);
         Spinner fontSizeSpinner = (Spinner) findViewById(R.id.domain_settings_font_size_spinner);
         Spinner displayWebpageImagesSpinner = (Spinner) findViewById(R.id.domain_settings_display_webpage_images_spinner);
+        Spinner nightModeSpinner = (Spinner) findViewById(R.id.domain_settings_night_mode_spinner);
         Switch pinnedSslCertificateSwitch = (Switch) findViewById(R.id.domain_settings_pinned_ssl_certificate_switch);
         RadioButton savedSslCertificateRadioButton = (RadioButton) findViewById(R.id.saved_ssl_certificate_radiobutton);
         RadioButton currentWebsiteCertificateRadioButton = (RadioButton) findViewById(R.id.current_website_certificate_radiobutton);
@@ -534,6 +535,7 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
         int userAgentPositionInt = userAgentSpinner.getSelectedItemPosition();
         int fontSizePositionInt = fontSizeSpinner.getSelectedItemPosition();
         int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
+        int nightModeInt = nightModeSpinner.getSelectedItemPosition();
         boolean pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked();
 
         // Get the data for the `Spinners` from the entry values string arrays.
@@ -550,7 +552,7 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
         if (savedSslCertificateRadioButton.isChecked()) {  // The current certificate is being used.
             // Update the database except for the certificate.
             domainsDatabaseHelper.updateDomainExceptCertificate(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean,
-                    formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, pinnedSslCertificate);
+                    formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, nightModeInt, pinnedSslCertificate);
         } else if (currentWebsiteCertificateRadioButton.isChecked()) {  // The certificate is being updated with the current website certificate.
             // Get the current website SSL certificate.
             SslCertificate currentWebsiteSslCertificate = MainWebViewActivity.sslCertificate;
@@ -567,12 +569,12 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
 
             // Update the database.
             domainsDatabaseHelper.updateDomainWithCertificate(currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean, formDataEnabledBoolean,
-                    userAgentString, fontSizeInt, displayWebpageImagesInt, pinnedSslCertificate, issuedToCommonName, issuedToOrganization, issuedToOrganizationalUnit, issuedByCommonName, issuedByOrganization, issuedByOrganizationalUnit, startDateLong,
-                    endDateLong);
+                    userAgentString, fontSizeInt, displayWebpageImagesInt, nightModeInt, pinnedSslCertificate, issuedToCommonName, issuedToOrganization, issuedToOrganizationalUnit, issuedByCommonName, issuedByOrganization, issuedByOrganizationalUnit,
+                    startDateLong, endDateLong);
         } else {  // No certificate is selected.
             // Update the database, with PINNED_SSL_CERTIFICATE set to false.
             domainsDatabaseHelper.updateDomainExceptCertificate(currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean, formDataEnabledBoolean,
-                    userAgentString, fontSizeInt, displayWebpageImagesInt, false);
+                    userAgentString, fontSizeInt, displayWebpageImagesInt, nightModeInt, false);
         }
     }
 
index b13a785..02ac256 100644 (file)
@@ -41,6 +41,7 @@ import android.net.http.SslCertificate;
 import android.net.http.SslError;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Handler;
 import android.preference.PreferenceManager;
 import android.print.PrintDocumentAdapter;
 import android.print.PrintManager;
@@ -151,8 +152,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
     // `displayWebpageImagesBoolean` is public static so it can be accessed from `DomainSettingsFragment`.  It is also used in `applyAppSettings()` and `applyDomainSettings()`.
     public static boolean displayWebpageImagesBoolean;
 
-    // `reloadOnRestartBoolean` is public static so it can be accessed from `SettingsFragment`.  It is also used in `onRestart()`
-    public static boolean reloadOnRestartBoolean;
+    // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`.  It is also used in `onRestart()`
+    public static boolean reloadOnRestart;
+
+    // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment`.  It is also used in `onRestart()`.
+    public static boolean loadUrlOnRestart;
 
     // The pinned domain SSL Certificate variables are public static so they can be accessed from `PinnedSslCertificateMismatchDialog`.  They are also used in `onCreate()` and `applyDomainSettings()`.
     public static int domainSettingsDatabaseId;
@@ -203,22 +207,24 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
     private final Map<String, String> customHeaders = new HashMap<>();
 
-    // `javaScriptEnabled` is also used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `applyAppSettings()`.
-    // It is `Boolean` instead of `boolean` because `applyAppSettings()` needs to know if it is `null`.
-    private Boolean javaScriptEnabled;
+    // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`.
+    private boolean javaScriptEnabled;
 
-    // `firstPartyCookiesEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyAppSettings()`.
+    // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`.
     private boolean firstPartyCookiesEnabled;
 
-    // `thirdPartyCookiesEnabled` used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
+    // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
     private boolean thirdPartyCookiesEnabled;
 
-    // `domStorageEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
+    // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
     private boolean domStorageEnabled;
 
-    // `saveFormDataEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
+    // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
     private boolean saveFormDataEnabled;
 
+    // `nightMode` is used in `onCreate()` and  `applyDomainSettings()`.
+    private boolean nightMode;
+
     // `swipeToRefreshEnabled` is used in `onPrepareOptionsMenu()` and `applyAppSettings()`.
     private boolean swipeToRefreshEnabled;
 
@@ -749,6 +755,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
             // Update the URL in urlTextBox when the page starts to load.
             @Override
             public void onPageStarted(WebView view, String url, Bitmap favicon) {
+                // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
+                if (nightMode) {
+                    mainWebView.setVisibility(View.INVISIBLE);
+                }
+
                 // Check to see if we are waiting on Orbot.
                 if (!waitingForOrbot) {  // We are not waiting on Orbot, so we need to process the URL.
                     // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
@@ -770,7 +781,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
                 }
             }
 
-            // Update formattedUrlString and urlTextBox.  It is necessary to do this after the page finishes loading because the final URL can change during load.
+            // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
             @Override
             public void onPageFinished(WebView view, String url) {
                 // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes.
@@ -933,10 +944,36 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
             public void onProgressChanged(WebView view, int progress) {
                 progressBar.setProgress(progress);
                 if (progress < 100) {
+                    // Show the progress bar.
                     progressBar.setVisibility(View.VISIBLE);
                 } else {
+                    // Hide the progress bar.
                     progressBar.setVisibility(View.GONE);
 
+                    // Inject the night mode CSS if night mode is enabled.
+                    if (nightMode) {
+                        // `background-color: #212121` sets the background to be dark gray.  `color: #BDBDBD` sets the text color to be light gray.  `box-shadow: none` removes a lower underline on links used by WordPress.
+                        // `text-decoration: none` removes all text underlines.  `text-shadow: none` removes text shadows, which usually have a hard coded color.  `border: none` removes all borders, which can also be used to underline text.
+                        // `a {color: #1565C0}` sets links to be a dark blue.  `!important` takes precedent over any existing sub-settings.
+                        mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '" +
+                                "* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important; text-shadow: none !important; border: none !important}" +
+                                "a {color: #1565C0 !important;}" +
+                                "'; parent.appendChild(style)})()", null);
+                    }
+
+                    // Initialize a `Handler` to display `mainWebView`, which may have been hid by a night mode domain setting even if night mode is not currently enabled.
+                    Handler displayWebViewHandler = new Handler();
+
+                    // Setup a `Runnable` to display `mainWebView` after a delay to allow the CSS to be applied.
+                    Runnable displayWebViewRunnable = new Runnable() {
+                        public void run() {
+                            mainWebView.setVisibility(View.VISIBLE);
+                        }
+                    };
+
+                    // Use `displayWebViewHandler` to delay the displaying of `mainWebView` for 1000 milliseconds.
+                    displayWebViewHandler.postDelayed(displayWebViewRunnable, 1000);
+
                     //Stop the `SwipeToRefresh` indicator if it is running
                     swipeRefreshLayout.setRefreshing(false);
                 }
@@ -1079,6 +1116,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
         thirdPartyCookiesEnabled = false;
         domStorageEnabled = false;
         saveFormDataEnabled = false;
+        nightMode = false;
 
         // Initialize `webViewTitle`.
         webViewTitle = getString(R.string.no_title);
@@ -1139,12 +1177,21 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
         setDisplayWebpageImages();
 
         // Reload the webpage if displaying of images has been disabled in `SettingsFragment`.
-        if (reloadOnRestartBoolean) {
+        if (reloadOnRestart) {
             // Reload `mainWebView`.
             mainWebView.reload();
 
             // Reset `reloadOnRestartBoolean`.
-            reloadOnRestartBoolean = false;
+            reloadOnRestart = false;
+        }
+
+        // Load the URL on restart to apply changes to night mode.
+        if (loadUrlOnRestart) {
+            // Load the current `formattedUrlString`.
+            loadUrl(formattedUrlString);
+
+            // Reset `loadUrlOnRestart.
+            loadUrlOnRestart = false;
         }
     }
 
@@ -2619,10 +2666,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
             // Get a handle for the shared preference.  `this` references the current context.
             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
 
-            // Store the default font size and user agent information.
+            // Store the general preference information.
             String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100");
             String defaultUserAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0");
             String defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0");
+            nightMode = sharedPreferences.getBoolean("night_mode", false);
 
             if (domainSettingsApplied) {  // The url we are loading has custom domain settings.
                 // Get a cursor for the current host and move it to the first position.
@@ -2639,6 +2687,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
                 String userAgentString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
                 int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
                 displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
+                int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
                 pinnedDomainSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
                 pinnedDomainSslIssuedToCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
                 pinnedDomainSslIssuedToONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
@@ -2647,6 +2696,22 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
                 pinnedDomainSslIssuedByONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
                 pinnedDomainSslIssuedByUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
 
+                // Set `nightMode` according to `nightModeInt`.  If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
+                switch (nightModeInt) {
+                    case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
+                        nightMode = true;
+                        break;
+
+                    case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
+                        nightMode = false;
+                        break;
+                }
+
+                // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
+                if (nightMode) {
+                    javaScriptEnabled = true;
+                }
+
                 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
                     pinnedDomainSslStartDate = null;
@@ -2729,6 +2794,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation
                 domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false);
                 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false);
 
+                // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
+                if (nightMode) {
+                    javaScriptEnabled = true;
+                }
+
                 // Apply the default settings.
                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
index 3889ef7..31dd027 100644 (file)
@@ -87,15 +87,16 @@ public class DomainSettingsFragment extends Fragment {
         // Get a handle for the shared preference.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
 
-        // Store the default user agent string values.
+        // Store the default settings.
         final String defaultUserAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0");
         final String defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0");
         String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100");
-        boolean defaultDisplayWebpageImagesBoolean = sharedPreferences.getBoolean("display_website_images", true);
+        final boolean defaultDisplayWebpageImagesBoolean = sharedPreferences.getBoolean("display_website_images", true);
+        final boolean defaultNightModeBoolean = sharedPreferences.getBoolean("night_mode", false);
 
         // Get handles for the views in the fragment.
         final EditText domainNameEditText = (EditText) domainSettingsView.findViewById(R.id.domain_settings_name_edittext);
-        Switch javaScriptEnabledSwitch = (Switch) domainSettingsView.findViewById(R.id.domain_settings_javascript_switch);
+        final Switch javaScriptEnabledSwitch = (Switch) domainSettingsView.findViewById(R.id.domain_settings_javascript_switch);
         final ImageView javaScriptImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_javascript_imageview);
         Switch firstPartyCookiesEnabledSwitch = (Switch) domainSettingsView.findViewById(R.id.domain_settings_first_party_cookies_switch);
         final ImageView firstPartyCookiesImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_first_party_cookies_imageview);
@@ -106,14 +107,17 @@ public class DomainSettingsFragment extends Fragment {
         final ImageView domStorageImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_dom_storage_imageview);
         Switch formDataEnabledSwitch = (Switch) domainSettingsView.findViewById(R.id.domain_settings_form_data_switch);
         final ImageView formDataImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_form_data_imageview);
-        Spinner userAgentSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_user_agent_spinner);
+        final Spinner userAgentSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_user_agent_spinner);
         final TextView userAgentTextView = (TextView) domainSettingsView.findViewById(R.id.domain_settings_user_agent_textview);
         final EditText customUserAgentEditText = (EditText) domainSettingsView.findViewById(R.id.domain_settings_custom_user_agent_edittext);
-        Spinner fontSizeSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_font_size_spinner);
+        final Spinner fontSizeSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_font_size_spinner);
         final TextView fontSizeTextView = (TextView) domainSettingsView.findViewById(R.id.domain_settings_font_size_textview);
         final ImageView displayWebpageImagesImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_display_webpage_images_imageview);
-        Spinner displayWebpageImagesSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_display_webpage_images_spinner);
+        final Spinner displayWebpageImagesSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_display_webpage_images_spinner);
         final TextView displayImagesTextView = (TextView) domainSettingsView.findViewById(R.id.domain_settings_display_webpage_images_textview);
+        final ImageView nightModeImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_night_mode_imageview);
+        final Spinner nightModeSpinner = (Spinner) domainSettingsView.findViewById(R.id.domain_settings_night_mode_spinner);
+        final TextView nightModeTextView = (TextView) domainSettingsView.findViewById(R.id.domain_settings_night_mode_textview);
         final ImageView pinnedSslCertificateImageView = (ImageView) domainSettingsView.findViewById(R.id.domain_settings_pinned_ssl_certificate_imageview);
         Switch pinnedSslCertificateSwitch = (Switch) domainSettingsView.findViewById(R.id.domain_settings_pinned_ssl_certificate_switch);
         final LinearLayout savedSslCertificateLinearLayout = (LinearLayout) domainSettingsView.findViewById(R.id.saved_ssl_certificate_linearlayout);
@@ -157,14 +161,15 @@ public class DomainSettingsFragment extends Fragment {
 
         // Save the `Cursor` entries as variables.
         String domainNameString = domainCursor.getString(domainCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
-        int javaScriptEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT));
+        final int javaScriptEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT));
         int firstPartyCookiesEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES));
         int thirdPartyCookiesEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES));
-        int domStorageEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE));
+        final int domStorageEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE));
         int formDataEnabledInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA));
         final String currentUserAgentString = domainCursor.getString(domainCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
         int fontSizeInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
         int displayImagesInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
+        int nightModeInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
         int pinnedSslCertificateInt = domainCursor.getInt(domainCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE));
         final String savedSslCertificateIssuedToCNameString = domainCursor.getString(domainCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
         String savedSslCertificateIssuedToONameString = domainCursor.getString(domainCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
@@ -191,17 +196,20 @@ public class DomainSettingsFragment extends Fragment {
         final ArrayAdapter<CharSequence> userAgentEntryValuesArrayAdapter = ArrayAdapter.createFromResource(context, R.array.domain_settings_user_agent_entry_values, R.layout.spinner_item);
         ArrayAdapter<CharSequence> fontSizeArrayAdapter = ArrayAdapter.createFromResource(context, R.array.domain_settings_font_size_entries, R.layout.spinner_item);
         ArrayAdapter<CharSequence> fontSizeEntryValuesArrayAdapter = ArrayAdapter.createFromResource(context, R.array.domain_settings_font_size_entry_values, R.layout.spinner_item);
-        final ArrayAdapter<CharSequence> displayImagesArrayAdapter = ArrayAdapter.createFromResource(context, R.array.display_website_images_array, R.layout.spinner_item);
+        final ArrayAdapter<CharSequence> displayImagesArrayAdapter = ArrayAdapter.createFromResource(context, R.array.display_webpage_images_array, R.layout.spinner_item);
+        ArrayAdapter<CharSequence> nightModeArrayAdapter = ArrayAdapter.createFromResource(context, R.array.night_mode_array, R.layout.spinner_item);
 
         // Set the `DropDownViewResource` on the `Spinners`.
         userAgentArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
         fontSizeArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
         displayImagesArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
+        nightModeArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
 
         // Set the `ArrayAdapters` for the `Spinners`.
         userAgentSpinner.setAdapter(userAgentArrayAdapter);
         fontSizeSpinner.setAdapter(fontSizeArrayAdapter);
         displayWebpageImagesSpinner.setAdapter(displayImagesArrayAdapter);
+        nightModeSpinner.setAdapter(nightModeArrayAdapter);
 
         // Create a `SpannableStringBuilder` for each `TextView` that needs multiple colors of text.
         SpannableStringBuilder savedSslCertificateIssuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + savedSslCertificateIssuedToCNameString);
@@ -303,13 +311,28 @@ public class DomainSettingsFragment extends Fragment {
             }
         });
 
-        // Set the JavaScript status.
+        // Create a `boolean` to track if night mode is enabled.
+        boolean nightModeEnabled = (nightModeInt == DomainsDatabaseHelper.NIGHT_MODE_ENABLED) || ((nightModeInt == DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT) && defaultNightModeBoolean);
+
+        // Disable the JavaScript `Switch` if night mode is enabled.
+        if (nightModeEnabled) {
+            javaScriptEnabledSwitch.setEnabled(false);
+        } else {
+            javaScriptEnabledSwitch.setEnabled(true);
+        }
+
+        // Set the JavaScript icon.
+        if ((javaScriptEnabledInt == 1) || nightModeEnabled) {
+            javaScriptImageView.setImageDrawable(resources.getDrawable(R.drawable.javascript_enabled));
+        } else {
+            javaScriptImageView.setImageDrawable(resources.getDrawable(R.drawable.privacy_mode));
+        }
+
+        // Set the JavaScript `Switch` status.
         if (javaScriptEnabledInt == 1) {  // JavaScript is enabled.
             javaScriptEnabledSwitch.setChecked(true);
-            javaScriptImageView.setImageDrawable(resources.getDrawable(R.drawable.javascript_enabled));
         } else {  // JavaScript is disabled.
             javaScriptEnabledSwitch.setChecked(false);
-            javaScriptImageView.setImageDrawable(resources.getDrawable(R.drawable.privacy_mode));
         }
 
         // Set the first-party cookies status.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
@@ -369,7 +392,10 @@ public class DomainSettingsFragment extends Fragment {
         }
 
         // Only enable DOM storage if JavaScript is enabled.
-        if (javaScriptEnabledInt == 1) {  // JavaScript is enabled.
+        if ((javaScriptEnabledInt == 1) || nightModeEnabled) {  // JavaScript is enabled.
+            // Enable the DOM storage `Switch`.
+            domStorageEnabledSwitch.setEnabled(true);
+
             // Set the DOM storage status.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
             if (domStorageEnabledInt == 1) {  // Both JavaScript and DOM storage are enabled.
                 domStorageEnabledSwitch.setChecked(true);
@@ -386,6 +412,9 @@ public class DomainSettingsFragment extends Fragment {
                 }
             }
         } else {  // JavaScript is disabled.
+            // Disable the DOM storage `Switch`.
+            domStorageEnabledSwitch.setEnabled(false);
+
             // Set the checked status of DOM storage.
             if (domStorageEnabledInt == 1) {  // DOM storage is enabled but JavaScript is disabled.
                 domStorageEnabledSwitch.setChecked(true);
@@ -393,9 +422,6 @@ public class DomainSettingsFragment extends Fragment {
                 domStorageEnabledSwitch.setChecked(false);
             }
 
-            // Disable `domStorageEnabledSwitch`.
-            domStorageEnabledSwitch.setEnabled(false);
-
             // Set the icon according to the theme.
             if (MainWebViewActivity.darkTheme) {
                 domStorageImageView.setImageDrawable(resources.getDrawable(R.drawable.dom_storage_ghosted_dark));
@@ -482,6 +508,15 @@ public class DomainSettingsFragment extends Fragment {
             }
         }
 
+        // Open the user agent spinner when the `TextView` is clicked.
+        userAgentTextView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Open the user agent spinner.
+                userAgentSpinner.performClick();
+            }
+        });
+
         // Set the selected font size.
         int fontSizeArrayPosition = fontSizeEntryValuesArrayAdapter.getPosition(String.valueOf(fontSizeInt));
         fontSizeSpinner.setSelection(fontSizeArrayPosition);
@@ -497,27 +532,36 @@ public class DomainSettingsFragment extends Fragment {
             fontSizeTextView.setVisibility(View.GONE);
         }
 
-        // Set the selected display website images mode.
+        // Open the font size spinner when the `TextView` is clicked.
+        fontSizeTextView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Open the user agent spinner.
+                fontSizeSpinner.performClick();
+            }
+        });
+
+        // Display the website images mode in the spinner.
         displayWebpageImagesSpinner.setSelection(displayImagesInt);
 
         // Set the default display images text.
         if (defaultDisplayWebpageImagesBoolean) {
-            displayImagesTextView.setText(displayImagesArrayAdapter.getItem(1));
+            displayImagesTextView.setText(displayImagesArrayAdapter.getItem(DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED));
         } else {
-            displayImagesTextView.setText(displayImagesArrayAdapter.getItem(2));
+            displayImagesTextView.setText(displayImagesArrayAdapter.getItem(DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED));
         }
 
-        // Set the display website images icon and `TextView` settings.
+        // Set the display website images icon and `TextView` settings.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
         switch (displayImagesInt) {
             case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
-                if (MainWebViewActivity.displayWebpageImagesBoolean) {
+                if (defaultDisplayWebpageImagesBoolean) {  // Display webpage images enabled by default.
                     // Set the icon according to the theme.
                     if (MainWebViewActivity.darkTheme) {
                         displayWebpageImagesImageView.setImageDrawable(resources.getDrawable(R.drawable.images_enabled_dark));
                     } else {
                         displayWebpageImagesImageView.setImageDrawable(resources.getDrawable(R.drawable.images_enabled_light));
                     }
-                } else {
+                } else {  // Display webpage images disabled by default.
                     // Set the icon according to the theme.
                     if (MainWebViewActivity.darkTheme) {
                         displayWebpageImagesImageView.setImageDrawable(resources.getDrawable(R.drawable.images_disabled_dark));
@@ -554,9 +598,85 @@ public class DomainSettingsFragment extends Fragment {
                 displayImagesTextView.setVisibility(View.GONE);
                 break;
         }
+
+        // Open the display images spinner when the `TextView` is clicked.
+        displayImagesTextView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Open the user agent spinner.
+                displayWebpageImagesSpinner.performClick();
+            }
+        });
+
+        // Display the night mode in the spinner.
+        nightModeSpinner.setSelection(nightModeInt);
+
+        // Set the default night mode text.
+        if (defaultNightModeBoolean) {
+            nightModeTextView.setText(nightModeArrayAdapter.getItem(DomainsDatabaseHelper.NIGHT_MODE_ENABLED));
+        } else {
+            nightModeTextView.setText(nightModeArrayAdapter.getItem(DomainsDatabaseHelper.NIGHT_MODE_DISABLED));
+        }
+
+        // Set the night mode icon and `TextView` settings.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
+        switch (displayImagesInt) {
+            case DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT:
+                if (defaultNightModeBoolean) {  // Night mode enabled by default.
+                    // Set the icon according to the theme.
+                    if (MainWebViewActivity.darkTheme) {
+                        nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_dark));
+                    } else {
+                        nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_light));
+                    }
+                } else {  // Night mode disabled by default.
+                    // Set the icon according to the theme.
+                    if (MainWebViewActivity.darkTheme) {
+                        nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_dark));
+                    } else {
+                        nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_light));
+                    }
+                }
+
+                // Show `nightModeTextView`.
+                nightModeTextView.setVisibility(View.VISIBLE);
+                break;
+
+            case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
+                // Set the icon according to the theme.
+                if (MainWebViewActivity.darkTheme) {
+                    nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_dark));
+                } else {
+                    nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_light));
+                }
+
+                // Hide `nightModeTextView`.
+                nightModeTextView.setVisibility(View.GONE);
+                break;
+
+            case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
+                // Set the icon according to the theme.
+                if (MainWebViewActivity.darkTheme) {
+                    nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_dark));
+                } else {
+                    nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_light));
+                }
+
+                // Hide `nightModeTextView`.
+                nightModeTextView.setVisibility(View.GONE);
+                break;
+        }
+
+        // Open the night mode spinner when the `TextView` is clicked.
+        nightModeTextView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Open the user agent spinner.
+                nightModeSpinner.performClick();
+            }
+        });
         
         // Set the pinned SSL certificate icon.
-        if (pinnedSslCertificateInt == 1) {  // Pinned SSL certificate is enabled.
+        if (pinnedSslCertificateInt == 1) {  // Pinned SSL certificate is enabled.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
             // Check the switch.
             pinnedSslCertificateSwitch.setChecked(true);
 
@@ -962,7 +1082,7 @@ public class DomainSettingsFragment extends Fragment {
                 // Update the icon and the visibility of `displayImagesTextView`.
                 switch (position) {
                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
-                        if (MainWebViewActivity.displayWebpageImagesBoolean) {
+                        if (defaultDisplayWebpageImagesBoolean) {
                             // Set the icon according to the theme.
                             if (MainWebViewActivity.darkTheme) {
                                 displayWebpageImagesImageView.setImageDrawable(resources.getDrawable(R.drawable.images_enabled_dark));
@@ -1013,6 +1133,121 @@ public class DomainSettingsFragment extends Fragment {
                 // Do nothing.
             }
         });
+
+        // Set the `nightModeSpinner` `onItemSelectedListener()`.
+        nightModeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+                // Update the icon and the visibility of `nightModeTextView`.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
+                switch (position) {
+                    case DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT:
+                        if (defaultNightModeBoolean) {  // Night mode enabled by default.
+                            // Set the icon according to the theme.
+                            if (MainWebViewActivity.darkTheme) {
+                                nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_dark));
+                            } else {
+                                nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_light));
+                            }
+                        } else {  // Night mode disabled by default.
+                            // Set the icon according to the theme.
+                            if (MainWebViewActivity.darkTheme) {
+                                nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_dark));
+                            } else {
+                                nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_light));
+                            }
+                        }
+
+                        // Show `nightModeTextView`.
+                        nightModeTextView.setVisibility(View.VISIBLE);
+                        break;
+
+                    case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
+                        // Set the icon according to the theme.
+                        if (MainWebViewActivity.darkTheme) {
+                            nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_dark));
+                        } else {
+                            nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_enabled_light));
+                        }
+
+                        // Hide `nightModeTextView`.
+                        nightModeTextView.setVisibility(View.GONE);
+                        break;
+
+                    case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
+                        // Set the icon according to the theme.
+                        if (MainWebViewActivity.darkTheme) {
+                            nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_dark));
+                        } else {
+                            nightModeImageView.setImageDrawable(resources.getDrawable(R.drawable.night_mode_disabled_light));
+                        }
+
+                        // Hide `nightModeTextView`.
+                        nightModeTextView.setVisibility(View.GONE);
+                        break;
+                }
+
+                // Create a `boolean` to store the current night mode setting.
+                boolean currentNightModeEnabled = (position == DomainsDatabaseHelper.NIGHT_MODE_ENABLED) || ((position == DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT) && defaultNightModeBoolean);
+
+                // Disable the JavaScript `Switch` if night mode is enabled.
+                if (currentNightModeEnabled) {
+                    javaScriptEnabledSwitch.setEnabled(false);
+                } else {
+                    javaScriptEnabledSwitch.setEnabled(true);
+                }
+
+                // Update the JavaScript icon.
+                if ((javaScriptEnabledInt == 1) || currentNightModeEnabled) {
+                    javaScriptImageView.setImageDrawable(resources.getDrawable(R.drawable.javascript_enabled));
+                } else {
+                    javaScriptImageView.setImageDrawable(resources.getDrawable(R.drawable.privacy_mode));
+                }
+
+                // Update the DOM storage status.
+                if ((javaScriptEnabledInt == 1) || currentNightModeEnabled) {  // JavaScript is enabled.
+                    // Enable the DOM storage `Switch`.
+                    domStorageEnabledSwitch.setEnabled(true);
+
+                    // Set the DOM storage status.  Once minimum API >= 21 we can use a selector as the tint mode instead of specifying different icons.
+                    if (domStorageEnabledInt == 1) {  // Both JavaScript and DOM storage are enabled.
+                        domStorageEnabledSwitch.setChecked(true);
+                        domStorageImageView.setImageDrawable(resources.getDrawable(R.drawable.dom_storage_enabled));
+                    } else {  // JavaScript is enabled but DOM storage is disabled.
+                        // Set the DOM storage switch to off.
+                        domStorageEnabledSwitch.setChecked(false);
+
+                        // Set the icon according to the theme.
+                        if (MainWebViewActivity.darkTheme) {
+                            domStorageImageView.setImageDrawable(resources.getDrawable(R.drawable.dom_storage_disabled_dark));
+                        } else {
+                            domStorageImageView.setImageDrawable(resources.getDrawable(R.drawable.dom_storage_disabled_light));
+                        }
+                    }
+                } else {  // JavaScript is disabled.
+                    // Disable the DOM storage `Switch`.
+                    domStorageEnabledSwitch.setEnabled(false);
+
+                    // Set the checked status of DOM storage.
+                    if (domStorageEnabledInt == 1) {  // DOM storage is enabled but JavaScript is disabled.
+                        domStorageEnabledSwitch.setChecked(true);
+                    } else {  // Both JavaScript and DOM storage are disabled.
+                        domStorageEnabledSwitch.setChecked(false);
+                    }
+
+                    // Set the icon according to the theme.
+                    if (MainWebViewActivity.darkTheme) {
+                        domStorageImageView.setImageDrawable(resources.getDrawable(R.drawable.dom_storage_ghosted_dark));
+                    } else {
+                        domStorageImageView.setImageDrawable(resources.getDrawable(R.drawable.dom_storage_ghosted_light));
+                    }
+                }
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+                // Do nothing.
+            }
+        });
         
         // Set the `pinnedSSLCertificateSwitch` `onCheckedChangeListener()`.
         pinnedSslCertificateSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
index 34349d7..172a643 100644 (file)
@@ -80,6 +80,7 @@ public class DomainsListFragment extends Fragment {
                     EditText customUserAgentEditText = (EditText) domainSettingsFragmentView.findViewById(R.id.domain_settings_custom_user_agent_edittext);
                     Spinner fontSizeSpinner = (Spinner) domainSettingsFragmentView.findViewById(R.id.domain_settings_font_size_spinner);
                     Spinner displayWebpageImagesSpinner = (Spinner) domainSettingsFragmentView.findViewById(R.id.domain_settings_display_webpage_images_spinner);
+                    Spinner nightModeSpinner = (Spinner) domainSettingsFragmentView.findViewById(R.id.domain_settings_night_mode_spinner);
                     Switch pinnedSslCertificateSwitch = (Switch) domainSettingsFragmentView.findViewById(R.id.domain_settings_pinned_ssl_certificate_switch);
                     RadioButton savedSslCertificateRadioButton = (RadioButton) domainSettingsFragmentView.findViewById(R.id.saved_ssl_certificate_radiobutton);
                     RadioButton currentWebsiteCertificateRadioButton = (RadioButton) domainSettingsFragmentView.findViewById(R.id.current_website_certificate_radiobutton);
@@ -94,6 +95,7 @@ public class DomainsListFragment extends Fragment {
                     int userAgentPositionInt = userAgentSpinner.getSelectedItemPosition();
                     int fontSizePositionInt = fontSizeSpinner.getSelectedItemPosition();
                     int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
+                    int nightModeInt = nightModeSpinner.getSelectedItemPosition();
                     boolean pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked();
 
                     // Get the data for the `Spinners` from the entry values string arrays.
@@ -110,7 +112,7 @@ public class DomainsListFragment extends Fragment {
                     if (savedSslCertificateRadioButton.isChecked()) {  // The current certificate is being used.
                         // Update the database except for the certificate.
                         domainsDatabaseHelper.updateDomainExceptCertificate(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean,
-                                formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, pinnedSslCertificate);
+                                formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, nightModeInt, pinnedSslCertificate);
                     } else if (currentWebsiteCertificateRadioButton.isChecked()) {  // The certificate is being updated with the current website certificate.
                         // Get the current website SSL certificate.
                         SslCertificate currentWebsiteSslCertificate = MainWebViewActivity.sslCertificate;
@@ -127,12 +129,12 @@ public class DomainsListFragment extends Fragment {
 
                         // Update the database.
                         domainsDatabaseHelper.updateDomainWithCertificate(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean,
-                                formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, pinnedSslCertificate, issuedToCommonName, issuedToOrganization, issuedToOrganizationalUnit, issuedByCommonName, issuedByOrganization,
+                                formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, nightModeInt, pinnedSslCertificate, issuedToCommonName, issuedToOrganization, issuedToOrganizationalUnit, issuedByCommonName, issuedByOrganization,
                                 issuedByOrganizationalUnit, startDateLong, endDateLong);
                     } else {  // No certificate is selected.
                         // Update the database, with PINNED_SSL_CERTIFICATE set to false.
                         domainsDatabaseHelper.updateDomainExceptCertificate(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean,
-                                formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, false);
+                                formDataEnabledBoolean, userAgentString, fontSizeInt, displayWebpageImagesInt, nightModeInt, false);
                     }
                 }
 
index 6ce6cdc..cbacb38 100644 (file)
@@ -75,30 +75,34 @@ public class SettingsFragment extends PreferenceFragment {
         final Preference swipeToRefreshPreference = findPreference("swipe_to_refresh");
         final Preference displayAdditionalAppBarIconsPreference = findPreference("display_additional_app_bar_icons");
         final Preference darkThemePreference = findPreference("dark_theme");
+        final Preference nightModePreference = findPreference("night_mode");
         final Preference displayWebpageImagesPreference = findPreference("display_webpage_images");
 
         // Set dependencies.
-        domStoragePreference.setDependency("javascript_enabled");
         torHomepagePreference.setDependency("proxy_through_orbot");
         torSearchPreference.setDependency("proxy_through_orbot");
         hideSystemBarsPreference.setDependency("full_screen_browsing_mode");
 
-        // Get strings from the preferences.
+        // Get Strings from the preferences.
         String torSearchString = savedPreferences.getString("tor_search", "https://3g2upl4pq6kufc4m.onion/html/?q=");
         String searchString = savedPreferences.getString("search", "https://duckduckgo.com/html/?q=");
 
         // Get booleans from the preferences.
-        boolean javaScriptEnabledBoolean = savedPreferences.getBoolean("javascript_enabled", false);
+        final boolean javaScriptEnabledBoolean = savedPreferences.getBoolean("javascript_enabled", false);
         boolean firstPartyCookiesEnabledBoolean = savedPreferences.getBoolean("first_party_cookies_enabled", false);
         boolean thirdPartyCookiesEnabledBoolean = savedPreferences.getBoolean("third_party_cookies_enabled", false);
         boolean proxyThroughOrbotBoolean = savedPreferences.getBoolean("proxy_through_orbot", false);
         boolean fullScreenBrowsingModeBoolean = savedPreferences.getBoolean("full_screen_browsing_mode", false);
         boolean hideSystemBarsBoolean = savedPreferences.getBoolean("hide_system_bars", false);
         boolean clearEverythingBoolean = savedPreferences.getBoolean("clear_everything", true);
+        final boolean nightModeBoolean = savedPreferences.getBoolean("night_mode", false);
 
         // Only enable `thirdPartyCookiesPreference` if `firstPartyCookiesEnabledBoolean` is `true` and API >= 21.
         thirdPartyCookiesPreference.setEnabled(firstPartyCookiesEnabledBoolean && (Build.VERSION.SDK_INT >= 21));
 
+        // Only enable `domStoragePreference` if either `javaScriptEnabledBoolean` or `nightModeBoolean` is true.
+        domStoragePreference.setEnabled(javaScriptEnabledBoolean || nightModeBoolean);
+
         // We need to inflated a `WebView` to get the default user agent.
         LayoutInflater inflater = getActivity().getLayoutInflater();
         // `@SuppressLint("InflateParams")` removes the warning about using `null` as the `ViewGroup`, which in this case makes sense because we don't want to display `bare_webview` on the screen.  `false` does not attach the view to the root.
@@ -177,9 +181,11 @@ public class SettingsFragment extends PreferenceFragment {
         // Set the default font size as the summary text for the `Default Font Size` preference when the preference screen is loaded.  The default is `100`.
         defaultFontSizePreference.setSummary(savedPreferences.getString("default_font_size", "100") + "%%");
 
+        // Disable `javaScriptPreference` if `nightModeBoolean` is true.  JavaScript will be enabled for all web pages.
+        javaScriptPreference.setEnabled(!nightModeBoolean);
 
         // Set the `javaScriptPreference` icon.
-        if (javaScriptEnabledBoolean) {
+        if (javaScriptEnabledBoolean || nightModeBoolean) {
             javaScriptPreference.setIcon(R.drawable.javascript_enabled);
         } else {
             javaScriptPreference.setIcon(R.drawable.privacy_mode);
@@ -216,17 +222,17 @@ public class SettingsFragment extends PreferenceFragment {
         }
 
         // Set the `domStoragePreference` icon.
-        if (javaScriptEnabledBoolean) {
-            if (savedPreferences.getBoolean("dom_storage_enabled", false)) {
+        if (javaScriptEnabledBoolean || nightModeBoolean) {  // The preference is enabled.
+            if (savedPreferences.getBoolean("dom_storage_enabled", false)) {  // DOM storage is enabled.
                 domStoragePreference.setIcon(R.drawable.dom_storage_enabled);
-            } else {
+            } else {  // DOM storage is disabled.
                 if (MainWebViewActivity.darkTheme) {
                     domStoragePreference.setIcon(R.drawable.dom_storage_disabled_dark);
                 } else {
                     domStoragePreference.setIcon(R.drawable.dom_storage_disabled_light);
                 }
             }
-        } else {
+        } else {  // The preference is disabled.  The icon should be ghosted.
             if (MainWebViewActivity.darkTheme) {
                 domStoragePreference.setIcon(R.drawable.dom_storage_ghosted_dark);
             } else {
@@ -506,6 +512,21 @@ public class SettingsFragment extends PreferenceFragment {
             darkThemePreference.setIcon(R.drawable.theme_light);
         }
 
+        // Set the `nightModePreference` icon.
+        if (nightModeBoolean) {
+            if (MainWebViewActivity.darkTheme) {
+                nightModePreference.setIcon(R.drawable.night_mode_enabled_dark);
+            } else {
+                nightModePreference.setIcon(R.drawable.night_mode_enabled_light);
+            }
+        } else {
+            if (MainWebViewActivity.darkTheme) {
+                nightModePreference.setIcon(R.drawable.night_mode_disabled_dark);
+            } else {
+                nightModePreference.setIcon(R.drawable.night_mode_disabled_light);
+            }
+        }
+
         // Set the `displayWebpageImagesPreference` icon.
         if (savedPreferences.getBoolean("display_webpage_images", true)) {
             if (MainWebViewActivity.darkTheme) {
@@ -531,12 +552,15 @@ public class SettingsFragment extends PreferenceFragment {
 
                 switch (key) {
                     case "javascript_enabled":
-                        // Update the icons.
-                        if (sharedPreferences.getBoolean("javascript_enabled", false)) {
-                            // Update the icon for `javascript_enabled`.
+                        // Update the icons and the DOM storage preference status.
+                        if (sharedPreferences.getBoolean("javascript_enabled", false)) {  // The JavaScript preference is enabled.
+                            // Update the icon for the JavaScript preference.
                             javaScriptPreference.setIcon(R.drawable.javascript_enabled);
 
-                            // Update the icon for `dom_storage_enabled`.
+                            // Update the status of the DOM storage preference.
+                            domStoragePreference.setEnabled(true);
+
+                            // Update the icon for the DOM storage preference.
                             if (sharedPreferences.getBoolean("dom_storage_enabled", false)) {
                                 domStoragePreference.setIcon(R.drawable.dom_storage_enabled);
                             } else {
@@ -546,11 +570,14 @@ public class SettingsFragment extends PreferenceFragment {
                                     domStoragePreference.setIcon(R.drawable.dom_storage_disabled_light);
                                 }
                             }
-                        } else {  // `javascript_enabled` is `false`.
-                            // Update the icon for `javascript_enabled`.
+                        } else {  // The JavaScript preference is disabled.
+                            // Update the icon for the JavaScript preference.
                             javaScriptPreference.setIcon(R.drawable.privacy_mode);
 
-                            // Set the icon for `dom_storage_disabled` to be ghosted.
+                            // Update the status of the DOM storage preference.
+                            domStoragePreference.setEnabled(false);
+
+                            // Set the icon for DOM storage preference to be ghosted.
                             if (MainWebViewActivity.darkTheme) {
                                 domStoragePreference.setIcon(R.drawable.dom_storage_ghosted_dark);
                             } else {
@@ -1188,6 +1215,60 @@ public class SettingsFragment extends PreferenceFragment {
                         startActivity(intent);
                         break;
 
+                    case "night_mode":
+                        // Set the URL to be reloaded on restart to apply the new night mode setting.
+                        MainWebViewActivity.loadUrlOnRestart = true;
+
+                        // Store the current night mode status.
+                        boolean currentNightModeBoolean = sharedPreferences.getBoolean("night_mode", false);
+                        boolean currentJavaScriptBoolean = sharedPreferences.getBoolean("javascript_enabled", false);
+
+                        // Update the icon.
+                        if (currentNightModeBoolean) {
+                            if (MainWebViewActivity.darkTheme) {
+                                nightModePreference.setIcon(R.drawable.night_mode_enabled_dark);
+                            } else {
+                                nightModePreference.setIcon(R.drawable.night_mode_enabled_light);
+                            }
+                        } else {
+                            if (MainWebViewActivity.darkTheme) {
+                                nightModePreference.setIcon(R.drawable.night_mode_disabled_dark);
+                            } else {
+                                nightModePreference.setIcon(R.drawable.night_mode_disabled_light);
+                            }
+                        }
+
+                        // Update the status of `javaScriptPreference` and `domStoragePreference`.
+                        javaScriptPreference.setEnabled(!currentNightModeBoolean);
+                        domStoragePreference.setEnabled(currentNightModeBoolean || currentJavaScriptBoolean);
+
+                        // Update the `javaScriptPreference` icon.
+                        if (currentNightModeBoolean || currentJavaScriptBoolean) {
+                            javaScriptPreference.setIcon(R.drawable.javascript_enabled);
+                        } else {
+                            javaScriptPreference.setIcon(R.drawable.privacy_mode);
+                        }
+
+                        // Update the `domStoragePreference` icon.
+                        if (currentNightModeBoolean || currentJavaScriptBoolean) {  // The preference is enabled.
+                            if (sharedPreferences.getBoolean("dom_storage_enabled", false)) {  // DOM storage is enabled.
+                                domStoragePreference.setIcon(R.drawable.dom_storage_enabled);
+                            } else {  // DOM storage is disabled.
+                                if (MainWebViewActivity.darkTheme) {
+                                    domStoragePreference.setIcon(R.drawable.dom_storage_disabled_dark);
+                                } else {
+                                    domStoragePreference.setIcon(R.drawable.dom_storage_disabled_light);
+                                }
+                            }
+                        } else {  // The preference is disabled.  The icon should be ghosted.
+                            if (MainWebViewActivity.darkTheme) {
+                                domStoragePreference.setIcon(R.drawable.dom_storage_ghosted_dark);
+                            } else {
+                                domStoragePreference.setIcon(R.drawable.dom_storage_ghosted_light);
+                            }
+                        }
+                        break;
+
                     case "display_webpage_images":
                         if (sharedPreferences.getBoolean("display_webpage_images", true)) {
                             // Update the icon.
@@ -1198,7 +1279,7 @@ public class SettingsFragment extends PreferenceFragment {
                             }
 
                             // `mainWebView` does not need to be reloaded because unloaded images will load automatically.
-                            MainWebViewActivity.reloadOnRestartBoolean = false;
+                            MainWebViewActivity.reloadOnRestart = false;
                         } else {
                             // Update the icon.
                             if (MainWebViewActivity.darkTheme) {
@@ -1208,7 +1289,7 @@ public class SettingsFragment extends PreferenceFragment {
                             }
 
                             // Set `mainWebView` to reload on restart to remove the current images.
-                            MainWebViewActivity.reloadOnRestartBoolean = true;
+                            MainWebViewActivity.reloadOnRestart = true;
                         }
                         break;
                 }
index 3100d69..0331a01 100644 (file)
@@ -26,7 +26,7 @@ import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 
 public class DomainsDatabaseHelper extends SQLiteOpenHelper {
-    private static final int SCHEMA_VERSION = 3;
+    private static final int SCHEMA_VERSION = 4;
     private static final String DOMAINS_DATABASE = "domains.db";
     private static final String DOMAINS_TABLE = "domains";
 
@@ -40,6 +40,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
     public static final String USER_AGENT = "useragent";
     public static final String FONT_SIZE = "fontsize";
     public static final String DISPLAY_IMAGES = "displayimages";
+    public static final String NIGHT_MODE = "nightmode";
     public static final String PINNED_SSL_CERTIFICATE = "pinnedsslcertificate";
     public static final String SSL_ISSUED_TO_COMMON_NAME = "sslissuedtocommonname";
     public static final String SSL_ISSUED_TO_ORGANIZATION = "sslissuedtoorganization";
@@ -50,10 +51,16 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
     public static final String SSL_START_DATE = "sslstartdate";
     public static final String SSL_END_DATE = "sslenddate";
 
+    // Display webpage images constants.
     public static final int DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT = 0;
     public static final int DISPLAY_WEBPAGE_IMAGES_ENABLED = 1;
     public static final int DISPLAY_WEBPAGE_IMAGES_DISABLED = 2;
 
+    // Night mode constants.
+    public static final int NIGHT_MODE_SYSTEM_DEFAULT = 0;
+    public static final int NIGHT_MODE_ENABLED = 1;
+    public static final int NIGHT_MODE_DISABLED = 2;
+
     // Initialize the database.  The lint warnings for the unused parameters are suppressed.
     public DomainsDatabaseHelper(Context context, @SuppressWarnings("UnusedParameters") String name, SQLiteDatabase.CursorFactory cursorFactory, @SuppressWarnings("UnusedParameters") int version) {
         super(context, DOMAINS_DATABASE, cursorFactory, SCHEMA_VERSION);
@@ -73,6 +80,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
                 USER_AGENT + " TEXT, " +
                 FONT_SIZE + " INTEGER, " +
                 DISPLAY_IMAGES + " INTEGER, " +
+                NIGHT_MODE + " INTEGER, " +
                 PINNED_SSL_CERTIFICATE + " BOOLEAN, " +
                 SSL_ISSUED_TO_COMMON_NAME + " TEXT, " +
                 SSL_ISSUED_TO_ORGANIZATION + " TEXT, " +
@@ -108,6 +116,11 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
                 domainsDatabase.execSQL("ALTER TABLE " + DOMAINS_TABLE + " ADD COLUMN " + SSL_ISSUED_BY_ORGANIZATIONAL_UNIT + " TEXT");
                 domainsDatabase.execSQL("ALTER TABLE " + DOMAINS_TABLE + " ADD COLUMN " + SSL_START_DATE + " INTEGER");
                 domainsDatabase.execSQL("ALTER TABLE " + DOMAINS_TABLE + " ADD COLUMN " + SSL_END_DATE + " INTEGER");
+
+            // Upgrade from `SCHEMA_VERSION` 3.
+            case 3:
+                // Add the `NIGHT_MODE` column.
+                domainsDatabase.execSQL("ALTER TABLE " + DOMAINS_TABLE + " ADD COLUMN " + NIGHT_MODE + " INTEGER");
         }
     }
 
@@ -166,7 +179,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         // Store the domain data in a `ContentValues`.
         ContentValues domainContentValues = new ContentValues();
 
-        // Create entries for each field in the database.  The ID is created automatically.
+        // Create entries for the database fields.  The ID is created automatically.  The pinned SSL certificate information is not created unless added by the user.
         domainContentValues.put(DOMAIN_NAME, domainName);
         domainContentValues.put(ENABLE_JAVASCRIPT, false);
         domainContentValues.put(ENABLE_FIRST_PARTY_COOKIES, false);
@@ -176,6 +189,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         domainContentValues.put(USER_AGENT, "System default user agent");
         domainContentValues.put(FONT_SIZE, 0);
         domainContentValues.put(DISPLAY_IMAGES, 0);
+        domainContentValues.put(NIGHT_MODE, 0);
 
         // Get a writable database handle.
         SQLiteDatabase domainsDatabase = this.getWritableDatabase();
@@ -186,11 +200,12 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         // Close the database handle.
         domainsDatabase.close();
 
+        // Return the new domain database ID.
         return newDomainDatabaseId;
     }
 
     public void updateDomainExceptCertificate(int databaseId, String domainName, boolean javaScriptEnabled, boolean firstPartyCookiesEnabled, boolean thirdPartyCookiesEnabled, boolean domStorageEnabled, boolean formDataEnabled, String userAgent, int fontSize,
-                                              int displayImages, boolean pinnedSslCertificate) {
+                                              int displayImages, int nightMode, boolean pinnedSslCertificate) {
         // Store the domain data in a `ContentValues`.
         ContentValues domainContentValues = new ContentValues();
 
@@ -204,6 +219,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         domainContentValues.put(USER_AGENT, userAgent);
         domainContentValues.put(FONT_SIZE, fontSize);
         domainContentValues.put(DISPLAY_IMAGES, displayImages);
+        domainContentValues.put(NIGHT_MODE, nightMode);
         domainContentValues.put(PINNED_SSL_CERTIFICATE, pinnedSslCertificate);
 
         // Get a writable database handle.
@@ -217,7 +233,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
     }
 
     public void updateDomainWithCertificate(int databaseId, String domainName, boolean javaScriptEnabled, boolean firstPartyCookiesEnabled, boolean thirdPartyCookiesEnabled, boolean domStorageEnabled, boolean formDataEnabled, String userAgent, int fontSize,
-                                            int displayImages, boolean pinnedSslCertificate, String sslIssuedToCommonName, String sslIssuedToOrganization, String sslIssuedToOrganizationalUnit, String sslIssuedByCommonName, String sslIssuedByOrganization,
+                                            int displayImages, int nightMode, boolean pinnedSslCertificate, String sslIssuedToCommonName, String sslIssuedToOrganization, String sslIssuedToOrganizationalUnit, String sslIssuedByCommonName, String sslIssuedByOrganization,
                                             String sslIssuedByOrganizationalUnit, long sslStartDate, long sslEndDate) {
         // Store the domain data in a `ContentValues`.
         ContentValues domainContentValues = new ContentValues();
@@ -232,6 +248,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         domainContentValues.put(USER_AGENT, userAgent);
         domainContentValues.put(FONT_SIZE, fontSize);
         domainContentValues.put(DISPLAY_IMAGES, displayImages);
+        domainContentValues.put(NIGHT_MODE, nightMode);
         domainContentValues.put(PINNED_SSL_CERTIFICATE, pinnedSslCertificate);
         domainContentValues.put(SSL_ISSUED_TO_COMMON_NAME, sslIssuedToCommonName);
         domainContentValues.put(SSL_ISSUED_TO_ORGANIZATION, sslIssuedToOrganization);
index 1da26b7..da95078 100644 (file)
@@ -6,7 +6,7 @@
     android:viewportHeight="24.0"
     android:viewportWidth="24.0" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FFE0E0E0"
         android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
index a64e797..3719987 100644 (file)
@@ -6,7 +6,7 @@
     android:viewportHeight="24.0"
     android:viewportWidth="24.0" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FFFFFFFF"
         android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
diff --git a/app/src/main/res/drawable/night_mode_disabled_dark.xml b/app/src/main/res/drawable/night_mode_disabled_dark.xml
new file mode 100644 (file)
index 0000000..175e81b
--- /dev/null
@@ -0,0 +1,19 @@
+<!-- `night_mode_disabled_dark.xml` is derived from `ic_compare`, which is part of the Android Material icon set.  `ic_compare` is released under the Apache License 2.0.
+    Modifications copyright © 2017 Soren Stoutner <soren@stoutner.com>.  The resulting image is released under the GPLv3+ license. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:autoMirrored="true"
+    tools:ignore="VectorRaster" >
+
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF9E9E9E"
+        android:pathData="m14,3h5c1.1,0 2,0.9 2,2v14c0,1.1 -0.9,2 -2,2h-5v2L12,23L12,1h2zM14,18h5L14,12ZM5,3h5L10,5L5,5v13l5,-6v9L5,21C3.9,21 3,20.1 3,19L3,5C3,3.9 3.9,3 5,3Z"/>
+</vector>
diff --git a/app/src/main/res/drawable/night_mode_disabled_light.xml b/app/src/main/res/drawable/night_mode_disabled_light.xml
new file mode 100644 (file)
index 0000000..81af037
--- /dev/null
@@ -0,0 +1,19 @@
+<!-- `night_mode_disabled_dark.xml` is derived from `ic_compare`, which is part of the Android Material icon set.  `ic_compare` is released under the Apache License 2.0.
+    Modifications copyright © 2017 Soren Stoutner <soren@stoutner.com>.  The resulting image is released under the GPLv3+ license. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:autoMirrored="true"
+    tools:ignore="VectorRaster" >
+
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#88000000"
+        android:pathData="m14,3h5c1.1,0 2,0.9 2,2v14c0,1.1 -0.9,2 -2,2h-5v2L12,23L12,1h2zM14,18h5L14,12ZM5,3h5L10,5L5,5v13l5,-6v9L5,21C3.9,21 3,20.1 3,19L3,5C3,3.9 3.9,3 5,3Z"/>
+</vector>
diff --git a/app/src/main/res/drawable/night_mode_enabled_dark.xml b/app/src/main/res/drawable/night_mode_enabled_dark.xml
new file mode 100644 (file)
index 0000000..b71a983
--- /dev/null
@@ -0,0 +1,19 @@
+<!-- `night_mode_enabled_dark.xml` is derived from `ic_compare`, which is part of the Android Material icon set.  `ic_compare` is released under the Apache License 2.0.
+    Modifications copyright © 2017 Soren Stoutner <soren@stoutner.com>.  The resulting image is released under the GPLv3+ license. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:autoMirrored="true"
+    tools:ignore="VectorRaster" >
+
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF1E88E5"
+        android:pathData="m14,3h5c1.1,0 2,0.9 2,2v14c0,1.1 -0.9,2 -2,2h-5v2L12,23L12,1h2zM14,18h5L14,12ZM5,3h5L10,5L5,5v13l5,-6v9L5,21C3.9,21 3,20.1 3,19L3,5C3,3.9 3.9,3 5,3Z"/>
+</vector>
diff --git a/app/src/main/res/drawable/night_mode_enabled_light.xml b/app/src/main/res/drawable/night_mode_enabled_light.xml
new file mode 100644 (file)
index 0000000..2d13d31
--- /dev/null
@@ -0,0 +1,19 @@
+<!-- `night_mode_enabled_light.xml` is derived from `ic_compare`, which is part of the Android Material icon set.  `ic_compare` is released under the Apache License 2.0.
+    Modifications copyright © 2017 Soren Stoutner <soren@stoutner.com>.  The resulting image is released under the GPLv3+ license. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:autoMirrored="true"
+    tools:ignore="VectorRaster" >
+
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF1565C0"
+        android:pathData="m14,3h5c1.1,0 2,0.9 2,2v14c0,1.1 -0.9,2 -2,2h-5v2L12,23L12,1h2zM14,18h5L14,12ZM5,3h5L10,5L5,5v13l5,-6v9L5,21C3.9,21 3,20.1 3,19L3,5C3,3.9 3.9,3 5,3Z"/>
+</vector>
index d5879ce..8d12208 100644 (file)
@@ -8,21 +8,21 @@
     android:viewportHeight="256.0"
     android:viewportWidth="256.0" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillAlpha="1"
         android:fillColor="#0d4781"
         android:pathData="m128,12.8 l-94.25,41.89 0,62.84c0,58.12 40.22,112.48 94.25,125.67 54.04,-13.2 94.25,-67.55 94.25,-125.67l0,-62.84z"
         android:strokeColor="#00000000" />
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillAlpha="1"
         android:fillColor="#1976d2"
         android:pathData="m128,0 l-104.73,46.55 0,69.82C23.27,180.95 67.96,241.34 128,256 188.04,241.34 232.73,180.95 232.73,116.36l0,-69.82L128,0ZM128,127.88 L209.45,127.88C203.29,175.83 171.29,218.53 128,231.91l0,-103.91 -81.45,0 0,-66.33L128,25.48l0,102.4z"
         android:strokeColor="#00000000" />
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillAlpha="1"
         android:fillColor="#ffffff"
index 0059dd5..753dd7d 100644 (file)
@@ -11,7 +11,7 @@
     android:viewportWidth="24.0"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF9E9E9E"
         android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
index 2758345..37eb36f 100644 (file)
@@ -11,7 +11,7 @@
     android:viewportWidth="24.0"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#88000000"
         android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
index 6c17502..23fd9c6 100644 (file)
@@ -11,7 +11,7 @@
     android:viewportWidth="24.0"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF1E88E5"
         android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
index f2e0916..3dd3eca 100644 (file)
@@ -11,7 +11,7 @@
     android:viewportWidth="24.0"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF1565C0"
         android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
index b27a1c4..73c8e5d 100644 (file)
                 android:textSize="13sp" />
         </LinearLayout>
 
+        <!-- Night Mode. -->
+        <LinearLayout
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:orientation="vertical"
+            android:layout_marginTop="14dp"
+            android:layout_marginBottom="14dp" >
+
+            <LinearLayout
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:orientation="horizontal" >
+
+                <ImageView
+                    android:id="@+id/domain_settings_night_mode_imageview"
+                    android:layout_height="wrap_content"
+                    android:layout_width="wrap_content"
+                    android:layout_marginTop="1dp"
+                    android:layout_marginEnd="10dp"
+                    android:layout_gravity="center_vertical"
+                    android:contentDescription="@string/night_mode" />
+
+                <Spinner
+                    android:id="@+id/domain_settings_night_mode_spinner"
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent" />
+            </LinearLayout>
+
+            <TextView
+                android:id="@+id/domain_settings_night_mode_textview"
+                android:layout_height="match_parent"
+                android:layout_width="match_parent"
+                android:layout_marginStart="45dp"
+                android:layout_marginEnd="36dp"
+                android:textSize="13sp" />
+        </LinearLayout>
+
         <!-- Pinned SSL Certificate -->
         <LinearLayout
             android:layout_height="wrap_content"
index 1b14eff..74ae827 100644 (file)
@@ -19,6 +19,7 @@
   along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>. -->
 
 <!-- `android:layout_weight="1"` sets the `RelativeLayout` to fill the rest of the screen because it is encapsulated in a `LinearLayout` with `android:orientation="vertical"`. -->
+<!-- `android:background=@color/gray_900` sets the background color that is displayed when a website is loading in night mode. -->
 <RelativeLayout
     android:id="@+id/main_webview_relativelayout"
     xmlns:android="http://schemas.android.com/apk/res/android"
@@ -26,6 +27,7 @@
     android:layout_height="0dp"
     android:layout_width="match_parent"
     android:layout_weight="1"
+    android:background="@color/gray_900"
     tools:context="com.stoutner.privacybrowser.activities.MainWebViewActivity" >
 
     <!-- This `TextView` has an id of `adView` so that the ad commands (which do nothing in the standard flavor) don't produce errors. -->
index 4111fc6..d99b337 100644 (file)
     <string name="domain_name">Nombre de dominio</string>
     <string name="domain_deleted">Dominio borrado</string>
     <string name="domain_name_instructions">*. puede ser añadido a un dominio para incluir todos los subdominios (p.ej. *.stoutner.com)</string>
-    <string-array name="display_website_images_array">
+    <string-array name="display_webpage_images_array">
         <item>Por defecto del sistema</item>
         <item>Imágenes habilitadas</item>
         <item>Imágenes deshabilitadas</item>
index 2dcf2ba..e73cf7a 100644 (file)
     <string name="domain_name">Nome del Dominio</string>
     <string name="domain_deleted">Dominio Eliminato</string>
     <string name="domain_name_instructions">è possibile anteporre *. a un dominio per includere tutti i sottodomini (es. *.stoutner.com)</string>
-    <string-array name="display_website_images_array">
+    <string-array name="display_webpage_images_array">
         <item>Impostazioni di default</item>
         <item>Abilita Immagini</item>
         <item>Disabilita Immagini</item>
index c15727c..62e75a0 100644 (file)
     <string name="domain_name">Domain name</string>
     <string name="domain_deleted">Domain deleted</string>
     <string name="domain_name_instructions">*. may be prepended to a domain to include all subdomains (eg. *.stoutner.com)</string>
-    <string-array name="display_website_images_array">
+    <string-array name="display_webpage_images_array">
         <item>System default</item>
         <item>Images enabled</item>
         <item>Images disabled</item>
     </string-array>
+    <string-array name="night_mode_array">
+        <item>System default</item>
+        <item>Night mode enabled</item>
+        <item>Night mode disabled</item>
+    </string-array>
     <string name="pinned_ssl_certificate">Pinned SSL certificate</string>
     <string name="saved_ssl_certificate">Saved SSL certificate</string>
     <string name="current_website_ssl_certificate">Current website SSL certificate</string>
         <string name="display_additional_app_bar_icons_summary">Display icons for toggling cookies, DOM storage, and form data in the app bar if there is room.</string>
         <string name="dark_theme">Dark theme</string>
         <string name="dark_theme_summary">Changing the theme will restart Privacy Browser.</string>
+        <string name="night_mode">Night mode</string>
+        <string name="night_mode_summary">Enabling night mode will also enable JavaScript for all web pages.</string>
         <string name="display_webpage_images">Display webpage images</string>
         <string name="display_webpage_images_summary">Disable to conserve bandwidth.</string>
 
index fbfb633..160536d 100644 (file)
             android:summary="@string/dark_theme_summary"
             android:defaultValue="false" />
 
+        <SwitchPreference
+            android:key="night_mode"
+            android:title="@string/night_mode"
+            android:summary="@string/night_mode_summary"
+            android:defaultValue="false" />
+
         <SwitchPreference
             android:key="display_webpage_images"
             android:title="@string/display_webpage_images"
diff --git a/fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch - it.png b/fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch - it.png
deleted file mode 100644 (file)
index 8cce8d9..0000000
Binary files a/fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch - it.png and /dev/null differ
diff --git a/fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch.png b/fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch.png
new file mode 100644 (file)
index 0000000..14bb61e
Binary files /dev/null and b/fastlane/metadata/android/it/sevenInchScreenshots/02 - SSL Certificate Mismatch.png differ