diff --git a/app/src/main/java/im/conversations/android/Conversations.java b/app/src/main/java/im/conversations/android/Conversations.java index c7982790b..ab7af6150 100644 --- a/app/src/main/java/im/conversations/android/Conversations.java +++ b/app/src/main/java/im/conversations/android/Conversations.java @@ -1,8 +1,12 @@ package im.conversations.android; import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.PreferenceManager; import com.google.android.material.color.DynamicColors; +import com.google.android.material.color.DynamicColorsOptions; import im.conversations.android.dns.Resolver; import im.conversations.android.notification.Channels; import im.conversations.android.xmpp.ConnectionPool; @@ -30,8 +34,53 @@ public class Conversations extends Application { channels.initialize(); Resolver.init(this); ConnectionPool.getInstance(this).reconfigure(); - AppCompatDelegate.setDefaultNightMode( - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); // For night mode theme - DynamicColors.applyToActivitiesIfAvailable(this); + applyThemeSettings(); + } + + public void applyThemeSettings() { + final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + if (sharedPreferences == null) { + return; + } + applyThemeSettings(sharedPreferences); + } + + private void applyThemeSettings(final SharedPreferences sharedPreferences) { + AppCompatDelegate.setDefaultNightMode(getDesiredNightMode(this, sharedPreferences)); + var dynamicColorsOptions = + new DynamicColorsOptions.Builder() + .setPrecondition((activity, t) -> isDynamicColorsDesired(activity)) + .build(); + DynamicColors.applyToActivitiesIfAvailable(this, dynamicColorsOptions); + } + + public static int getDesiredNightMode(final Context context) { + final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (sharedPreferences == null) { + return AppCompatDelegate.getDefaultNightMode(); + } + return getDesiredNightMode(context, sharedPreferences); + } + + public static boolean isDynamicColorsDesired(final Context context) { + final var preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getBoolean("dynamic_colors", false); + } + + private static int getDesiredNightMode( + final Context context, final SharedPreferences sharedPreferences) { + final String theme = + sharedPreferences.getString("theme", context.getString(R.string.theme)); + return getDesiredNightMode(theme); + } + + public static int getDesiredNightMode(final String theme) { + if ("automatic".equals(theme)) { + return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + } else if ("light".equals(theme)) { + return AppCompatDelegate.MODE_NIGHT_NO; + } else { + return AppCompatDelegate.MODE_NIGHT_YES; + } } } diff --git a/app/src/main/java/im/conversations/android/ui/Activities.java b/app/src/main/java/im/conversations/android/ui/Activities.java index 1cf2df191..fdfb5e732 100644 --- a/app/src/main/java/im/conversations/android/ui/Activities.java +++ b/app/src/main/java/im/conversations/android/ui/Activities.java @@ -13,11 +13,20 @@ public final class Activities { public static void setStatusAndNavigationBarColors( final AppCompatActivity activity, final View view) { + setStatusAndNavigationBarColors(activity, view, false); + } + + public static void setStatusAndNavigationBarColors( + final AppCompatActivity activity, final View view, final boolean raisedStatusBar) { final var isLightMode = isLightMode(activity); final var window = activity.getWindow(); final var flags = view.getSystemUiVisibility(); // an elevation of 4 matches the MaterialToolbar elevation - window.setStatusBarColor(SurfaceColors.SURFACE_0.getColor(activity)); + if (raisedStatusBar) { + window.setStatusBarColor(SurfaceColors.SURFACE_5.getColor(activity)); + } else { + window.setStatusBarColor(SurfaceColors.SURFACE_0.getColor(activity)); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { window.setNavigationBarColor(SurfaceColors.SURFACE_1.getColor(activity)); if (isLightMode) { diff --git a/app/src/main/java/im/conversations/android/ui/activity/BaseActivity.java b/app/src/main/java/im/conversations/android/ui/activity/BaseActivity.java new file mode 100644 index 000000000..4391f8c24 --- /dev/null +++ b/app/src/main/java/im/conversations/android/ui/activity/BaseActivity.java @@ -0,0 +1,49 @@ +package im.conversations.android.ui.activity; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import im.conversations.android.Conversations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class BaseActivity extends AppCompatActivity { + + private static final Logger LOGGER = LoggerFactory.getLogger(BaseActivity.class); + + private Boolean isDynamicColors; + + @Override + public void onStart() { + super.onStart(); + final int desiredNightMode = Conversations.getDesiredNightMode(this); + if (setDesiredNightMode(desiredNightMode)) { + return; + } + final boolean isDynamicColors = Conversations.isDynamicColorsDesired(this); + setDynamicColors(isDynamicColors); + } + + public void setDynamicColors(final boolean isDynamicColors) { + if (this.isDynamicColors == null) { + this.isDynamicColors = isDynamicColors; + } else { + if (this.isDynamicColors != isDynamicColors) { + LOGGER.info( + "Recreating {} because dynamic color setting has changed", + getClass().getSimpleName()); + recreate(); + } + } + } + + public boolean setDesiredNightMode(final int desiredNightMode) { + if (desiredNightMode == AppCompatDelegate.getDefaultNightMode()) { + return false; + } + AppCompatDelegate.setDefaultNightMode(desiredNightMode); + LOGGER.info( + "Recreating {} because desired night mode has changed", getClass().getSimpleName()); + recreate(); + return true; + } +} diff --git a/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java b/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java index 6b7a58b4d..5608bde51 100644 --- a/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java +++ b/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java @@ -1,14 +1,13 @@ package im.conversations.android.ui.activity; import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.DataBindingUtil; import im.conversations.android.R; import im.conversations.android.databinding.ActivityMainBinding; import im.conversations.android.service.ForegroundService; import im.conversations.android.ui.Activities; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends BaseActivity { @Override protected void onCreate(final Bundle savedInstanceState) { diff --git a/app/src/main/java/im/conversations/android/ui/activity/SettingsActivity.java b/app/src/main/java/im/conversations/android/ui/activity/SettingsActivity.java index 34d2e7a56..550725e4b 100644 --- a/app/src/main/java/im/conversations/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/im/conversations/android/ui/activity/SettingsActivity.java @@ -1,7 +1,6 @@ package im.conversations.android.ui.activity; import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.DataBindingUtil; import im.conversations.android.R; import im.conversations.android.databinding.ActivitySettingsBinding; @@ -9,19 +8,32 @@ import im.conversations.android.service.ForegroundService; import im.conversations.android.ui.Activities; import im.conversations.android.ui.fragment.settings.MainSettingsFragment; -public class SettingsActivity extends AppCompatActivity { - - private ActivitySettingsBinding binding; +public class SettingsActivity extends BaseActivity { @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); ForegroundService.start(this); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_settings); - Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.fragment_container, new MainSettingsFragment()) - .commit(); + final ActivitySettingsBinding binding = + DataBindingUtil.setContentView(this, R.layout.activity_settings); + setSupportActionBar(binding.materialToolbar); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot(), true); + + final var fragmentManager = getSupportFragmentManager(); + final var currentFragment = fragmentManager.findFragmentById(R.id.fragment_container); + if (currentFragment == null) { + fragmentManager + .beginTransaction() + .replace(R.id.fragment_container, new MainSettingsFragment()) + .commit(); + } + binding.materialToolbar.setNavigationOnClickListener( + view -> { + if (fragmentManager.getBackStackEntryCount() == 0) { + finish(); + } else { + fragmentManager.popBackStack(); + } + }); } } diff --git a/app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java b/app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java index 2c3fecdd2..8b4bc10b5 100644 --- a/app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java +++ b/app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java @@ -2,7 +2,6 @@ package im.conversations.android.ui.activity; import android.content.Intent; import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.DataBindingUtil; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavController; @@ -16,7 +15,7 @@ import im.conversations.android.ui.model.SetupViewModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SetupActivity extends AppCompatActivity { +public class SetupActivity extends BaseActivity { private static final Logger LOGGER = LoggerFactory.getLogger(SetupActivity.class); diff --git a/app/src/main/java/im/conversations/android/ui/fragment/settings/InterfaceSettingsFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/settings/InterfaceSettingsFragment.java new file mode 100644 index 000000000..623d58906 --- /dev/null +++ b/app/src/main/java/im/conversations/android/ui/fragment/settings/InterfaceSettingsFragment.java @@ -0,0 +1,54 @@ +package im.conversations.android.ui.fragment.settings; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import im.conversations.android.Conversations; +import im.conversations.android.R; +import im.conversations.android.ui.activity.SettingsActivity; + +public class InterfaceSettingsFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.preferences_interface, rootKey); + final var themePreference = findPreference("theme"); + final var dynamicColors = findPreference("dynamic_colors"); + if (themePreference != null) { + themePreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + if (newValue instanceof String) { + final String theme = (String) newValue; + final int desiredNightMode = Conversations.getDesiredNightMode(theme); + requireSettingsActivity().setDesiredNightMode(desiredNightMode); + } + return true; + }); + } + if (dynamicColors != null) { + dynamicColors.setOnPreferenceChangeListener( + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange( + @NonNull Preference preference, Object newValue) { + requireSettingsActivity() + .setDynamicColors(Boolean.TRUE.equals(newValue)); + return true; + } + }); + } + } + + public SettingsActivity requireSettingsActivity() { + final var activity = requireActivity(); + if (activity instanceof SettingsActivity) { + return (SettingsActivity) activity; + } + throw new IllegalStateException( + String.format( + "%s is not %s", + activity.getClass().getName(), SettingsActivity.class.getName())); + } +} diff --git a/app/src/main/res/drawable/ic_arrow_back_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_24dp.xml new file mode 100644 index 000000000..cd06f3098 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_24dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_dark_mode_24dp.xml b/app/src/main/res/drawable/ic_dark_mode_24dp.xml new file mode 100644 index 000000000..e52c5baff --- /dev/null +++ b/app/src/main/res/drawable/ic_dark_mode_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_touch_app_24dp.xml b/app/src/main/res/drawable/ic_touch_app_24dp.xml new file mode 100644 index 000000000..0744c9698 --- /dev/null +++ b/app/src/main/res/drawable/ic_touch_app_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 0ae23458a..c2f463293 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -25,13 +25,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:liftOnScroll="false" - app:elevation="0dp"> + app:elevation="4dp"> + app:title="@string/title_activity_settings" + app:navigationIcon="@drawable/ic_arrow_back_24dp"/> + + + automatic + + @string/pref_theme_automatic + @string/pref_theme_light + @string/pref_theme_dark + + + automatic + light + dark + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57f688ecb..1fb076f4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1017,10 +1017,14 @@ Accounts Spaces Chats - Appearance + Interface Security Encryption, Blind Trust Before Verification, MIM Detection Notification relay for UnifiedPush compatible third party apps + Dynamic colors + System colors (Material You) + Light/dark mode + Appearance diff --git a/app/src/main/res/values/theme-settings.xml b/app/src/main/res/values/theme-settings.xml new file mode 100644 index 000000000..b770b1b26 --- /dev/null +++ b/app/src/main/res/values/theme-settings.xml @@ -0,0 +1,14 @@ + + + + light + + @string/pref_theme_light + @string/pref_theme_dark + + + light + dark + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0b6f615bf..ddf74586f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -27,6 +27,5 @@ @color/md_theme_light_inverseOnSurface @color/md_theme_light_inverseSurface @color/md_theme_light_inversePrimary - ?colorSurface diff --git a/app/src/main/res/xml/preferences_interface.xml b/app/src/main/res/xml/preferences_interface.xml new file mode 100644 index 000000000..b0166d1c0 --- /dev/null +++ b/app/src/main/res/xml/preferences_interface.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_main.xml b/app/src/main/res/xml/preferences_main.xml index feb62cbfb..dab1626ea 100644 --- a/app/src/main/res/xml/preferences_main.xml +++ b/app/src/main/res/xml/preferences_main.xml @@ -2,10 +2,10 @@ + android:icon="@drawable/ic_touch_app_24dp" + app:fragment="im.conversations.android.ui.fragment.settings.InterfaceSettingsFragment"/>