Browse Source

Dark mode: light, dark, follow system

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 5 years ago
parent
commit
5e687f2c17

+ 6 - 6
src/main/java/com/nextcloud/client/preferences/AppPreferences.java

@@ -39,7 +39,7 @@ public interface AppPreferences {
      * events.
      */
     interface Listener {
-        default void onDarkThemeEnabledChanged(boolean enabled) {
+        default void onDarkThemeModeChanged(DarkMode mode) {
             /* default empty implementation */
         };
     }
@@ -274,18 +274,18 @@ public interface AppPreferences {
     int getUploaderBehaviour();
 
     /**
-     * Enable dark theme.
+     * Changes dark theme mode
      *
      * This is reactive property. Listeners will be invoked if registered.
      *
-     * @param enabled true to turn dark theme on, false to turn it off
+     * @param mode dark mode setting: on, off, system
      */
-    void setDarkThemeEnabled(boolean enabled);
+    void setDarkThemeMode(DarkMode mode);
 
     /**
-     * @return true if application uses dark UI theme, false otherwise
+     * @return dark mode setting: on, off, system
      */
-    boolean isDarkThemeEnabled();
+    DarkMode getDarkThemeMode();
 
     /**
      * Saves the uploader behavior which the user has set last.

+ 13 - 9
src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -21,7 +21,6 @@
 
 package com.nextcloud.client.preferences;
 
-import android.accounts.Account;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -57,6 +56,7 @@ public final class AppPreferencesImpl implements AppPreferences {
      */
     public static final String AUTO_PREF__LAST_SEEN_VERSION_CODE = "lastSeenVersionCode";
     public static final String STORAGE_PATH = "storage_path";
+    public static final String PREF__DARK_THEME = "dark_theme_mode";
     public static final float DEFAULT_GRID_COLUMN = 4.0f;
 
     private static final String AUTO_PREF__LAST_UPLOAD_PATH = "last_upload_path";
@@ -79,7 +79,6 @@ public final class AppPreferencesImpl implements AppPreferences {
     private static final String PREF__AUTO_UPLOAD_INIT = "autoUploadInit";
     private static final String PREF__FOLDER_SORT_ORDER = "folder_sort_order";
     private static final String PREF__FOLDER_LAYOUT = "folder_layout";
-    static final String PREF__DARK_THEME_ENABLED = "dark_theme_enabled";
 
     private static final String PREF__LOCK_TIMESTAMP = "lock_timestamp";
     private static final String PREF__SHOW_MEDIA_SCAN_NOTIFICATIONS = "show_media_scan_notifications";
@@ -121,10 +120,10 @@ public final class AppPreferencesImpl implements AppPreferences {
 
         @Override
         public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
-            if(PREF__DARK_THEME_ENABLED.equals(key)) {
-                boolean enabled = preferences.isDarkThemeEnabled();
+            if (PREF__DARK_THEME.equals(key)) {
+                DarkMode mode = preferences.getDarkThemeMode();
                 for(Listener l : listeners) {
-                    l.onDarkThemeEnabledChanged(enabled);
+                    l.onDarkThemeModeChanged(mode);
                 }
             }
         }
@@ -408,13 +407,18 @@ public final class AppPreferencesImpl implements AppPreferences {
     }
 
     @Override
-    public void setDarkThemeEnabled(boolean enabled) {
-        preferences.edit().putBoolean(PREF__DARK_THEME_ENABLED, enabled).apply();
+    public void setDarkThemeMode(DarkMode mode) {
+        preferences.edit().putString(PREF__DARK_THEME, mode.name()).apply();
     }
 
     @Override
-    public boolean isDarkThemeEnabled() {
-        return preferences.getBoolean(PREF__DARK_THEME_ENABLED, false);
+    public DarkMode getDarkThemeMode() {
+        try {
+            return DarkMode.valueOf(preferences.getString(PREF__DARK_THEME, DarkMode.LIGHT.name()));
+        } catch (ClassCastException e) {
+            preferences.edit().putString(PREF__DARK_THEME, DarkMode.LIGHT.name()).apply();
+            return DarkMode.DARK;
+        }
     }
 
     @Override

+ 27 - 0
src/main/java/com/nextcloud/client/preferences/DarkMode.java

@@ -0,0 +1,27 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.preferences;
+
+public enum DarkMode {
+    DARK, LIGHT, SYSTEM
+}

+ 13 - 6
src/main/java/com/owncloud/android/MainApp.java

@@ -57,6 +57,7 @@ import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.client.onboarding.OnboardingService;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferencesImpl;
+import com.nextcloud.client.preferences.DarkMode;
 import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.MediaFolder;
@@ -247,7 +248,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     @SuppressFBWarnings("ST")
     @Override
     public void onCreate() {
-        setAppTheme(preferences.isDarkThemeEnabled());
+        setAppTheme(preferences.getDarkThemeMode());
         super.onCreate();
 
         insertConscrypt();
@@ -821,11 +822,17 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     }
 
 
-    public static void setAppTheme(Boolean darkTheme) {
-        if (darkTheme) {
-            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
-        } else {
-            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+    public static void setAppTheme(DarkMode mode) {
+        switch (mode) {
+            case LIGHT:
+                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+                break;
+            case DARK:
+                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
+                break;
+            case SYSTEM:
+                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
+                break;
         }
     }
 }

+ 7 - 6
src/main/java/com/owncloud/android/ui/activity/BaseActivity.java

@@ -15,6 +15,7 @@ import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.java.util.Optional;
+import com.nextcloud.client.preferences.DarkMode;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
@@ -59,8 +60,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
 
     private AppPreferences.Listener onPreferencesChanged = new AppPreferences.Listener() {
         @Override
-        public void onDarkThemeEnabledChanged(boolean enabled) {
-            BaseActivity.this.onThemeSettingsChanged();
+        public void onDarkThemeModeChanged(DarkMode mode) {
+            onThemeSettingsModeChanged();
         }
     };
 
@@ -91,7 +92,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
         super.onResume();
         paused = false;
 
-        if(themeChangePending) {
+        if (themeChangePending) {
             recreate();
         }
     }
@@ -129,8 +130,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
         Log_OC.v(TAG, "onRestart() end");
     }
 
-    private void onThemeSettingsChanged() {
-        if(paused) {
+    private void onThemeSettingsModeChanged() {
+        if (paused) {
             themeChangePending = true;
         } else {
             recreate();
@@ -226,7 +227,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
             return Optional.empty();
         }
     }
-    
+
     public FileDataStorageManager getStorageManager() {
         return storageManager;
     }

+ 7 - 5
src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -57,11 +57,11 @@ import com.bumptech.glide.request.animation.GlideAnimation;
 import com.bumptech.glide.request.target.SimpleTarget;
 import com.google.android.material.navigation.NavigationView;
 import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.onboarding.FirstRunActivity;
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.client.preferences.DarkMode;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.PassCodeManager;
@@ -71,7 +71,6 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.ExternalLink;
 import com.owncloud.android.lib.common.ExternalLinkType;
-import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.Quota;
 import com.owncloud.android.lib.common.UserInfo;
 import com.owncloud.android.lib.common.accounts.ExternalLinksOperation;
@@ -1275,9 +1274,12 @@ public abstract class DrawerActivity extends ToolbarActivity
     @Override
     protected void onResume() {
         super.onResume();
-        getDelegate().setLocalNightMode(preferences.isDarkThemeEnabled() ?
-                                        AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
-        getDelegate().applyDayNight();
+        if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) {
+
+            getDelegate().setLocalNightMode(DarkMode.DARK == preferences.getDarkThemeMode() ?
+                                                AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
+            getDelegate().applyDayNight();
+        }
         setDrawerMenuItemChecked(mCheckedMenuItem);
     }
 

+ 23 - 6
src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java

@@ -59,6 +59,7 @@ import com.nextcloud.client.logger.ui.LogsActivity;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferencesImpl;
+import com.nextcloud.client.preferences.DarkMode;
 import com.owncloud.android.BuildConfig;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -79,6 +80,7 @@ import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.util.ArrayList;
+import java.util.List;
 
 import javax.inject.Inject;
 
@@ -106,6 +108,7 @@ public class SettingsActivity extends ThemedPreferenceActivity
     public static final String LOCK_PASSCODE = "passcode";
     public static final String LOCK_DEVICE_CREDENTIALS = "device_credentials";
 
+
     public final static String PREFERENCE_USE_FINGERPRINT = "use_fingerprint";
     public static final String PREFERENCE_SHOW_MEDIA_SCAN_NOTIFICATIONS = "show_media_scan_notifications";
 
@@ -692,13 +695,27 @@ public class SettingsActivity extends ThemedPreferenceActivity
 
         loadStoragePath();
 
-        SwitchPreference themePref = (SwitchPreference) findPreference("dark_theme_enabled");
-        boolean darkThemeEnabled = preferences.isDarkThemeEnabled();
-        int summaryResId = darkThemeEnabled ? R.string.prefs_value_theme_dark : R.string.prefs_value_theme_light;
-        themePref.setSummary(summaryResId);
+        ListPreference themePref = (ListPreference) findPreference("darkTheme");
+
+        List<String> themeEntries = new ArrayList<>(3);
+        themeEntries.add(getString(R.string.prefs_value_theme_light));
+        themeEntries.add(getString(R.string.prefs_value_theme_dark));
+        themeEntries.add(getString(R.string.prefs_value_theme_system));
+
+        List<String> themeValues = new ArrayList<>(3);
+        themeValues.add(DarkMode.LIGHT.name());
+        themeValues.add(DarkMode.DARK.name());
+        themeValues.add(DarkMode.SYSTEM.name());
+
+        themePref.setEntries(themeEntries.toArray(new String[0]));
+        themePref.setEntryValues(themeValues.toArray(new String[0]));
+        themePref.setSummary(themePref.getEntry().length() == 0 ? DarkMode.LIGHT.name() : themePref.getEntry());
+
         themePref.setOnPreferenceChangeListener((preference, newValue) -> {
-            boolean enabled = (Boolean)newValue;
-            MainApp.setAppTheme(enabled);
+            DarkMode mode = DarkMode.valueOf((String) newValue);
+            preferences.setDarkThemeMode(mode);
+            MainApp.setAppTheme(mode);
+
             return true;
         });
     }

+ 6 - 3
src/main/java/com/owncloud/android/ui/activity/ThemedPreferenceActivity.java

@@ -24,6 +24,7 @@ import android.os.Bundle;
 import android.preference.PreferenceActivity;
 
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.client.preferences.DarkMode;
 
 import javax.inject.Inject;
 
@@ -41,8 +42,10 @@ public class ThemedPreferenceActivity extends PreferenceActivity {
 
     private AppPreferences.Listener onThemeChangedListener = new AppPreferences.Listener() {
         @Override
-        public void onDarkThemeEnabledChanged(boolean enabled) {
-            if(paused) {
+        public void onDarkThemeModeChanged(DarkMode mode) {
+            preferences.setDarkThemeMode(mode);
+
+            if (paused) {
                 themeChangePending = true;
                 return;
             }
@@ -73,7 +76,7 @@ public class ThemedPreferenceActivity extends PreferenceActivity {
         super.onResume();
         paused = false;
 
-        if(themeChangePending) {
+        if (themeChangePending) {
             recreate();
         }
     }

+ 1 - 0
src/main/res/values/strings.xml

@@ -58,6 +58,7 @@
     <string name="prefs_imprint">Imprint</string>
     <string name="prefs_value_theme_light">Light</string>
     <string name="prefs_value_theme_dark">Dark</string>
+    <string name="prefs_value_theme_system">Follow system</string>
     <string name="prefs_theme_title">Theme</string>
 
 

+ 0 - 3
src/main/res/values/styles.xml

@@ -322,7 +322,4 @@
         <item name="android:scaleType">fitCenter</item>
         <item name="android:layout_gravity">center_vertical</item>
     </style>
-    <style name="SwitchPreference" parent="Widget.AppCompat.CompoundButton.Switch">
-        <item name="android:colorForeground">@color/fg_default</item>
-    </style>
 </resources>

+ 3 - 6
src/main/res/xml/preferences.xml

@@ -26,13 +26,10 @@
         <ListPreference
             android:title="@string/prefs_storage_path"
             android:key="storage_path"/>
-        <com.owncloud.android.ui.ThemeableSwitchPreference
-            android:id="@+id/dark_theme_preference"
-            android:defaultValue="@string/prefs_value_theme_light"
-            android:key="dark_theme_enabled"
-            android:summary="%s"
+        <ListPreference
             android:title="@string/prefs_theme_title"
-            android:theme="@style/SwitchPreference"/>
+            android:key="darkTheme"
+            android:summary="%s" />
 	</PreferenceCategory>
 	<PreferenceCategory
 		android:title="@string/drawer_synced_folders"

+ 16 - 11
src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java

@@ -11,10 +11,15 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 import org.mockito.InOrder;
 import org.mockito.Mock;
-import static org.mockito.Mockito.*;
-
 import org.mockito.MockitoAnnotations;
 
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
     TestAppPreferences.Preferences.class,
@@ -45,7 +50,7 @@ public class TestAppPreferences {
         @Before
         public void setUp() {
             MockitoAnnotations.initMocks(this);
-            when(appPreferences.isDarkThemeEnabled()).thenReturn(true);
+            when(appPreferences.getDarkThemeMode()).thenReturn(DarkMode.DARK);
             registry = new AppPreferencesImpl.ListenerRegistry(appPreferences);
         }
 
@@ -64,21 +69,21 @@ public class TestAppPreferences {
                 registry.remove(listener2);
                 registry.remove(listener3);
                 return null;
-            }).when(listener2).onDarkThemeEnabledChanged(anyBoolean());
+            }).when(listener2).onDarkThemeModeChanged(DarkMode.DARK);
 
             // WHEN
             //      callback is called twice
-            registry.onSharedPreferenceChanged(NOT_USED_NULL, AppPreferencesImpl.PREF__DARK_THEME_ENABLED);
-            registry.onSharedPreferenceChanged(NOT_USED_NULL, AppPreferencesImpl.PREF__DARK_THEME_ENABLED);
+            registry.onSharedPreferenceChanged(NOT_USED_NULL, AppPreferencesImpl.PREF__DARK_THEME);
+            registry.onSharedPreferenceChanged(NOT_USED_NULL, AppPreferencesImpl.PREF__DARK_THEME);
 
             // THEN
             //      no ConcurrentModificationException
             //      1st time, all listeners (including removed) are called
             //      2nd time removed callbacks are not called
-            verify(listener1, times(2)).onDarkThemeEnabledChanged(anyBoolean());
-            verify(listener2).onDarkThemeEnabledChanged(anyBoolean());
-            verify(listener3).onDarkThemeEnabledChanged(anyBoolean());
-            verify(listener4, times(2)).onDarkThemeEnabledChanged(anyBoolean());
+            verify(listener1, times(2)).onDarkThemeModeChanged(DarkMode.DARK);
+            verify(listener2).onDarkThemeModeChanged(DarkMode.DARK);
+            verify(listener3).onDarkThemeModeChanged(DarkMode.DARK);
+            verify(listener4, times(2)).onDarkThemeModeChanged(DarkMode.DARK);
         }
 
         @Test
@@ -90,7 +95,7 @@ public class TestAppPreferences {
 
             // WHEN
             //      callback is called
-            registry.onSharedPreferenceChanged(NOT_USED_NULL, AppPreferencesImpl.PREF__DARK_THEME_ENABLED);
+            registry.onSharedPreferenceChanged(NOT_USED_NULL, AppPreferencesImpl.PREF__DARK_THEME);
 
             // THEN
             //      nothing happens