add settings for dynamic colors and dark theme

This commit is contained in:
Daniel Gultsch 2023-02-20 20:30:26 +01:00
parent b80fe9802a
commit 7567dcff5e
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
17 changed files with 280 additions and 25 deletions

View file

@ -1,8 +1,12 @@
package im.conversations.android; package im.conversations.android;
import android.app.Application; import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
import com.google.android.material.color.DynamicColors; import com.google.android.material.color.DynamicColors;
import com.google.android.material.color.DynamicColorsOptions;
import im.conversations.android.dns.Resolver; import im.conversations.android.dns.Resolver;
import im.conversations.android.notification.Channels; import im.conversations.android.notification.Channels;
import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionPool;
@ -30,8 +34,53 @@ public class Conversations extends Application {
channels.initialize(); channels.initialize();
Resolver.init(this); Resolver.init(this);
ConnectionPool.getInstance(this).reconfigure(); ConnectionPool.getInstance(this).reconfigure();
AppCompatDelegate.setDefaultNightMode( applyThemeSettings();
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); // For night mode theme }
DynamicColors.applyToActivitiesIfAvailable(this);
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;
}
} }
} }

View file

@ -13,11 +13,20 @@ public final class Activities {
public static void setStatusAndNavigationBarColors( public static void setStatusAndNavigationBarColors(
final AppCompatActivity activity, final View view) { 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 isLightMode = isLightMode(activity);
final var window = activity.getWindow(); final var window = activity.getWindow();
final var flags = view.getSystemUiVisibility(); final var flags = view.getSystemUiVisibility();
// an elevation of 4 matches the MaterialToolbar elevation // 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.setNavigationBarColor(SurfaceColors.SURFACE_1.getColor(activity)); window.setNavigationBarColor(SurfaceColors.SURFACE_1.getColor(activity));
if (isLightMode) { if (isLightMode) {

View file

@ -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;
}
}

View file

@ -1,14 +1,13 @@
package im.conversations.android.ui.activity; package im.conversations.android.ui.activity;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import im.conversations.android.R; import im.conversations.android.R;
import im.conversations.android.databinding.ActivityMainBinding; import im.conversations.android.databinding.ActivityMainBinding;
import im.conversations.android.service.ForegroundService; import im.conversations.android.service.ForegroundService;
import im.conversations.android.ui.Activities; import im.conversations.android.ui.Activities;
public class MainActivity extends AppCompatActivity { public class MainActivity extends BaseActivity {
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {

View file

@ -1,7 +1,6 @@
package im.conversations.android.ui.activity; package im.conversations.android.ui.activity;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import im.conversations.android.R; import im.conversations.android.R;
import im.conversations.android.databinding.ActivitySettingsBinding; 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.Activities;
import im.conversations.android.ui.fragment.settings.MainSettingsFragment; import im.conversations.android.ui.fragment.settings.MainSettingsFragment;
public class SettingsActivity extends AppCompatActivity { public class SettingsActivity extends BaseActivity {
private ActivitySettingsBinding binding;
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ForegroundService.start(this); ForegroundService.start(this);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_settings); final ActivitySettingsBinding binding =
Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); DataBindingUtil.setContentView(this, R.layout.activity_settings);
getSupportFragmentManager() setSupportActionBar(binding.materialToolbar);
.beginTransaction() Activities.setStatusAndNavigationBarColors(this, binding.getRoot(), true);
.replace(R.id.fragment_container, new MainSettingsFragment())
.commit(); 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();
}
});
} }
} }

View file

@ -2,7 +2,6 @@ package im.conversations.android.ui.activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController; import androidx.navigation.NavController;
@ -16,7 +15,7 @@ import im.conversations.android.ui.model.SetupViewModel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class SetupActivity extends AppCompatActivity { public class SetupActivity extends BaseActivity {
private static final Logger LOGGER = LoggerFactory.getLogger(SetupActivity.class); private static final Logger LOGGER = LoggerFactory.getLogger(SetupActivity.class);

View file

@ -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()));
}
}

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9,11.24V7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5C7,9.06 7.79,10.43 9,11.24zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11H13v-6C13,6.67 12.33,6 11.5,6S10,6.67 10,7.5v10.74c-3.6,-0.76 -3.54,-0.75 -3.67,-0.75c-0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8l4.94,4.94C9.96,23.83 10.34,24 10.75,24h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2C19.75,16.63 19.37,16.09 18.84,15.87z" />
</vector>

