From a1f1693d0bf0b138b538ee5951fe16480ea317a0 Mon Sep 17 00:00:00 2001 From: Philip Cohn-Cort Date: Mon, 23 Apr 2018 18:31:44 +0200 Subject: [PATCH] Add a dark theme Summary: BUG: 375376 This revision adds dark mode support to the app ( T7044 ). It does so by injecting theme information into each activity, and making sure that all Views define their colors by reference to theme attributes. In order to make this work, all of the buttons with images (like the list of available devices) now are tinted according to the theme. While all versions of android support the theme, only devices running Android ICS or higher will have a toggle button in the drawer. Test Plan: Open all the screens, both with and without the dark theme on. Reviewers: #kde_connect, mtijink, #vdg, nicolasfella Reviewed By: #kde_connect, mtijink, nicolasfella Subscribers: apol, ngraham, nicolasfella, mtijink Tags: #kde_connect Differential Revision: https://phabricator.kde.org/D11694 --- .../state_list_drawer_background_dark.xml | 5 ++ res/layout-v14/nav_dark_mode_switch.xml | 18 ++++++ res/layout/activity_device.xml | 61 ++++++++++++------- res/layout/activity_main.xml | 7 +-- res/layout/list_item_with_icon_entry.xml | 7 ++- res/layout/mpris_control.xml | 3 +- res/layout/preference_with_button.xml | 7 ++- res/values-v21/styles-dark.xml | 12 ++++ res/values/attrs.xml | 6 ++ res/values/styles-dark.xml | 40 ++++++++++++ res/values/styles.xml | 14 +++++ .../FindMyPhoneActivity.java | 2 + .../MousePadPlugin/MousePadActivity.java | 2 + .../Plugins/MprisPlugin/MprisActivity.java | 2 + .../NotificationFilterActivity.java | 2 + .../RunCommandPlugin/RunCommandActivity.java | 2 + .../Plugins/SharePlugin/SendFileActivity.java | 2 + .../Plugins/SharePlugin/ShareActivity.java | 4 ++ .../AppCompatPreferenceActivity.java | 2 + .../UserInterface/CustomDevicesActivity.java | 1 + .../UserInterface/DeviceFragment.java | 1 + .../UserInterface/MainActivity.java | 44 +++++++++++++ .../kdeconnect/UserInterface/ThemeUtil.java | 46 ++++++++++++++ 23 files changed, 258 insertions(+), 32 deletions(-) create mode 100644 res/drawable/state_list_drawer_background_dark.xml create mode 100644 res/layout-v14/nav_dark_mode_switch.xml create mode 100644 res/values-v21/styles-dark.xml create mode 100644 res/values/attrs.xml create mode 100644 res/values/styles-dark.xml create mode 100644 src/org/kde/kdeconnect/UserInterface/ThemeUtil.java diff --git a/res/drawable/state_list_drawer_background_dark.xml b/res/drawable/state_list_drawer_background_dark.xml new file mode 100644 index 00000000..10a24fb6 --- /dev/null +++ b/res/drawable/state_list_drawer_background_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/res/layout-v14/nav_dark_mode_switch.xml b/res/layout-v14/nav_dark_mode_switch.xml new file mode 100644 index 00000000..8d5b590a --- /dev/null +++ b/res/layout-v14/nav_dark_mode_switch.xml @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/res/layout/activity_device.xml b/res/layout/activity_device.xml index 0794ed24..f7d4466f 100644 --- a/res/layout/activity_device.xml +++ b/res/layout/activity_device.xml @@ -1,5 +1,7 @@ - - + android:orientation="horizontal" + android:gravity="center" + android:visibility="gone" > + + + + + + + - + style="?attr/mainNavigationViewStyle" /> diff --git a/res/layout/list_item_with_icon_entry.xml b/res/layout/list_item_with_icon_entry.xml index a50cdf9c..ebeccff3 100644 --- a/res/layout/list_item_with_icon_entry.xml +++ b/res/layout/list_item_with_icon_entry.xml @@ -1,5 +1,7 @@ - - - diff --git a/res/layout/preference_with_button.xml b/res/layout/preference_with_button.xml index 2ad4afb3..3e1427c9 100644 --- a/res/layout/preference_with_button.xml +++ b/res/layout/preference_with_button.xml @@ -16,7 +16,9 @@ - - diff --git a/res/values-v21/styles-dark.xml b/res/values-v21/styles-dark.xml new file mode 100644 index 00000000..ba86a4b0 --- /dev/null +++ b/res/values-v21/styles-dark.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml new file mode 100644 index 00000000..323b2f20 --- /dev/null +++ b/res/values/attrs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/values/styles-dark.xml b/res/values/styles-dark.xml new file mode 100644 index 00000000..12f5950b --- /dev/null +++ b/res/values/styles-dark.xml @@ -0,0 +1,40 @@ + + + #555555 + #222222 + #333333 + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml index 18c5521e..9c068ac7 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -10,6 +10,10 @@ @color/primary @color/primaryDark @color/accent + @style/KdeConnectTheme.Toolbar + @style/ThemeOverlay.AppCompat.Light + @style/MainNavigationView + @android:color/black @android:color/black @android:color/black @@ -21,8 +25,18 @@ + diff --git a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java index 462b037d..0886dfc1 100644 --- a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java +++ b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java @@ -34,6 +34,7 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; public class FindMyPhoneActivity extends Activity { @@ -56,6 +57,7 @@ public class FindMyPhoneActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_find_my_phone); audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java index 956628cd..7078a5f3 100644 --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java @@ -37,6 +37,7 @@ import android.view.inputmethod.InputMethodManager; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; public class MousePadActivity extends AppCompatActivity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, MousePadGestureDetector.OnGestureListener { @@ -80,6 +81,7 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_mousepad); diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java index 1cce517d..d12e9ff1 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java @@ -46,6 +46,7 @@ import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.List; @@ -314,6 +315,7 @@ public class MprisActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_mpris); final String targetPlayerName = getIntent().getStringExtra("player"); diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java index 20bc4baa..0497d148 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java @@ -39,6 +39,7 @@ import android.widget.ListView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Helpers.StringsHelper; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.Arrays; @@ -93,6 +94,7 @@ public class NotificationFilterActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_notification_filter); appDatabase = new AppDatabase(NotificationFilterActivity.this, false); diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java index 26d15912..ab233cbb 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java @@ -35,6 +35,7 @@ import org.json.JSONObject; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.UserInterface.List.ListAdapter; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; @@ -115,6 +116,7 @@ public class RunCommandActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_runcommand); deviceId = getIntent().getStringExtra("deviceId"); diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java index 044cb639..953ce824 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java @@ -32,6 +32,7 @@ import android.widget.Toast; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; @@ -44,6 +45,7 @@ public class SendFileActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeUtil.setUserPreferredTheme(this); mDeviceId = getIntent().getStringExtra("deviceId"); diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java index e366ee46..d9265d4a 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java @@ -38,6 +38,7 @@ import org.kde.kdeconnect.Device; import org.kde.kdeconnect.UserInterface.List.EntryItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.SectionItem; +import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; @@ -145,8 +146,11 @@ public class ShareActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.devices_list); + ActionBar actionBar = getSupportActionBar(); mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh_list_layout); mSwipeRefreshLayout.setOnRefreshListener( diff --git a/src/org/kde/kdeconnect/UserInterface/AppCompatPreferenceActivity.java b/src/org/kde/kdeconnect/UserInterface/AppCompatPreferenceActivity.java index 0cd4cc07..43796bf4 100644 --- a/src/org/kde/kdeconnect/UserInterface/AppCompatPreferenceActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/AppCompatPreferenceActivity.java @@ -45,6 +45,8 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity { protected void onCreate(Bundle savedInstanceState) { getDelegate().installViewFactory(); getDelegate().onCreate(savedInstanceState); + // The superclass's onCreate() method calls setContentView, so this ThemeUtil call must be before that + ThemeUtil.setUserPreferredTheme(this); super.onCreate(savedInstanceState); } diff --git a/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java b/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java index 49221678..27c2e3a5 100644 --- a/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java @@ -57,6 +57,7 @@ public class CustomDevicesActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initializeDeviceList(this); + ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.custom_ip_list); list = (ListView) findViewById(android.R.id.list); diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index d2b47af5..f66de81d 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -334,6 +334,7 @@ public class DeviceFragment extends Fragment { boolean onData = NetworkHelper.isOnMobileNetwork(getContext()); rootView.findViewById(R.id.pairing_buttons).setVisibility(paired ? View.GONE : View.VISIBLE); + rootView.findViewById(R.id.error_message_container).setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); rootView.findViewById(R.id.not_reachable_message).setVisibility((paired && !reachable && !onData) ? View.VISIBLE : View.GONE); rootView.findViewById(R.id.on_data_message).setVisibility((paired && !reachable && onData) ? View.VISIBLE : View.GONE); diff --git a/src/org/kde/kdeconnect/UserInterface/MainActivity.java b/src/org/kde/kdeconnect/UserInterface/MainActivity.java index 16031cc9..6f6b2538 100644 --- a/src/org/kde/kdeconnect/UserInterface/MainActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MainActivity.java @@ -7,8 +7,11 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -16,12 +19,15 @@ import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.SwitchCompat; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; @@ -55,6 +61,10 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // We need to set this theme before the call to 'setContentView' below + ThemeUtil.setUserPreferredTheme(this); + setContentView(R.layout.activity_main); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mNavigationView = (NavigationView) findViewById(R.id.navigation_drawer); @@ -91,6 +101,10 @@ public class MainActivity extends AppCompatActivity { mDrawerHeader.findViewById(R.id.kdeconnect_label).setOnClickListener(renameListener); mDrawerHeader.findViewById(R.id.device_name).setOnClickListener(renameListener); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + addDarkModeSwitch((ViewGroup) mDrawerHeader); + } + mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { @@ -132,6 +146,36 @@ public class MainActivity extends AppCompatActivity { onDeviceSelected(savedDevice); } + /** + * Adds a {@link SwitchCompat} to the bottom of the navigation header for + * toggling dark mode on and off. Call from {@link #onCreate(Bundle)}. + *

