/* * Copyright 2018-2020 Pranav Pandey * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pranavpandey.android.dynamic.support.theme; import android.annotation.TargetApi; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Color; import android.os.Build; import android.os.PowerManager; import android.view.LayoutInflater; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.StyleRes; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.view.LayoutInflaterCompat; import androidx.work.ExistingWorkPolicy; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import com.google.gson.Gson; import com.pranavpandey.android.dynamic.preferences.DynamicPreferences; import com.pranavpandey.android.dynamic.support.R; import com.pranavpandey.android.dynamic.support.listener.DynamicListener; import com.pranavpandey.android.dynamic.support.listener.DynamicResolver; import com.pranavpandey.android.dynamic.support.model.DynamicAppTheme; import com.pranavpandey.android.dynamic.support.model.DynamicRemoteTheme; import com.pranavpandey.android.dynamic.support.model.DynamicWidgetTheme; import com.pranavpandey.android.dynamic.support.permission.DynamicPermissions; import com.pranavpandey.android.dynamic.support.theme.work.DynamicThemeWork; import com.pranavpandey.android.dynamic.support.utils.DynamicResourceUtils; import com.pranavpandey.android.dynamic.support.widget.WidgetDefaults; import com.pranavpandey.android.dynamic.theme.Theme; import com.pranavpandey.android.dynamic.utils.DynamicColorUtils; import com.pranavpandey.android.dynamic.utils.DynamicSdkUtils; import com.pranavpandey.android.dynamic.utils.DynamicUnitUtils; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; /** * Helper class to manage theme for the whole application and its activities. * <p>It must be initialized before using any activity or widget as they are * heavily dependent on this class to generate colors dynamically. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class DynamicTheme implements DynamicListener, DynamicResolver { /** * Dynamic theme shared preferences. */ public static final String ADS_PREF_THEME = "ads_dynamic_theme"; /** * Key for the theme preference. */ public static final String ADS_PREF_THEME_KEY = "ads_theme_"; /** * Normal delay in milliseconds for updating the views. */ public static final long DELAY_NORMAL = 250; /** * Theme change delay in milliseconds which will be useful in some situations like changing * the app theme, updating the widgets, etc. */ public static final long DELAY_THEME_CHANGE = 150; /** * Default shift amount to generate the darker color. */ public static final float COLOR_SHIFT_DARK_DEFAULT = 0.863f; /** * Default primary color used by this theme if no color is supplied. */ private static final @ColorInt int COLOR_PRIMARY_DEFAULT = Color.parseColor("#3F51B5"); /** * Default dark primary color used by this theme if no color is supplied. */ private static final @ColorInt int COLOR_PRIMARY_DARK_DEFAULT = Color.parseColor("#303F9F"); /** * Default accent color used by this theme if no color is supplied. */ private static final @ColorInt int COLOR_ACCENT_DEFAULT = Color.parseColor("#E91E63"); /** * Default font scale for the theme. */ public static final int FONT_SCALE_DEFAULT = 100; /** * Default corner size for the theme. */ private static final int CORNER_SIZE_DEFAULT = DynamicUnitUtils.convertDpToPixels(2); /** * {@code true} if power save mode is enabled. */ private boolean mPowerSaveMode; /** * Default theme used by the application. */ private DynamicAppTheme mDefaultApplicationTheme; /** * Default theme used by the local context. */ private DynamicAppTheme mDefaultLocalTheme; /** * Theme used by the application. */ private DynamicAppTheme mApplicationTheme; /** * Theme used by the local context. */ private DynamicAppTheme mLocalTheme; /** * Theme used by the remote elements. */ private DynamicRemoteTheme mRemoteTheme; /** * Singleton instance of {@link DynamicTheme}. */ private static DynamicTheme sInstance; /** * Application context used by this theme instance. */ private Context mContext; /** * Local activity context used by this theme instance. */ private Context mLocalContext; /** * Broadcast receiver to listen various events. */ private BroadcastReceiver mBroadcastReceiver; /** * Power manager to perform battery and screen related events. */ private PowerManager mPowerManager; /** * Collection of dynamic listeners to send them event callback. */ private List<DynamicListener> mDynamicListeners; /** * Resolver used by the dynamic theme. */ private DynamicResolver mDynamicResolver; /** * Making default constructor private so that it cannot be initialized without a context. * <p>Use {@link #initializeInstance(Context, DynamicResolver)} instead. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) protected DynamicTheme() { } /** * Constructor to initialize an object of this class. * * @param context The application context to be attached with the dynamic theme. * @param dynamicResolver The resolver for the dynamic theme. * <p>Pass {@code null} to use the default implementation. */ private DynamicTheme(@NonNull Context context, @Nullable DynamicResolver dynamicResolver) { DynamicPermissions.initializeInstance(context); this.mContext = context; this.mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); this.mDynamicListeners = new ArrayList<>(); this.mDynamicResolver = dynamicResolver != null ? dynamicResolver : this; this.mDefaultApplicationTheme = new DynamicAppTheme(COLOR_PRIMARY_DEFAULT, COLOR_PRIMARY_DARK_DEFAULT, COLOR_ACCENT_DEFAULT, FONT_SCALE_DEFAULT, CORNER_SIZE_DEFAULT, Theme.BackgroundAware.ENABLE); this.mApplicationTheme = new DynamicAppTheme(); this.mRemoteTheme = new DynamicRemoteTheme(); this.mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent != null && intent.getAction() != null) { if (intent.getAction().equals( PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) { mPowerSaveMode = mPowerManager.isPowerSaveMode(); onPowerSaveModeChanged(mPowerSaveMode); } else { setDynamicThemeWork(!WorkManager.getInstance(context) .getWorkInfosForUniqueWork(DynamicThemeWork.TAG).isDone()); onAutoThemeChanged(); } } } }; IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_TIME_CHANGED); intentFilter.addAction(Intent.ACTION_DATE_CHANGED); intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); if (DynamicSdkUtils.is21()) { intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); this.mPowerSaveMode = mPowerManager.isPowerSaveMode(); } else { this.mPowerSaveMode = false; } mContext.registerReceiver(mBroadcastReceiver, intentFilter); } /** * Sets the dynamic theme work to schedule auto theme event according to the time. * * @param enqueue {@code true} to enqueue the dynamic theme work. */ public void setDynamicThemeWork(boolean enqueue) { if (enqueue) { long delay; Date date = new Date(); if (isNight()) { Calendar calendar = Calendar.getInstance(); calendar.setTime(getDynamicResolver().getNightTimeEnd()); if (date.after(calendar.getTime())) { calendar.add(Calendar.DAY_OF_MONTH, 1); } delay = calendar.getTimeInMillis() - date.getTime(); } else { delay = getDynamicResolver().getNightTimeStart().getTime() - date.getTime(); } WorkManager.getInstance(mContext).enqueueUniqueWork( DynamicThemeWork.TAG, ExistingWorkPolicy.REPLACE, new OneTimeWorkRequest.Builder(DynamicThemeWork.class) .setInitialDelay(delay, TimeUnit.MILLISECONDS) .build()); } else { WorkManager.getInstance(mContext).cancelUniqueWork(DynamicThemeWork.TAG); } } /** * Attach a local context to this theme. * <p>It can be an activity in case different themes are required for different activities. * * @param localContext The context to be attached with this theme. * @param layoutInflater The layout inflater factory for the local context. * <p>{@code null} to use no custom layout inflater. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public DynamicTheme attach(@NonNull Context localContext, @Nullable LayoutInflater.Factory2 layoutInflater) { this.mLocalContext = localContext; this.mDefaultLocalTheme = new DynamicAppTheme(COLOR_PRIMARY_DEFAULT, COLOR_PRIMARY_DARK_DEFAULT, COLOR_ACCENT_DEFAULT, FONT_SCALE_DEFAULT, CORNER_SIZE_DEFAULT, Theme.BackgroundAware.ENABLE); this.mLocalTheme = new DynamicAppTheme(); if (localContext instanceof Activity && layoutInflater != null && ((Activity) localContext).getLayoutInflater().getFactory2() == null) { LayoutInflaterCompat.setFactory2(((Activity) localContext) .getLayoutInflater(), layoutInflater); } return this; } /** * Attach a local context to this theme. * <p>It can be an activity in case different themes are required for different activities. * * @param localContext The context to be attached with this theme. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public DynamicTheme attach(@NonNull Context localContext) { return attach(localContext, new DynamicLayoutInflater()); } /** * Initialize theme when application starts. * <p>Must be initialized once. * * @param context The context to retrieve resources. * @param dynamicResolver The resolver for the dynamic theme. * <p>Pass {@code null} to use the default implementation. */ public static synchronized void initializeInstance(@Nullable Context context, @Nullable DynamicResolver dynamicResolver) { if (context == null) { throw new NullPointerException("Context should not be null"); } if (sInstance == null) { sInstance = new DynamicTheme(context, dynamicResolver); } } /** * Get instance to access public methods. * <p>Must be called before accessing methods. * * @return The singleton instance of this class. */ public static synchronized DynamicTheme getInstance() { if (sInstance == null) { throw new IllegalStateException(DynamicTheme.class.getSimpleName() + " is not initialized, call initializeInstance(..) method first."); } return sInstance; } /** * Initialize colors from the supplied theme resource. * * @param theme The theme resource to initialize colors. * @param dynamicTheme The dynamic app theme to initialize colors. * @param initializeRemoteColors {@code true} to initialize remote colors also. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public @NonNull DynamicTheme setThemeRes(@StyleRes int theme, @Nullable DynamicAppTheme dynamicTheme, boolean initializeRemoteColors) { if (theme != DynamicResourceUtils.ADS_DEFAULT_RESOURCE_ID) { mContext.getTheme().applyStyle(theme, true); mDefaultApplicationTheme.setThemeRes(theme) .setBackgroundColor(DynamicResourceUtils.resolveColor( mContext, theme, android.R.attr.windowBackground, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setSurfaceColor(DynamicResourceUtils.resolveColor( mContext, theme, R.attr.colorSurface, DynamicAppTheme.AUTO)) .setTintSurfaceColor(DynamicResourceUtils.resolveColor( mContext, theme, R.attr.colorOnSurface, DynamicAppTheme.AUTO)) .setPrimaryColor(DynamicResourceUtils.resolveColor( mContext, theme, R.attr.colorPrimary, mDefaultApplicationTheme.getPrimaryColor())) .setPrimaryColorDark(DynamicResourceUtils.resolveColor( mContext, theme, R.attr.colorPrimaryDark, mDefaultApplicationTheme.getPrimaryColorDark())) .setAccentColor(DynamicResourceUtils.resolveColor( mContext, theme, R.attr.colorAccent, mDefaultApplicationTheme.getAccentColor())) .setAccentColorDark(mApplicationTheme.getAccentColor()) .setTextPrimaryColor(DynamicResourceUtils.resolveColor( mContext, theme, android.R.attr.textColorPrimary, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setTextSecondaryColor(DynamicResourceUtils.resolveColor( mContext, theme, android.R.attr.textColorSecondary, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setTextPrimaryColorInverse(DynamicResourceUtils.resolveColor( mContext, theme, android.R.attr.textColorPrimaryInverse, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setTextSecondaryColorInverse(DynamicResourceUtils.resolveColor( mContext, theme, android.R.attr.textColorSecondaryInverse, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setFontScale(DynamicResourceUtils.resolveInteger( mContext, theme, R.attr.ads_fontScale, mDefaultApplicationTheme.getFontScale())) .setCornerRadius(DynamicResourceUtils.resolveDimensionPixelOffSet( mContext, theme, R.attr.ads_cornerRadius, mDefaultApplicationTheme.getCornerRadius())) .setBackgroundAware(DynamicResourceUtils.resolveInteger( mContext, theme, R.attr.ads_backgroundAware, mDefaultApplicationTheme.getBackgroundAware())); mApplicationTheme = new DynamicAppTheme(dynamicTheme == null ? mDefaultApplicationTheme : dynamicTheme); if (initializeRemoteColors) { initializeRemoteColors(); } } return this; } /** * Initialize colors from the supplied dynamic app theme. * * @param theme The theme resource to initialize colors. * @param dynamicTheme The dynamic app theme to initialize colors. * @param initializeRemoteColors {@code true} to initialize remote colors also. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public @NonNull DynamicTheme setTheme(@StyleRes int theme, @Nullable DynamicAppTheme dynamicTheme, boolean initializeRemoteColors) { if (dynamicTheme != null) { if (dynamicTheme.getThemeRes() == DynamicResourceUtils.ADS_DEFAULT_RESOURCE_ID) { throw new IllegalStateException("Dynamic app theme style resource " + "id is not found for the application theme."); } setThemeRes(dynamicTheme.getThemeRes(), dynamicTheme, false); } else { setThemeRes(theme, null, false); } return this; } /** * Initialize colors from the supplied local theme resource. * * @param localTheme The local theme resource to initialize colors. * @param dynamicLocalTheme The local dynamic app theme to initialize colors. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public @NonNull DynamicTheme setLocalThemeRes(@StyleRes int localTheme, @Nullable DynamicAppTheme dynamicLocalTheme) { if (mLocalContext == null) { throw new IllegalStateException("Not attached to a local context."); } if (localTheme != DynamicResourceUtils.ADS_DEFAULT_RESOURCE_ID) { mLocalContext.getTheme().applyStyle(localTheme, true); mDefaultLocalTheme.setThemeRes(localTheme) .setBackgroundColor(DynamicResourceUtils.resolveColor( mContext, localTheme, android.R.attr.windowBackground, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setSurfaceColor(DynamicResourceUtils.resolveColor( mContext, localTheme, R.attr.colorSurface, DynamicAppTheme.AUTO)) .setTintSurfaceColor(DynamicResourceUtils.resolveColor( mContext, localTheme, R.attr.colorOnSurface, DynamicAppTheme.AUTO)) .setPrimaryColor(DynamicResourceUtils.resolveColor( mContext, localTheme, R.attr.colorPrimary, mDefaultLocalTheme.getPrimaryColor())) .setPrimaryColorDark(DynamicResourceUtils.resolveColor( mContext, localTheme, R.attr.colorPrimaryDark, mDefaultLocalTheme.getPrimaryColorDark())) .setAccentColor(DynamicResourceUtils.resolveColor( mContext, localTheme, R.attr.colorAccent, mDefaultLocalTheme.getAccentColor())) .setAccentColorDark(mLocalTheme.getAccentColor()) .setTextPrimaryColor(DynamicResourceUtils.resolveColor( mContext, localTheme, android.R.attr.textColorPrimary, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setTextSecondaryColor(DynamicResourceUtils.resolveColor( mContext, localTheme, android.R.attr.textColorSecondary, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setTextPrimaryColorInverse(DynamicResourceUtils.resolveColor( mContext, localTheme, android.R.attr.textColorPrimaryInverse, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setTextSecondaryColorInverse(DynamicResourceUtils.resolveColor( mContext, localTheme, android.R.attr.textColorSecondaryInverse, DynamicResourceUtils.ADS_DEFAULT_RESOURCE_VALUE)) .setFontScale(DynamicResourceUtils.resolveInteger( mContext, localTheme, R.attr.ads_fontScale, mDefaultLocalTheme.getFontScale())) .setCornerRadius(DynamicResourceUtils.resolveDimensionPixelOffSet( mContext, localTheme, R.attr.ads_cornerRadius, mDefaultLocalTheme.getCornerRadius())) .setBackgroundAware(DynamicResourceUtils.resolveInteger( mContext, localTheme, R.attr.ads_backgroundAware, mDefaultLocalTheme.getBackgroundAware())); mLocalTheme = new DynamicAppTheme(dynamicLocalTheme == null ? mDefaultLocalTheme : dynamicLocalTheme); addDynamicListener(mLocalContext); } return this; } /** * Initialize colors from the supplied local dynamic app theme. * * @param localTheme The local theme resource to initialize colors. * @param dynamicLocalTheme The local dynamic app theme to initialize colors. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public @NonNull DynamicTheme setLocalTheme(@StyleRes int localTheme, @Nullable DynamicAppTheme dynamicLocalTheme) { if (dynamicLocalTheme != null) { if (dynamicLocalTheme.getThemeRes() == DynamicResourceUtils.ADS_DEFAULT_RESOURCE_ID) { throw new IllegalStateException("Dynamic app theme style resource " + "id is not found for the application theme."); } setLocalThemeRes(dynamicLocalTheme.getThemeRes(), dynamicLocalTheme); } else { setLocalThemeRes(localTheme, null); } return this; } /** * Initialize remote colors according to the base colors. * <p>They can be set individually by calling the appropriate methods. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public @NonNull DynamicTheme initializeRemoteColors() { mRemoteTheme = (DynamicRemoteTheme) new DynamicRemoteTheme(mApplicationTheme) .setBackgroundColor(ContextCompat.getColor(getResolvedContext(), !DynamicSdkUtils.is21() ? R.color.notification_background : R.color.notification_background_light)); return this; } /** * Resolve background aware according to the constant value. * * @param backgroundAware The background aware to be resolved. * * @return The resolved background aware value. * <p>Either {@link Theme.BackgroundAware#ENABLE} * or {@link Theme.BackgroundAware#DISABLE}. * * @see Theme.BackgroundAware */ public @Theme.BackgroundAware int resolveBackgroundAware( @Theme.BackgroundAware int backgroundAware) { if (backgroundAware == Theme.BackgroundAware.AUTO) { return DynamicTheme.getInstance().get().getBackgroundAware(); } return backgroundAware; } /** * Resolve color according to the color type. * * @param colorType The color type to be resolved. * * @return The resolved color value. * * @see Theme.ColorType */ public @ColorInt int resolveColorType(@Theme.ColorType int colorType) { switch (colorType) { default: return WidgetDefaults.ADS_COLOR_UNKNOWN; case Theme.ColorType.PRIMARY: return get().getPrimaryColor(); case Theme.ColorType.PRIMARY_DARK: return get().getPrimaryColorDark(); case Theme.ColorType.ACCENT: return get().getAccentColor(); case Theme.ColorType.ACCENT_DARK: return get().getAccentColorDark(); case Theme.ColorType.TINT_PRIMARY: return get().getTintPrimaryColor(); case Theme.ColorType.TINT_PRIMARY_DARK: return get().getTintPrimaryColorDark(); case Theme.ColorType.TINT_ACCENT: return get().getTintAccentColor(); case Theme.ColorType.TINT_ACCENT_DARK: return get().getTintAccentColorDark(); case Theme.ColorType.BACKGROUND: return get().getBackgroundColor(); case Theme.ColorType.TINT_BACKGROUND: return get().getTintBackgroundColor(); case Theme.ColorType.TEXT_PRIMARY: return get().getTextPrimaryColor(); case Theme.ColorType.TEXT_SECONDARY: return get().getTextSecondaryColor(); case Theme.ColorType.TEXT_PRIMARY_INVERSE: return get().getTextPrimaryColorInverse(); case Theme.ColorType.TEXT_SECONDARY_INVERSE: return get().getTextSecondaryColorInverse(); case Theme.ColorType.SURFACE: return get().getSurfaceColor(); case Theme.ColorType.TINT_SURFACE: return get().getTintSurfaceColor(); } } /** * Get the application context. * * @return The application context. */ public @NonNull Context getContext() { return mContext; } /** * Set the application context for this theme. * * @param context The application context to be set. * * @return The {@link DynamicTheme} object to allow for chaining of calls to set methods. */ public @NonNull DynamicTheme setContext(@NonNull Context context) { this.mContext = context; return this; } /** * Get the local context. * * @return The local context. */ public @Nullable Context getLocalContext() { return mLocalContext; } /** * Get the power manager used by the application. * * @return The power manager used by the application. */ public PowerManager getPowerManager() { return mPowerManager; } /** * Get the theme used by the application. * * @return The theme used by the application. */ public DynamicAppTheme getApplication() { return mApplicationTheme; } /** * Get the theme used by the local context. * * @return The theme used by the local context. */ public DynamicAppTheme getLocal() { return mLocalTheme; } /** * @return The theme used by the remote views. */ public DynamicWidgetTheme getRemote() { return mRemoteTheme; } /** * Get the theme according to the current state. * <p>Either application theme or local theme. * * @return The theme according to the current state. */ public DynamicAppTheme get() { return mLocalContext != null ? mLocalTheme : mApplicationTheme; } /** * Get the default theme according to the current state. * <p>Either default application theme or default local theme. * * @return The default theme according to the current state. */ public DynamicAppTheme getDefault() { return mLocalContext != null ? mDefaultLocalTheme : mDefaultApplicationTheme; } /** * Recreate local activity to update all the views with new theme. */ public void recreateLocal() { if (mLocalContext == null) { throw new IllegalStateException("Not attached to a local context"); } if (!(mLocalContext instanceof Activity)) { throw new IllegalStateException("Not an instance of Activity"); } ActivityCompat.recreate(((Activity) mLocalContext)); } /** * Set the initialized instance to {@code null} when app terminates for better theme * results when theme is changed. */ public void onDestroy() { if (sInstance == null) { return; } mContext.unregisterReceiver(mBroadcastReceiver); mContext = null; mLocalContext = null; mBroadcastReceiver = null; mDefaultApplicationTheme = null; mApplicationTheme = null; mDefaultLocalTheme = null; mLocalTheme = null; mRemoteTheme = null; sInstance.mContext = null; sInstance.mApplicationTheme = null; sInstance.mDefaultApplicationTheme = null; sInstance.mDefaultLocalTheme = null; sInstance.mLocalContext = null; sInstance.mLocalTheme = null; sInstance.mRemoteTheme = null; sInstance = null; clearDynamicListeners(); } /** * Set the initialized instance to {@code null} when local destroys for better theme * results when theme is changed. */ public void onLocalDestroy() { if (sInstance == null) { return; } removeDynamicListener(mLocalContext); saveLocalTheme(); mLocalContext = null; mLocalTheme = null; } /** * Generates default theme according to the current settings. * * @return The generated default theme. */ public @NonNull DynamicAppTheme generateDefaultTheme() { return new DynamicAppTheme().setBackgroundColor( getDefault().getBackgroundColor(), false); } /** * Generates surface color according to the supplied color. * * @param color The color to generate the surface color. * * @return The generated surface color. */ public @ColorInt int generateSurfaceColor(@ColorInt int color) { return DynamicColorUtils.isColorDark(color) ? DynamicColorUtils.getLighterColor(color, WidgetDefaults.ADS_FACTOR_SURFACE) : DynamicColorUtils.getLighterColor(color, WidgetDefaults.ADS_FACTOR_SURFACE * 2); } /** * Generates dark variant of the supplied color. * * @param color The color to generate the dark variant. * * @return The generated dark variant of the color. */ public @ColorInt int generateDarkColor(@ColorInt int color) { return DynamicColorUtils.shiftColor(color, DynamicTheme.COLOR_SHIFT_DARK_DEFAULT); } /** * Returns the currently used context. * <p>Generally, either application or an activity. * * @return The currently used context. */ private Context getResolvedContext() { return mLocalContext != null ? mLocalContext : mContext; } /** * Returns the default contrast with color to tint the background aware views accordingly. * * @return The default contrast with color. */ public @ColorInt int getDefaultContrastWith() { return get().getBackgroundColor(); } /** * Returns the resolver used by the dynamic theme. * * @return The resolver used by the dynamic theme. */ public @NonNull DynamicResolver getDynamicResolver() { return mDynamicResolver != null ? mDynamicResolver : this; } /** * Sets the resolver used by the dynamic theme. * * @param dynamicResolver The resolver to be set. */ public void setDynamicResolver(@Nullable DynamicResolver dynamicResolver) { this.mDynamicResolver = dynamicResolver; } /** * Add a dynamic listener to receive the various callbacks. * * @param dynamicListener The dynamic listener to be added. * * @see DynamicListener */ public void addDynamicListener(@Nullable Context dynamicListener) { if (dynamicListener instanceof DynamicListener && !mDynamicListeners.contains(dynamicListener)) { mDynamicListeners.add((DynamicListener) dynamicListener); } } /** * Remove a dynamic listener. * * @param dynamicListener The dynamic listener to be removed. * * @see DynamicListener */ public void removeDynamicListener(@Nullable Context dynamicListener) { if (dynamicListener instanceof DynamicListener) { mDynamicListeners.remove(dynamicListener); } } /** * Checks whether a dynamic listener is already registered. * * @param dynamicListener The dynamic listener to be checked. * * @see DynamicListener */ public boolean isDynamicListener(@Nullable Context dynamicListener) { if (!(dynamicListener instanceof DynamicListener)) { return false; } return mDynamicListeners.contains(dynamicListener); } /** * Remove all the dynamic listeners. */ public void clearDynamicListeners() { if (!mDynamicListeners.isEmpty()) { mDynamicListeners.clear(); } } @Override public void onNavigationBarThemeChanged() { for (DynamicListener dynamicListener : mDynamicListeners) { dynamicListener.onNavigationBarThemeChanged(); } } @Override public void onDynamicChanged(boolean context, boolean recreate) { for (DynamicListener dynamicListener : mDynamicListeners) { dynamicListener.onDynamicChanged(context, recreate); } } @Override public void onDynamicConfigurationChanged(boolean locale, boolean fontScale, boolean orientation, boolean uiMode, boolean density) { for (DynamicListener dynamicListener : mDynamicListeners) { dynamicListener.onDynamicConfigurationChanged(locale, fontScale, orientation, uiMode, density); } } @Override public void onAutoThemeChanged() { for (DynamicListener dynamicListener : mDynamicListeners) { dynamicListener.onAutoThemeChanged(); } } @Override public void onPowerSaveModeChanged(boolean powerSaveMode) { for (DynamicListener dynamicListener : mDynamicListeners) { dynamicListener.onPowerSaveModeChanged(powerSaveMode); } } @Override public @NonNull String toString() { StringBuilder theme = new StringBuilder(); if (mApplicationTheme != null) { theme.append(mApplicationTheme.toString()); } if (mLocalTheme != null) { theme.append(mLocalTheme.toString()); } if (mRemoteTheme != null) { theme.append(mRemoteTheme.toString()); } return theme.toString(); } /** * Save the local context theme in shared preferences. */ public void saveLocalTheme() { if (mLocalContext != null) { DynamicPreferences.getInstance().save(ADS_PREF_THEME, ADS_PREF_THEME_KEY + mLocalContext.getClass().getName(), toString()); } } /** * Returns the supplied context theme from the shared preferences. * * @param context The context to retrieve the theme. * * @return The supplied context theme from shared preferences. */ public @Nullable String getLocalTheme(@NonNull Context context) { return DynamicPreferences.getInstance().load(ADS_PREF_THEME, ADS_PREF_THEME_KEY + context.getClass().getName(), null); } /** * Delete the supplied context theme from shared preferences. * * @param context The context to delete the theme. */ public void deleteLocalTheme(@NonNull Context context) { try { DynamicPreferences.getInstance().delete(ADS_PREF_THEME, ADS_PREF_THEME_KEY + context.getClass().getName()); } catch (Exception ignored) { } } /** * Returns the dynamic app theme from the JSON string. * * @param theme The dynamic app theme JSON string to be converted. * * @return The dynamic app theme from the JSON string. */ public @Nullable DynamicAppTheme getTheme(@Nullable String theme) { return new Gson().fromJson(theme, DynamicAppTheme.class); } @Override public boolean isSystemNightMode() { return (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; } @Override public int resolveSystemColor(boolean isNight) { if (DynamicSdkUtils.is28()) { return isNight ? DynamicRemoteTheme.SYSTEM_COLOR_NIGHT : DynamicRemoteTheme.SYSTEM_COLOR; } else if (DynamicSdkUtils.is21()) { return DynamicRemoteTheme.SYSTEM_COLOR; } else { return DynamicRemoteTheme.SYSTEM_COLOR_NIGHT; } } @Override public boolean isNight() { Date date = new Date(); return date.getTime() >= getNightTimeStart().getTime() || date.getTime() < getNightTimeEnd().getTime(); } @Override public boolean isNight(@Theme int theme) { return theme == Theme.NIGHT || (theme == Theme.AUTO && isNight()); } @Override public boolean isNight(@Theme.ToString String theme) { return getDynamicResolver().isNight(Integer.parseInt(theme)); } @Override public @NonNull Date getNightTimeStart() { Date date = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, 19); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); return calendar.getTime(); } @Override public @NonNull Date getNightTimeEnd() { Date date = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, 6); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); return calendar.getTime(); } @Override public boolean resolveNightTheme(@Theme int appTheme, @Theme.Night int implementation) { if (appTheme == Theme.AUTO) { switch (implementation) { default: case Theme.Night.SYSTEM: return getDynamicResolver().isSystemNightMode(); case Theme.Night.CUSTOM: return false; case Theme.Night.AUTO: return getDynamicResolver().isNight(appTheme); case Theme.Night.BATTERY: return mPowerSaveMode; } } return appTheme == Theme.NIGHT; } @Override public boolean resolveNightTheme(@Theme.ToString String appTheme, @Theme.Night.ToString String implementation) { return getDynamicResolver().resolveNightTheme( Integer.parseInt(appTheme), Integer.parseInt(implementation)); } }