View file

@ -25,13 +25,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:liftOnScroll="false" app:liftOnScroll="false"
app:elevation="0dp"> app:elevation="4dp">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/material_toolbar" android:id="@+id/material_toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:title="@string/title_activity_settings"/> app:title="@string/title_activity_settings"
app:navigationIcon="@drawable/ic_arrow_back_24dp"/>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:fillViewport="true" android:fillViewport="true"

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="theme">automatic</string>
<string-array name="themes" tools:ignore="InconsistentArrays">
<item>@string/pref_theme_automatic</item>
<item>@string/pref_theme_light</item>
<item>@string/pref_theme_dark</item>
</string-array>
<string-array name="themes_values" tools:ignore="InconsistentArrays">
<item>automatic</item>
<item>light</item>
<item>dark</item>
</string-array>
</resources>

View file

@ -1017,10 +1017,14 @@
<string name="accounts">Accounts</string> <string name="accounts">Accounts</string>
<string name="spaces">Spaces</string> <string name="spaces">Spaces</string>
<string name="chats">Chats</string> <string name="chats">Chats</string>
<string name="pref_title_appearance">Appearance</string> <string name="pref_title_interface">Interface</string>
<string name="pref_summary_appearance"><![CDATA[Theme & Colors]]></string> <string name="pref_summary_appearance"><![CDATA[Theme & Colors]]></string>
<string name="pref_title_security">Security</string> <string name="pref_title_security">Security</string>
<string name="pref_summary_security">Encryption, Blind Trust Before Verification, MIM Detection</string> <string name="pref_summary_security">Encryption, Blind Trust Before Verification, MIM Detection</string>
<string name="unified_push_summary">Notification relay for UnifiedPush compatible third party apps</string> <string name="unified_push_summary">Notification relay for UnifiedPush compatible third party apps</string>
<string name="pref_dynamic_colors">Dynamic colors</string>
<string name="pref_dynamic_colors_summary">System colors (Material You)</string>
<string name="pref_light_dark_mode">Light/dark mode</string>
<string name="appearance">Appearance</string>
</resources> </resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="theme">light</string>
<string-array name="themes">
<item>@string/pref_theme_light</item>
<item>@string/pref_theme_dark</item>
</string-array>
<string-array name="themes_values">
<item>light</item>
<item>dark</item>
</string-array>
</resources>

View file

@ -27,6 +27,5 @@
<item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item> <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
<item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item> <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
<item name="colorPrimaryInverse">@color/md_theme_light_inversePrimary</item> <item name="colorPrimaryInverse">@color/md_theme_light_inversePrimary</item>
<item name="android:statusBarColor">?colorSurface</item>
</style> </style>
</resources> </resources>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/appearance">
<SwitchPreferenceCompat
android:icon="@drawable/ic_palette_24dp"
android:key="dynamic_colors"
android:title="@string/pref_dynamic_colors"
android:summary="@string/pref_dynamic_colors_summary"/>
<ListPreference
android:defaultValue="@string/theme"
android:entries="@array/themes"
android:entryValues="@array/themes_values"
android:icon="@drawable/ic_dark_mode_24dp"
android:key="theme"
android:title="@string/pref_light_dark_mode"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -2,10 +2,10 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference <Preference
app:title="@string/pref_title_appearance" app:title="@string/pref_title_interface"
app:summary="@string/pref_summary_appearance" app:summary="@string/pref_summary_appearance"
android:icon="@drawable/ic_palette_24dp" android:icon="@drawable/ic_touch_app_24dp"
app:fragment="im.conversations.android.ui.fragment.MainSettingsFragment"/> app:fragment="im.conversations.android.ui.fragment.settings.InterfaceSettingsFragment"/>
<Preference <Preference
app:title="@string/pref_title_security" app:title="@string/pref_title_security"
app:summary="@string/pref_summary_security" app:summary="@string/pref_summary_security"