+ * Only supports android ICS and higher because {@link SwitchCompat} + * requires that. + *

+ * + * @param drawerHeader the layout which should contain the switch + */ + @RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void addDarkModeSwitch(ViewGroup drawerHeader) { + getLayoutInflater().inflate(R.layout.nav_dark_mode_switch, drawerHeader); + + SwitchCompat darkThemeSwitch = (SwitchCompat) drawerHeader.findViewById(R.id.dark_theme); + darkThemeSwitch.setChecked(ThemeUtil.shouldUseDarkTheme(this)); + darkThemeSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @RequiresApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onCheckedChanged(CompoundButton darkThemeSwitch, boolean isChecked) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + boolean isDarkAlready = prefs.getBoolean("darkTheme", false); + if (isDarkAlready != isChecked) { + prefs.edit().putBoolean("darkTheme", isChecked).apply(); + MainActivity.this.recreate(); + } + } + }); + } + //like onNewDeviceSelected but assumes that the new device is simply requesting to be paired //and can't be null private void onNewDeviceSelected(String deviceId, String pairStatus) { diff --git a/src/org/kde/kdeconnect/UserInterface/ThemeUtil.java b/src/org/kde/kdeconnect/UserInterface/ThemeUtil.java new file mode 100644 index 00000000..e3e40335 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/ThemeUtil.java @@ -0,0 +1,46 @@ +package org.kde.kdeconnect.UserInterface; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.kde.kdeconnect_tp.R; + +/** + * Utilities for working with android {@link android.content.res.Resources.Theme Themes}. + */ +public class ThemeUtil { + + /** + * This method should be called from the {@code activity}'s onCreate method, before + * any calls to {@link Activity#setContentView} or + * {@link android.preference.PreferenceActivity#setPreferenceScreen}. + * + * @param activity any Activity on screen + */ + public static void setUserPreferredTheme(Activity activity) { + boolean useDarkTheme = shouldUseDarkTheme(activity); + + // Only MainActivity sets its own Toolbar as the ActionBar. + boolean usesOwnActionBar = activity instanceof MainActivity; + + if (useDarkTheme) { + activity.setTheme(usesOwnActionBar ? R.style.KdeConnectTheme_Dark_NoActionBar : R.style.KdeConnectTheme_Dark); + } else { + activity.setTheme(usesOwnActionBar ? R.style.KdeConnectTheme_NoActionBar : R.style.KdeConnectTheme); + } + } + + /** + * Checks {@link SharedPreferences} to figure out whether we should use the light + * theme or the dark theme. The app defaults to light theme. + * + * @param context any active context (Activity, Service, Application, etc.) + * @return true if the dark theme should be active, false otherwise + */ + public static boolean shouldUseDarkTheme(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean("darkTheme", false); + } +}