diff --git a/app/build.gradle b/app/build.gradle index f91b766ad..29e877ff2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,6 +86,12 @@ android { } dependencies { + // Room components + implementation "androidx.room:room-runtime:$rootProject.roomVersion" + annotationProcessor "androidx.room:room-compiler:$rootProject.roomVersion" + + // Gson + implementation "com.google.code.gson:gson:$rootProject.gsonVersion" implementation 'androidx.media3:media3-exoplayer:1.7.1' implementation 'androidx.media3:media3-common:1.7.1' implementation 'androidx.preference:preference:1.2.1' diff --git a/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java b/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java index 9626e0bc3..5cbbc11a0 100644 --- a/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java +++ b/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java @@ -16,6 +16,7 @@ import com.best.deskclock.alarms.AlarmNotifications; import com.best.deskclock.alarms.AlarmStateManager; +import com.best.deskclock.holiday.HolidayRepository; import com.best.deskclock.controller.Controller; import com.best.deskclock.data.DataModel; import com.best.deskclock.data.SettingsDAO; @@ -98,6 +99,12 @@ public void onReceive(final Context context, Intent intent) { Controller.getController().updateShortcuts(); } + // Update holiday data on boot or app update + if (Intent.ACTION_BOOT_COMPLETED.equals(action) + || Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) { + HolidayRepository.getInstance(context).updateWorkdayData(); + } + // Update alarm status once receive the status update broadcast if (ACTION_UPDATE_ALARM_STATUS.equals(action)) { long alarmTime = intent.getLongExtra(TIME, 0L); diff --git a/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java b/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java index 81e85e994..9ac402f60 100644 --- a/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java +++ b/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java @@ -5,8 +5,8 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_END_OF_RINGTONE; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_NEVER; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; import android.app.Dialog; import android.content.Context; @@ -185,9 +185,9 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { mEndOfRingtoneCheckbox = view.findViewById(R.id.end_of_ringtone); mEditMinutes.setText(String.valueOf(editMinutes)); - if (editMinutes == ALARM_TIMEOUT_END_OF_RINGTONE) { + if (editMinutes == TIMEOUT_END_OF_RINGTONE) { mEditMinutes.setText(""); - } else if (editMinutes == ALARM_TIMEOUT_NEVER) { + } else if (editMinutes == TIMEOUT_NEVER) { mEditMinutes.setText(String.valueOf(0)); } mEditMinutes.setEnabled(!isEndOfRingtone); @@ -260,7 +260,7 @@ private void setAutoSilenceDuration() { int minutes = 0; if (mEndOfRingtoneCheckbox.isChecked()) { - minutes = ALARM_TIMEOUT_END_OF_RINGTONE; + minutes = TIMEOUT_END_OF_RINGTONE; } else { String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; @@ -269,7 +269,7 @@ private void setAutoSilenceDuration() { } if (minutes == 0) { - minutes = ALARM_TIMEOUT_NEVER; + minutes = TIMEOUT_NEVER; } } diff --git a/app/src/main/java/com/best/deskclock/BaseActivity.java b/app/src/main/java/com/best/deskclock/BaseActivity.java new file mode 100644 index 000000000..593a92723 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/BaseActivity.java @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.settings.PreferencesDefaultValues.AMOLED_DARK_MODE; +import static com.best.deskclock.settings.PreferencesDefaultValues.BLACK_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.BLUE_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.BLUE_GRAY_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.BROWN_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.DARK_THEME; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEBUG_LANGUAGE_CODE; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_DARK_MODE; +import static com.best.deskclock.settings.PreferencesDefaultValues.GREEN_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.INDIGO_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.LIGHT_THEME; +import static com.best.deskclock.settings.PreferencesDefaultValues.ORANGE_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.PINK_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.PURPLE_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.RED_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesDefaultValues.SYSTEM_THEME; +import static com.best.deskclock.settings.PreferencesDefaultValues.YELLOW_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesKeys.KEY_AUTO_NIGHT_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesKeys.KEY_CARD_BACKGROUND; +import static com.best.deskclock.settings.PreferencesKeys.KEY_CARD_BORDER; +import static com.best.deskclock.settings.PreferencesKeys.KEY_CUSTOM_LANGUAGE_CODE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_DARK_MODE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_FADE_TRANSITIONS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_GENERAL_FONT; +import static com.best.deskclock.settings.PreferencesKeys.KEY_NIGHT_ACCENT_COLOR; +import static com.best.deskclock.settings.PreferencesKeys.KEY_THEME; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; + +import androidx.activity.EdgeToEdge; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; + +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.SdkUtils; +import com.best.deskclock.utils.ThemeUtils; +import com.best.deskclock.utils.Utils; +import com.google.android.material.color.MaterialColors; + +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * Base activity that ensures consistent theme, accent color, and locale settings + * across all activities in the app. + *

+ * This class handles: + *

+ *

+ */ +public class BaseActivity extends AppCompatActivity { + + /** + * List of supported preference keys for theme and UI settings management. + *

+ * This list is used to monitor only relevant keys within + * {@link #registerThemeListener()} to optimize change handling.

+ */ + private static final List SUPPORTED_PREF_KEYS = List.of( + KEY_THEME, KEY_DARK_MODE, KEY_GENERAL_FONT, KEY_ACCENT_COLOR, KEY_AUTO_NIGHT_ACCENT_COLOR, + KEY_NIGHT_ACCENT_COLOR, KEY_CUSTOM_LANGUAGE_CODE, KEY_CARD_BACKGROUND, KEY_CARD_BORDER, + KEY_FADE_TRANSITIONS + ); + + /** + * Map to store listeners by SharedPreferences so they can be removed cleanly + */ + private static final Map mListenerMap = new WeakHashMap<>(); + + private SharedPreferences mPrefs; + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(Utils.getLocalizedContext(newBase)); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + mPrefs = getDefaultSharedPreferences(this); + + initDebugAndNightlyDefaults(); + applyThemeAndAccentColor(); + + super.onCreate(savedInstanceState); + + registerThemeListener(); + } + + @Override + protected void onDestroy() { + unregisterThemeListener(); + super.onDestroy(); + } + + /** + * Initializes the default accent color and locale for debug and nightly builds. + */ + private void initDebugAndNightlyDefaults() { + if (!mPrefs.contains(KEY_ACCENT_COLOR)) { + if (BuildConfig.IS_DEBUG_BUILD) { + mPrefs.edit().putString(KEY_ACCENT_COLOR, RED_ACCENT_COLOR).apply(); + } else if (BuildConfig.IS_NIGHTLY_BUILD) { + mPrefs.edit().putString(KEY_ACCENT_COLOR, PURPLE_ACCENT_COLOR).apply(); + } + } + + if (!mPrefs.contains(KEY_CUSTOM_LANGUAGE_CODE)) { + if (BuildConfig.IS_DEBUG_BUILD || BuildConfig.IS_NIGHTLY_BUILD) { + mPrefs.edit().putString(KEY_CUSTOM_LANGUAGE_CODE, DEBUG_LANGUAGE_CODE).apply(); + } + } + } + + /** + * Apply the theme and the accent color to the activities. + */ + private void applyThemeAndAccentColor() { + final String theme = SettingsDAO.getTheme(mPrefs); + final String darkMode = SettingsDAO.getDarkMode(mPrefs); + final String accentColor = SettingsDAO.getAccentColor(mPrefs); + final boolean isAutoNightAccentColorEnabled = SettingsDAO.isAutoNightAccentColorEnabled(mPrefs); + final String nightAccentColor = SettingsDAO.getNightAccentColor(mPrefs); + + applyDarkThemeVariant(theme, darkMode); + + applyAccentColor(isAutoNightAccentColorEnabled, accentColor, nightAccentColor, darkMode); + + applyNavigationBarColor(darkMode); + } + + /** + * Apply the dark mode to the activities. + */ + private void applyDarkThemeVariant(String theme, String darkMode) { + if (darkMode.equals(DEFAULT_DARK_MODE)) { + applySystemNightMode(theme); + } else if (darkMode.equals(AMOLED_DARK_MODE) && !theme.equals(SYSTEM_THEME) && !theme.equals(LIGHT_THEME)) { + setTheme(R.style.AmoledTheme); + } + } + + /** + * Sets the accent color theme for the specified activity based on user preferences. + *

+ * Chooses between the regular and night accent color depending on the current theme + * and whether auto-night accent color is enabled.

+ * + * @param isAutoNightAccentColorEnabled True if automatic night accent color is enabled. + * @param accentColor The regular accent color value. + * @param nightAccentColor The night accent color value. + */ + private void applyAccentColor(boolean isAutoNightAccentColorEnabled, String accentColor, + String nightAccentColor, String darkMode) { + + String color = isAutoNightAccentColorEnabled + ? accentColor + : (ThemeUtils.isNight(getResources()) ? nightAccentColor : accentColor); + + switch (color) { + case BLACK_ACCENT_COLOR -> setTheme(R.style.BlackAccentColor); + case BLUE_ACCENT_COLOR -> setTheme(R.style.BlueAccentColor); + case BLUE_GRAY_ACCENT_COLOR -> setTheme(R.style.BlueGrayAccentColor); + case BROWN_ACCENT_COLOR -> setTheme(R.style.BrownAccentColor); + case GREEN_ACCENT_COLOR -> setTheme(R.style.GreenAccentColor); + case INDIGO_ACCENT_COLOR -> setTheme(R.style.IndigoAccentColor); + case ORANGE_ACCENT_COLOR -> setTheme(R.style.OrangeAccentColor); + case PINK_ACCENT_COLOR -> setTheme(R.style.PinkAccentColor); + case PURPLE_ACCENT_COLOR -> setTheme(R.style.PurpleAccentColor); + case RED_ACCENT_COLOR -> setTheme(R.style.RedAccentColor); + case YELLOW_ACCENT_COLOR -> setTheme(R.style.YellowAccentColor); + } + + if (ThemeUtils.isNight(getResources()) && darkMode.equals(AMOLED_DARK_MODE)) { + getWindow().getDecorView().setBackgroundColor(Color.BLACK); + } + } + + /** + * Applies a color to the navigation bar for activities. + */ + private void applyNavigationBarColor(String darkMode) { + if (SdkUtils.isAtLeastAndroid10()) { + if (this instanceof DeskClock) { + EdgeToEdge.enable(this); + getWindow().setNavigationBarContrastEnforced(false); + } + } else { + boolean isPhoneInLandscapeMode = !ThemeUtils.isTablet() && ThemeUtils.isLandscape(); + boolean isCardBackgroundDisplayed = SettingsDAO.isCardBackgroundDisplayed(mPrefs); + + if (ThemeUtils.isNight(getResources()) && darkMode.equals(AMOLED_DARK_MODE)) { + getWindow().setNavigationBarColor(Color.BLACK); + } else if (this instanceof DeskClock) { + getWindow().setNavigationBarColor(MaterialColors.getColor(this, + isPhoneInLandscapeMode || !isCardBackgroundDisplayed + ? android.R.attr.colorBackground + : com.google.android.material.R.attr.colorSurface, Color.BLACK)); + } else { + getWindow().setNavigationBarColor(MaterialColors.getColor(this, + android.R.attr.colorBackground, Color.BLACK)); + } + } + } + + /** + * Sets the night mode of AppCompatDelegate according to the selected theme. + * + * + * @param theme The theme value (corresponding to {@code KEY_THEME}) to apply. + */ + private void applySystemNightMode(String theme) { + switch (theme) { + case SYSTEM_THEME -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + case LIGHT_THEME -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + case DARK_THEME -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } + } + + /** + * Registers a SharedPreferences listener on the given activity to monitor UI-related settings. + * Triggers appropriate actions (theme update, locale change, etc.) when preferences change. + * Automatically recreates the activity when needed (e.g., on accent color, language, or theme change). + * + */ + private void registerThemeListener() { + // Avoid registering the listener multiple times for the same prefs + if (mListenerMap.containsKey(this)) { + return; + } + + // Important: we use a cached map of preference values to avoid unnecessary activity recreation. + // Without this check, any preference change (even with the same value) triggers recreate(), + // which causes significant slowdown when opening the settings screen, + // especially on low-end devices due to how Preferences are initialized. + Map cachedValues = Utils.initCachedValues(SUPPORTED_PREF_KEYS, this::getPreferenceValue); + + SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, key) -> { + if (key == null || !cachedValues.containsKey(key)) { + return; + } + + Object oldValue = cachedValues.get(key); + Object newValue = getPreferenceValue(key); + + boolean changed = (newValue == null && oldValue != null) + || (newValue != null && !newValue.equals(oldValue)); + + // If the value has not changed, do nothing + if (!changed) { + return; + } + + cachedValues.put(key, newValue); + + switch (key) { + case KEY_THEME -> { + String getTheme = SettingsDAO.getTheme(sharedPreferences); + applySystemNightMode(getTheme); + } + + case KEY_GENERAL_FONT, KEY_ACCENT_COLOR, KEY_CUSTOM_LANGUAGE_CODE -> recreate(); + + case KEY_DARK_MODE, KEY_NIGHT_ACCENT_COLOR -> { + if (ThemeUtils.isNight(getResources())) { + recreate(); + } + } + + // Add a short delay to have a smooth animation when the setting is a switch button + default -> new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (!isFinishing() && !isDestroyed()) { + recreate(); + } + }, 300); + } + }; + + mPrefs.registerOnSharedPreferenceChangeListener(listener); + mListenerMap.put(this, listener); + } + + /** + * Unregisters the internal listener to avoid memory leaks. + */ + private void unregisterThemeListener() { + SharedPreferences.OnSharedPreferenceChangeListener registeredListener = mListenerMap.remove(this); + if (registeredListener != null) { + mPrefs.unregisterOnSharedPreferenceChangeListener(registeredListener); + } + } + + /** + * Retrieves the value of the preference associated with the given key from SharedPreferences, + * returning a suitable default value based on the key. + * + * @param key The preference key to retrieve. + */ + private Object getPreferenceValue(String key) { + return switch (key) { + case KEY_THEME -> SettingsDAO.getTheme(mPrefs); + case KEY_DARK_MODE -> SettingsDAO.getDarkMode(mPrefs); + case KEY_GENERAL_FONT -> SettingsDAO.getGeneralFont(mPrefs); + case KEY_ACCENT_COLOR -> SettingsDAO.getAccentColor(mPrefs); + case KEY_AUTO_NIGHT_ACCENT_COLOR -> SettingsDAO.isAutoNightAccentColorEnabled(mPrefs); + case KEY_NIGHT_ACCENT_COLOR -> SettingsDAO.getNightAccentColor(mPrefs); + case KEY_CUSTOM_LANGUAGE_CODE -> SettingsDAO.getCustomLanguageCode(mPrefs); + case KEY_CARD_BACKGROUND -> SettingsDAO.isCardBackgroundDisplayed(mPrefs); + case KEY_CARD_BORDER -> SettingsDAO.isCardBorderDisplayed(mPrefs); + case KEY_FADE_TRANSITIONS -> SettingsDAO.isFadeTransitionsEnabled(mPrefs); + default -> null; + }; + } +} diff --git a/app/src/main/java/com/best/deskclock/DeskClockApplication.java b/app/src/main/java/com/best/deskclock/DeskClockApplication.java index 0da229f6d..8896cd0f0 100644 --- a/app/src/main/java/com/best/deskclock/DeskClockApplication.java +++ b/app/src/main/java/com/best/deskclock/DeskClockApplication.java @@ -17,6 +17,7 @@ import com.best.deskclock.controller.Controller; import com.best.deskclock.controller.ThemeController; import com.best.deskclock.data.DataModel; +import com.best.deskclock.holiday.HolidayRepository; import com.best.deskclock.events.LogEventTracker; import com.best.deskclock.uidata.UiDataModel; import com.best.deskclock.utils.LogUtils; @@ -43,6 +44,9 @@ public void onCreate() { Controller.getController().setContext(applicationContext); Controller.getController().addEventTracker(new LogEventTracker(applicationContext)); Controller.getController().updateShortcuts(); + + // Download holiday data on start + HolidayRepository.getInstance(applicationContext).updateWorkdayData(); } public static Context getContext() { diff --git a/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java b/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java index 19a6eec90..140231410 100644 --- a/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java +++ b/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java @@ -64,7 +64,7 @@ public static boolean processRestoredData(Context context) { if (alarm.enabled) { // Create the next alarm instance to schedule. - AlarmInstance alarmInstance = alarm.createInstanceAfter(now); + AlarmInstance alarmInstance = alarm.createInstanceAfter(this, now); // Add the next alarm instance to the database. AlarmInstance.addInstance(contentResolver, alarmInstance); diff --git a/app/src/main/java/com/best/deskclock/HandleApiCalls.java b/app/src/main/java/com/best/deskclock/HandleApiCalls.java index a27ddab66..d5c59cb4f 100644 --- a/app/src/main/java/com/best/deskclock/HandleApiCalls.java +++ b/app/src/main/java/com/best/deskclock/HandleApiCalls.java @@ -360,7 +360,7 @@ private void handleSetAlarm(Intent intent) { // Schedule the next instance. final Calendar now = DataModel.getDataModel().getCalendar(); - final AlarmInstance alarmInstance = alarm.createInstanceAfter(now); + final AlarmInstance alarmInstance = alarm.createInstanceAfter(this, now); setupInstance(alarmInstance, skipUi); final String time = DateFormat.getTimeFormat(this) diff --git a/app/src/main/java/com/best/deskclock/alarms/AlarmStateManager.java b/app/src/main/java/com/best/deskclock/alarms/AlarmStateManager.java index 5b0569453..f4db5fd45 100644 --- a/app/src/main/java/com/best/deskclock/alarms/AlarmStateManager.java +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmStateManager.java @@ -11,7 +11,7 @@ import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_SNOOZE_DURATION_DISABLED; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_NEVER; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; import static com.best.deskclock.utils.AlarmUtils.ACTION_NEXT_ALARM_CHANGED_BY_CLOCK; import android.app.AlarmManager; @@ -38,6 +38,7 @@ import com.best.deskclock.data.DataModel; import com.best.deskclock.data.SettingsDAO; import com.best.deskclock.events.Events; +import com.best.deskclock.holiday.HolidayUtils; import com.best.deskclock.provider.Alarm; import com.best.deskclock.provider.AlarmInstance; import com.best.deskclock.utils.AlarmUtils; @@ -243,12 +244,26 @@ private static void updateParentAlarm(Context context, AlarmInstance instance) { // Schedule the next repeating instance which may be before the current instance if a // time jump has occurred. Otherwise, if the current instance is the next instance // and has already been fired, schedule the subsequent instance. - AlarmInstance nextRepeatedInstance = alarm.createInstanceAfter(getCurrentTime()); + Calendar nextAlarmTime = HolidayUtils.getNextWorkdayAlarmTime(context, alarm.holidayOption, alarm, getCurrentTime()); if (instance.mAlarmState > AlarmInstance.FIRED_STATE - && nextRepeatedInstance.getAlarmTime().equals(instance.getAlarmTime())) { - nextRepeatedInstance = alarm.createInstanceAfter(instance.getAlarmTime()); + && nextAlarmTime.equals(instance.getAlarmTime())) { + nextAlarmTime = HolidayUtils.getNextWorkdayAlarmTime(context, alarm.holidayOption, alarm, instance.getAlarmTime()); } + AlarmInstance nextRepeatedInstance = new AlarmInstance(nextAlarmTime, alarm.id); + nextRepeatedInstance.mVibrate = alarm.vibrate; + nextRepeatedInstance.mFlash = alarm.flash; + nextRepeatedInstance.mLabel = alarm.label; + nextRepeatedInstance.mRingtone = RingtoneUtils.isRandomRingtone(alarm.alert) + ? RingtoneUtils.getRandomRingtoneUri() + : RingtoneUtils.isRandomCustomRingtone(alarm.alert) + ? RingtoneUtils.getRandomCustomRingtoneUri() + : alarm.alert; + nextRepeatedInstance.mAutoSilenceDuration = alarm.autoSilenceDuration; + nextRepeatedInstance.mSnoozeDuration = alarm.snoozeDuration; + nextRepeatedInstance.mCrescendoDuration = alarm.crescendoDuration; + nextRepeatedInstance.mAlarmVolume = alarm.alarmVolume; + LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " + AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime())); AlarmInstance.addInstance(cr, nextRepeatedInstance); @@ -462,7 +477,7 @@ public static void setMissedState(Context context, AlarmInstance instance) { // If the "Alarm silence" setting has not been set to "Never", we don't want alarms // to be seen as missed but snoozed. // This avoids having to create multiple alarms for the same reason. - if (instance.mAutoSilenceDuration != ALARM_TIMEOUT_NEVER) { + if (instance.mAutoSilenceDuration != TIMEOUT_NEVER) { setSnoozeState(context, instance, true); return; } diff --git a/app/src/main/java/com/best/deskclock/alarms/AlarmTimeClickHandler.java b/app/src/main/java/com/best/deskclock/alarms/AlarmTimeClickHandler.java index b95e24aa0..c3b556ba1 100644 --- a/app/src/main/java/com/best/deskclock/alarms/AlarmTimeClickHandler.java +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmTimeClickHandler.java @@ -9,44 +9,58 @@ import static android.content.Context.AUDIO_SERVICE; import static android.media.AudioManager.STREAM_ALARM; import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_SNOOZE_DURATION_DISABLED; import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_DATE_PICKER_STYLE; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VOLUME_CRESCENDO_DURATION; import static com.best.deskclock.settings.PreferencesDefaultValues.SPINNER_DATE_PICKER_STYLE; import static com.best.deskclock.settings.PreferencesDefaultValues.SPINNER_TIME_PICKER_STYLE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Typeface; import android.media.AudioManager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.widget.DatePicker; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.Observer; import com.best.deskclock.AlarmClockFragment; import com.best.deskclock.AutoSilenceDurationDialogFragment; import com.best.deskclock.AlarmSnoozeDurationDialogFragment; import com.best.deskclock.LabelDialogFragment; import com.best.deskclock.R; +import com.best.deskclock.VibrationPatternDialogFragment; import com.best.deskclock.VolumeCrescendoDurationDialogFragment; import com.best.deskclock.alarms.dataadapter.AlarmItemHolder; import com.best.deskclock.holiday.HolidayDialogFragment; import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.data.Weekdays; import com.best.deskclock.events.Events; import com.best.deskclock.provider.Alarm; import com.best.deskclock.provider.AlarmInstance; import com.best.deskclock.ringtone.RingtonePickerActivity; +import com.best.deskclock.uicomponents.CustomDialog; import com.best.deskclock.utils.LogUtils; +import com.best.deskclock.utils.ThemeUtils; import com.best.deskclock.utils.Utils; import com.google.android.material.datepicker.CalendarConstraints; import com.google.android.material.datepicker.DateValidatorPointForward; import com.google.android.material.datepicker.MaterialDatePicker; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.Calendar; +import java.util.TimeZone; /** * Click handler for an alarm time item. @@ -58,14 +72,17 @@ public final class AlarmTimeClickHandler implements OnTimeSetListener { private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap"; private final Fragment mFragment; private final Context mContext; + private final SharedPreferences mPrefs; private final AlarmUpdateHandler mAlarmUpdateHandler; private Alarm mSelectedAlarm; private Bundle mPreviousDaysOfWeekMap; + private AlertDialog mCurrentSpinnerDatePickerDialog = null; public AlarmTimeClickHandler(Fragment fragment, Bundle savedState, AlarmUpdateHandler alarmUpdateHandler) { mFragment = fragment; mContext = mFragment.requireContext(); + mPrefs = getDefaultSharedPreferences(mContext); mAlarmUpdateHandler = alarmUpdateHandler; if (savedState != null) { @@ -113,6 +130,14 @@ public void setAlarmVibrationEnabled(Alarm alarm, boolean newState) { } } + public void setVibrationPattern(Alarm alarm) { + Events.sendAlarmEvent(R.string.action_set_vibration_pattern, R.string.label_deskclock); + String vibrationPattern = alarm.vibrationPattern; + final VibrationPatternDialogFragment fragment = + VibrationPatternDialogFragment.newInstance(alarm, vibrationPattern, mFragment.getTag()); + VibrationPatternDialogFragment.show(mFragment.getParentFragmentManager(), fragment); + } + public void setAlarmFlashEnabled(Alarm alarm, boolean newState) { if (newState != alarm.flash) { alarm.flash = newState; @@ -138,22 +163,35 @@ public void setAutoSilenceDuration(Alarm alarm) { int autoSilenceDuration = alarm.autoSilenceDuration; final AutoSilenceDurationDialogFragment fragment = AutoSilenceDurationDialogFragment.newInstance(alarm, autoSilenceDuration, - autoSilenceDuration == ALARM_TIMEOUT_END_OF_RINGTONE, - mFragment.getTag()); + autoSilenceDuration == TIMEOUT_END_OF_RINGTONE, + autoSilenceDuration == TIMEOUT_NEVER, mFragment.getTag()); AutoSilenceDurationDialogFragment.show(mFragment.getParentFragmentManager(), fragment); } public void setSnoozeDuration(Alarm alarm) { Events.sendAlarmEvent(R.string.action_set_snooze_duration, R.string.label_deskclock); + int snoozeDuration = alarm.snoozeDuration; final AlarmSnoozeDurationDialogFragment fragment = - AlarmSnoozeDurationDialogFragment.newInstance(alarm, alarm.snoozeDuration, mFragment.getTag()); + AlarmSnoozeDurationDialogFragment.newInstance(alarm, snoozeDuration, + snoozeDuration == ALARM_SNOOZE_DURATION_DISABLED, mFragment.getTag()); AlarmSnoozeDurationDialogFragment.show(mFragment.getParentFragmentManager(), fragment); } + public void setMissedAlarmRepeatLimit(Alarm alarm) { + Events.sendAlarmEvent(R.string.action_set_missed_alarm_repeat_limit, R.string.label_deskclock); + int missedAlarmRepeatLimit = alarm.missedAlarmRepeatLimit; + final AlarmMissedRepeatLimitDialogFragment fragment = + AlarmMissedRepeatLimitDialogFragment.newInstance(alarm, missedAlarmRepeatLimit, mFragment.getTag()); + AlarmMissedRepeatLimitDialogFragment.show(mFragment.getParentFragmentManager(), fragment); + } + public void setCrescendoDuration(Alarm alarm) { Events.sendAlarmEvent(R.string.action_set_crescendo_duration, R.string.label_deskclock); + int crescendoDuration = alarm.crescendoDuration; final VolumeCrescendoDurationDialogFragment fragment = - VolumeCrescendoDurationDialogFragment.newInstance(alarm, alarm.crescendoDuration, mFragment.getTag()); + VolumeCrescendoDurationDialogFragment.newInstance(alarm, crescendoDuration, + crescendoDuration == DEFAULT_VOLUME_CRESCENDO_DURATION, + mFragment.getTag()); VolumeCrescendoDurationDialogFragment.show(mFragment.getParentFragmentManager(), fragment); } @@ -168,10 +206,17 @@ public void setDayOfWeekEnabled(Alarm alarm, boolean checked, int index) { final Calendar now = Calendar.getInstance(); final Calendar oldNextAlarmTime = alarm.getNextAlarmTime(now); - final int weekday = SettingsDAO.getWeekdayOrder(getDefaultSharedPreferences(mContext)).getCalendarDays().get(index); + // Reset date if a date is specified + if (alarm.isSpecifiedDate()) { + alarm.year = now.get(Calendar.YEAR); + alarm.month = now.get(Calendar.MONTH); + alarm.day = now.get(Calendar.DAY_OF_MONTH); + } + + final int weekday = SettingsDAO.getWeekdayOrder(mPrefs).getCalendarDays().get(index); alarm.daysOfWeek = alarm.daysOfWeek.setBit(weekday, checked); - // if the change altered the next scheduled alarm time, tell the user + // If the change altered the next scheduled alarm time, tell the user final Calendar newNextAlarmTime = alarm.getNextAlarmTime(now); final boolean popupToast = !oldNextAlarmTime.equals(newNextAlarmTime); mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, false); @@ -179,11 +224,10 @@ public void setDayOfWeekEnabled(Alarm alarm, boolean checked, int index) { Utils.setVibrationTime(mContext, 10); } - public void dismissAlarmInstance(Alarm alarm, AlarmInstance alarmInstance) { + public void dismissAlarmInstance(AlarmInstance alarmInstance) { final Intent dismissIntent = AlarmStateManager.createStateChangeIntent(mContext, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance, AlarmInstance.PREDISMISSED_STATE); mContext.startService(dismissIntent); - mAlarmUpdateHandler.showPredismissToast(alarm, alarmInstance); Utils.setVibrationTime(mContext, 50); } @@ -224,29 +268,51 @@ public void onEditLabelClicked(Alarm alarm) { public void onClockClicked(Alarm alarm) { mSelectedAlarm = alarm; - Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock); - if (SettingsDAO.getMaterialTimePickerStyle( - getDefaultSharedPreferences(mContext)).equals(SPINNER_TIME_PICKER_STYLE)) { - showCustomSpinnerTimePicker(alarm.hour, alarm.minutes); + + if (SettingsDAO.getMaterialTimePickerStyle(mPrefs).equals(SPINNER_TIME_PICKER_STYLE)) { + showSpinnerTimePickerDialog(alarm.hour, alarm.minutes); } else { showMaterialTimePicker(alarm.hour, alarm.minutes); } } - private void showCustomSpinnerTimePicker(int hour, int minutes) { - CustomSpinnerTimePickerDialog.show(mContext, mFragment, hour, minutes, this); + public void onClockLongClicked(Alarm alarm) { + mSelectedAlarm = alarm; + showAlarmDelayPickerDialog(); } - private void showMaterialTimePicker(int hour, int minutes) { - MaterialTimePickerDialog.show(mContext, ((AppCompatActivity) mContext).getSupportFragmentManager(), - TAG, hour, minutes, getDefaultSharedPreferences(mContext), this); + public void showAlarmDelayPickerDialog() { + Events.sendAlarmEvent(R.string.action_set_delay, R.string.label_deskclock); + + final AlarmDelayPickerDialogFragment fragment = + AlarmDelayPickerDialogFragment.newInstance(0, 0); + AlarmDelayPickerDialogFragment.show(mFragment.getParentFragmentManager(), fragment); + } + + public void showSpinnerTimePickerDialog(int hours, int minutes) { + Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock); + + final SpinnerTimePickerDialogFragment fragment = SpinnerTimePickerDialogFragment.newInstance(hours, minutes); + SpinnerTimePickerDialogFragment.show(mFragment.getParentFragmentManager(), fragment); + } + + public void showMaterialTimePicker(int hours, int minutes) { + FragmentManager fragmentManager = ((AppCompatActivity) mContext).getSupportFragmentManager(); + + // Prevents opening the same dialog twice + if (fragmentManager.findFragmentByTag(TAG) != null) { + return; + } + + Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock); + + MaterialTimePickerDialog.show(mContext, fragmentManager, TAG, hours, minutes, mPrefs, this); } public void onDateClicked(Alarm alarm) { mSelectedAlarm = alarm; - Events.sendAlarmEvent(R.string.action_set_date, R.string.label_deskclock); - if (SettingsDAO.getMaterialDatePickerStyle( - getDefaultSharedPreferences(mContext)).equals(SPINNER_DATE_PICKER_STYLE)) { + + if (SettingsDAO.getMaterialDatePickerStyle(mPrefs).equals(SPINNER_DATE_PICKER_STYLE)) { showSpinnerDatePicker(alarm); } else { showMaterialDatePicker(alarm); @@ -254,52 +320,97 @@ public void onDateClicked(Alarm alarm) { } public void showSpinnerDatePicker(Alarm alarm) { + if (mCurrentSpinnerDatePickerDialog != null && mCurrentSpinnerDatePickerDialog.isShowing()) { + return; + } + + Events.sendAlarmEvent(R.string.action_set_date, R.string.label_deskclock); + LayoutInflater inflater = mFragment.getLayoutInflater(); + @SuppressLint("InflateParams") View dialogView = inflater.inflate(R.layout.spinner_date_picker, null); DatePicker datePicker = dialogView.findViewById(R.id.spinner_date_picker); - Calendar currentCalendar = Calendar.getInstance(); - long currentDateInMillis = currentCalendar.getTimeInMillis(); - - // If the alarm is set for today or is still set in the past, and the time has already - // passed, prevent today from being selected. Otherwise, allow the current day to be selected. - int currentMonth = currentCalendar.get(Calendar.MONTH); - if (alarm.year == currentCalendar.get(Calendar.YEAR) - && alarm.month == currentMonth - && alarm.day == currentCalendar.get(Calendar.DAY_OF_MONTH)) { - - if (alarm.hour < currentCalendar.get(Calendar.HOUR_OF_DAY) - || (alarm.hour == currentCalendar.get(Calendar.HOUR_OF_DAY) && alarm.minutes < currentCalendar.get(Calendar.MINUTE)) - || (alarm.hour == currentCalendar.get(Calendar.HOUR_OF_DAY) && alarm.minutes == currentCalendar.get(Calendar.MINUTE))) { - currentCalendar.add(Calendar.DAY_OF_MONTH, 1); - datePicker.setMinDate(currentCalendar.getTimeInMillis()); - } else { - datePicker.setMinDate(currentDateInMillis); + Calendar now = Calendar.getInstance(); + Calendar selectionDate = (Calendar) now.clone(); + Calendar minDate = (Calendar) now.clone(); + + // Date selection and minimum date to display + boolean timePassed = alarm.isTimeBeforeOrEqual(now); + boolean isTomorrow = alarm.isTomorrow(now); + + // Date not specified + if (!alarm.isSpecifiedDate()) { + // Case 1: today or tomorrow depending on isTomorrow() + if (isTomorrow) { + selectionDate.add(Calendar.DAY_OF_MONTH, 1); + minDate.add(Calendar.DAY_OF_MONTH, 1); } + // else: keep today as selection and minDate } else { - datePicker.setMinDate(currentDateInMillis); + // Alarm has specified date + if (alarm.isDateInThePast() || alarm.isScheduledForToday(now)) { + // Case 2.1: date in the past or today + if (timePassed) { + selectionDate.add(Calendar.DAY_OF_MONTH, 1); + minDate.add(Calendar.DAY_OF_MONTH, 1); + } + // else: today is valid + } else { + // Case 2.2: future date + selectionDate.set(alarm.year, alarm.month, alarm.day); + + if (timePassed) { + minDate.add(Calendar.DAY_OF_MONTH, 1); + } + } } - datePicker.init(alarm.year, alarm.month, alarm.day, null); + datePicker.setMinDate(minDate.getTimeInMillis()); + + datePicker.init(selectionDate.get(Calendar.YEAR), selectionDate.get(Calendar.MONTH), + selectionDate.get(Calendar.DAY_OF_MONTH), null); - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mContext, R.style.SpinnerDialogTheme); - builder - .setTitle(mContext.getString(R.string.date_picker_dialog_title)) - .setView(dialogView) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { + mCurrentSpinnerDatePickerDialog = CustomDialog.create( + mContext, + R.style.SpinnerDialogTheme, + null, + mContext.getString(R.string.date_picker_dialog_title), + null, + dialogView, + mContext.getString(android.R.string.ok), + (d, w) -> { int newYear = datePicker.getYear(); int newMonth = datePicker.getMonth(); int newDay = datePicker.getDayOfMonth(); onDateSet(newYear, newMonth, newDay, alarm.hour, alarm.minutes); - }) - .setNegativeButton(android.R.string.cancel, null); - - builder.create().show(); + }, + mContext.getString(android.R.string.cancel), + null, + null, + null, + null, + CustomDialog.SoftInputMode.SHOW_KEYBOARD + ); + + mCurrentSpinnerDatePickerDialog.setOnDismissListener(dialog -> + mCurrentSpinnerDatePickerDialog = null); + + mCurrentSpinnerDatePickerDialog.show(); } public void showMaterialDatePicker(Alarm alarm) { - String materialDatePickerStyle = SettingsDAO.getMaterialDatePickerStyle(getDefaultSharedPreferences(mContext)); + FragmentManager fragmentManager = ((AppCompatActivity) mContext).getSupportFragmentManager(); + + // Prevents opening the same dialog twice + if (fragmentManager.findFragmentByTag(TAG) != null) { + return; + } + + Events.sendAlarmEvent(R.string.action_set_date, R.string.label_deskclock); + + String materialDatePickerStyle = SettingsDAO.getMaterialDatePickerStyle(mPrefs); MaterialDatePicker.Builder builder = MaterialDatePicker.Builder.datePicker(); // Set date picker style @@ -307,55 +418,103 @@ public void showMaterialDatePicker(Alarm alarm) { ? MaterialDatePicker.INPUT_MODE_CALENDAR : MaterialDatePicker.INPUT_MODE_TEXT); - // If a date has already been selected, select it when opening the MaterialDatePicker. - if (alarm.isSpecifiedDate()) { - Calendar currentCalendar = Calendar.getInstance(); - // If the date is in the past, select today's date. - if (alarm.isDateInThePast()) { - long currentDateInMillis = currentCalendar.getTimeInMillis(); - builder.setSelection(currentDateInMillis); + Calendar now = Calendar.getInstance(); + Calendar selectionDate = (Calendar) now.clone(); + + // Date selection + boolean timePassed = alarm.isTimeBeforeOrEqual(now); + + // Date not specified + if (!alarm.isSpecifiedDate()) { + // Case 1: today or tomorrow depending on isTomorrow() + if (alarm.isTomorrow(now)) { + selectionDate.add(Calendar.DAY_OF_MONTH, 1); + } + } else { + // Alarm has specified date + if (alarm.isDateInThePast() || alarm.isScheduledForToday(now)) { + // Case 2.1: Date in the past or today's date + if (timePassed) { + selectionDate.add(Calendar.DAY_OF_MONTH, 1); + } } else { - currentCalendar.set(alarm.year, alarm.month, alarm.day); - long alarmDate = currentCalendar.getTimeInMillis(); - builder.setSelection(alarmDate); + // Case 2.2: Date in the future + selectionDate.set(alarm.year, alarm.month, alarm.day); } } - // If the alarm is set for today or is still set in the past, and the time has already - // passed, prevent today from being selected. Otherwise, allow the current day to be selected. CalendarConstraints.Builder constraintsBuilder = new CalendarConstraints.Builder(); - Calendar currentTime = Calendar.getInstance(); - int currentMonth = currentTime.get(Calendar.MONTH); - - if (alarm.isDateInThePast() - || (alarm.year == currentTime.get(Calendar.YEAR) - && alarm.month == currentMonth - && alarm.day == currentTime.get(Calendar.DAY_OF_MONTH))) { + // Prevents navigation to past months + constraintsBuilder.setStart(now.getTimeInMillis()); - if (alarm.hour < currentTime.get(Calendar.HOUR_OF_DAY) - || (alarm.hour == currentTime.get(Calendar.HOUR_OF_DAY) && alarm.minutes < currentTime.get(Calendar.MINUTE)) - || (alarm.hour == currentTime.get(Calendar.HOUR_OF_DAY) && alarm.minutes == currentTime.get(Calendar.MINUTE))) { - constraintsBuilder.setValidator(DateValidatorPointForward.from(currentTime.getTimeInMillis())); - } else { - constraintsBuilder.setValidator(DateValidatorPointForward.now()); - } + // Set validator depending on whether the alarm time has passed or not + if (timePassed) { + constraintsBuilder.setValidator(DateValidatorPointForward.from(now.getTimeInMillis())); } else { constraintsBuilder.setValidator(DateValidatorPointForward.now()); } - // Don't display past months and years - constraintsBuilder.setStart(currentTime.getTimeInMillis()); - + builder.setSelection(selectionDate.getTimeInMillis()); builder.setCalendarConstraints(constraintsBuilder.build()); MaterialDatePicker materialDatePicker = builder.build(); - materialDatePicker.show(((AppCompatActivity) mContext).getSupportFragmentManager(), TAG); + materialDatePicker.getViewLifecycleOwnerLiveData().observeForever(new Observer<>() { + @Override public void onChanged(LifecycleOwner owner) { + if (owner == null) { + return; + } + + View root = materialDatePicker.getView(); + if (root == null) { + return; + } + + Typeface generalFont = ThemeUtils.loadFont(SettingsDAO.getGeneralFont(mPrefs)); + if (generalFont == null) { + materialDatePicker.getViewLifecycleOwnerLiveData().removeObserver(this); + return; + } + + // Bouton OK + TextView ok = root.findViewById( + com.google.android.material.R.id.confirm_button); + if (ok != null) { + ok.setTypeface(generalFont); + } + + // Bouton Cancel + TextView cancel = root.findViewById( + com.google.android.material.R.id.cancel_button); + if (cancel != null) { + cancel.setTypeface(generalFont); + } + + // Titre ("Select date") + TextView title = root.findViewById( + com.google.android.material.R.id.mtrl_picker_title_text); + if (title != null) { + title.setTypeface(generalFont); + } + + // Texte de sélection (la date en gros) + TextView headerSelection = root.findViewById( + com.google.android.material.R.id.mtrl_picker_header_selection_text); + if (headerSelection != null) { + headerSelection.setTypeface(generalFont); + } + + // Très important : on se désabonne + materialDatePicker.getViewLifecycleOwnerLiveData().removeObserver(this); + } + }); + + materialDatePicker.show(fragmentManager, TAG); materialDatePicker.addOnPositiveButtonClickListener(selection -> { // Selection contains the selected date as a timestamp (long) - Calendar calendar = Calendar.getInstance(); + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.setTimeInMillis(selection); int year = calendar.get(Calendar.YEAR); @@ -368,6 +527,10 @@ public void showMaterialDatePicker(Alarm alarm) { public void onDateSet(int year, int month, int day, int hourOfDay, int minute) { if (mSelectedAlarm != null) { + // Disable days of the week if one or more are selected + if (mSelectedAlarm.daysOfWeek.isRepeating()) { + mSelectedAlarm.daysOfWeek = Weekdays.NONE; + } mSelectedAlarm.year = year; mSelectedAlarm.month = month; mSelectedAlarm.day = day; @@ -387,44 +550,81 @@ public void onRemoveDateClicked(Alarm alarm) { @Override public void onTimeSet(int hourOfDay, int minute) { + setAlarm(hourOfDay, minute); + } + + public void setAlarm(int hour, int minute) { if (mSelectedAlarm == null) { - // If mSelectedAlarm is null then we're creating a new alarm. - final Alarm alarm = new Alarm(); - final SharedPreferences prefs = getDefaultSharedPreferences(mContext); - final AudioManager audioManager = (AudioManager) mContext.getSystemService(AUDIO_SERVICE); - - alarm.hour = hourOfDay; - alarm.minutes = minute; - alarm.enabled = true; - alarm.vibrate = SettingsDAO.areAlarmVibrationsEnabledByDefault(prefs); - alarm.flash = SettingsDAO.shouldTurnOnBackFlashForTriggeredAlarm(prefs); - alarm.deleteAfterUse = SettingsDAO.isOccasionalAlarmDeletedByDefault(prefs); - alarm.autoSilenceDuration = SettingsDAO.getAlarmTimeout(prefs); - alarm.snoozeDuration = SettingsDAO.getSnoozeLength(prefs); - alarm.crescendoDuration = SettingsDAO.getAlarmVolumeCrescendoDuration(prefs); - alarm.alarmVolume = audioManager.getStreamVolume(STREAM_ALARM); - - mAlarmUpdateHandler.asyncAddAlarm(alarm); + mAlarmUpdateHandler.asyncAddAlarm(buildNewAlarm(hour, minute)); } else { - mSelectedAlarm.hour = hourOfDay; - mSelectedAlarm.minutes = minute; - // Necessary when an existing alarm has been created in the past and it is not enabled. - // Even if the date is not specified, it is saved in AlarmInstance; we need to make - // sure that the date is not in the past when changing time, in which case we reset - // to the current date (an alarm cannot be triggered in the past). - // This is due to the change in the code made with commit : 6ac23cf. - // Fix https://github.com/BlackyHawky/Clock/issues/299 - if (mSelectedAlarm.isDateInThePast()) { - Calendar currentCalendar = Calendar.getInstance(); - mSelectedAlarm.year = currentCalendar.get(Calendar.YEAR); - mSelectedAlarm.month = currentCalendar.get(Calendar.MONTH); - mSelectedAlarm.day = currentCalendar.get(Calendar.DAY_OF_MONTH); - } - mSelectedAlarm.enabled = true; + updateExistingAlarm(hour, minute, false, false); + } + } - mAlarmUpdateHandler.asyncUpdateAlarm(mSelectedAlarm, true, false); + public void setAlarmWithDelay(int hour, int minute) { + Calendar alarmTime = Calendar.getInstance(); + alarmTime.add(Calendar.HOUR_OF_DAY, hour); + alarmTime.add(Calendar.MINUTE, minute); - mSelectedAlarm = null; + int h = alarmTime.get(Calendar.HOUR_OF_DAY); + int m = alarmTime.get(Calendar.MINUTE); + + if (mSelectedAlarm == null) { + mAlarmUpdateHandler.asyncAddAlarm(buildNewAlarm(h, m)); + } else { + updateExistingAlarm(h, m, true, true); + } + } + + private Alarm buildNewAlarm(int hour, int minute) { + final Alarm alarm = new Alarm(); + final AudioManager audioManager = (AudioManager) mContext.getSystemService(AUDIO_SERVICE); + + alarm.hour = hour; + alarm.minutes = minute; + alarm.enabled = true; + alarm.vibrate = SettingsDAO.areAlarmVibrationsEnabledByDefault(mPrefs); + alarm.vibrationPattern = SettingsDAO.getVibrationPattern(mPrefs); + alarm.flash = SettingsDAO.shouldTurnOnBackFlashForTriggeredAlarm(mPrefs); + alarm.deleteAfterUse = SettingsDAO.isOccasionalAlarmDeletedByDefault(mPrefs); + alarm.autoSilenceDuration = SettingsDAO.getAlarmTimeout(mPrefs); + alarm.snoozeDuration = SettingsDAO.getSnoozeLength(mPrefs); + alarm.missedAlarmRepeatLimit = SettingsDAO.getMissedAlarmRepeatLimit(mPrefs); + alarm.crescendoDuration = SettingsDAO.getAlarmVolumeCrescendoDuration(mPrefs); + alarm.alarmVolume = audioManager.getStreamVolume(STREAM_ALARM); + + return alarm; + } + + private void updateExistingAlarm(int hour, int minute, boolean resetDaysOfWeek, boolean checkSpecifiedDate) { + mSelectedAlarm.hour = hour; + mSelectedAlarm.minutes = minute; + + if (resetDaysOfWeek) { + mSelectedAlarm.daysOfWeek = Weekdays.fromBits(0); + } + + Calendar currentCalendar = Calendar.getInstance(); + + // Necessary when an existing alarm has been created in the past and it is not enabled. + // Even if the date is not specified, it is saved in AlarmInstance; we need to make + // sure that the date is not in the past when changing time, in which case we reset + // to the current date (an alarm cannot be triggered in the past). + // This is due to the change in the code made with commit : 6ac23cf. + // Fix https://github.com/BlackyHawky/Clock/issues/299 + boolean mustResetDate = mSelectedAlarm.isDateInThePast() || + (checkSpecifiedDate && mSelectedAlarm.isSpecifiedDate()); + + if (mustResetDate) { + mSelectedAlarm.year = currentCalendar.get(Calendar.YEAR); + mSelectedAlarm.month = currentCalendar.get(Calendar.MONTH); + mSelectedAlarm.day = currentCalendar.get(Calendar.DAY_OF_MONTH); } + + mSelectedAlarm.enabled = true; + + mAlarmUpdateHandler.asyncUpdateAlarm(mSelectedAlarm, true, false); + mSelectedAlarm = null; } + } diff --git a/app/src/main/java/com/best/deskclock/alarms/AlarmUpdateHandler.java b/app/src/main/java/com/best/deskclock/alarms/AlarmUpdateHandler.java index 0a2ea2167..cc701b7a5 100644 --- a/app/src/main/java/com/best/deskclock/alarms/AlarmUpdateHandler.java +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmUpdateHandler.java @@ -198,7 +198,7 @@ private void showUndoBar() { private AlarmInstance setupAlarmInstance(Alarm alarm) { final ContentResolver cr = mAppContext.getContentResolver(); - AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance()); + AlarmInstance newInstance = alarm.createInstanceAfter(mAppContext, Calendar.getInstance()); AlarmInstance.addInstance(cr, newInstance); // Register instance to state manager AlarmStateManager.registerInstance(mAppContext, newInstance, true); diff --git a/app/src/main/java/com/best/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java b/app/src/main/java/com/best/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java index e50abd6ea..29ec96793 100644 --- a/app/src/main/java/com/best/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java +++ b/app/src/main/java/com/best/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java @@ -10,11 +10,15 @@ import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; -import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_SNOOZE_DURATION_DISABLED; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_END_OF_RINGTONE; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_NEVER; -import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VOLUME_CRESCENDO_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; +import static com.best.deskclock.settings.PreferencesDefaultValues.VIBRATION_PATTERN_ESCALATING; +import static com.best.deskclock.settings.PreferencesDefaultValues.VIBRATION_PATTERN_HEARTBEAT; +import static com.best.deskclock.settings.PreferencesDefaultValues.VIBRATION_PATTERN_SOFT; +import static com.best.deskclock.settings.PreferencesDefaultValues.VIBRATION_PATTERN_STRONG; +import static com.best.deskclock.settings.PreferencesDefaultValues.VIBRATION_PATTERN_TICK_TOCK; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -22,12 +26,10 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; -import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.media.AudioManager; -import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -38,7 +40,6 @@ import android.widget.TextView; import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.best.deskclock.ItemAdapter; @@ -50,14 +51,12 @@ import com.best.deskclock.uidata.UiDataModel; import com.best.deskclock.utils.AlarmUtils; import com.best.deskclock.utils.AnimatorUtils; +import com.best.deskclock.utils.DeviceUtils; import com.best.deskclock.utils.RingtoneUtils; -import com.best.deskclock.utils.Utils; import com.google.android.material.chip.Chip; import com.google.android.material.color.MaterialColors; -import java.text.SimpleDateFormat; -import java.util.Calendar; import java.util.List; import java.util.Locale; @@ -65,61 +64,72 @@ * A ViewHolder containing views for an alarm item in expanded state. */ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder { + private final TextView holidayOption; public static final int VIEW_TYPE = R.layout.alarm_time_expanded; - private final SharedPreferences mPrefs; private final ImageView editLabelIcon; private final TextView editLabel; private final LinearLayout repeatDays; private final CompoundButton[] dayButtons = new CompoundButton[7]; - private final View emptyView; private final TextView scheduleAlarm; private final TextView selectedDate; private final ImageView addDate; private final ImageView removeDate; private final TextView ringtone; private final CheckBox vibrate; + private final TextView vibrationPatternTitle; + private final TextView vibrationPatternValue; private final CheckBox flash; private final CheckBox deleteOccasionalAlarmAfterUse; private final TextView autoSilenceDurationTitle; private final TextView autoSilenceDurationValue; private final TextView snoozeDurationTitle; private final TextView snoozeDurationValue; + private final TextView missedAlarmRepeatLimitTitle; + private final TextView missedAlarmRepeatLimitValue; private final TextView crescendoDurationTitle; private final TextView crescendoDurationValue; private final TextView alarmVolumeTitle; private final TextView alarmVolumeValue; private final Chip delete; private final Chip duplicate; - private final TextView holidayOption; private final boolean mHasVibrator; private final boolean mHasFlash; + private final SharedPreferences mPrefs; + private final Typeface mGeneralTypeface; + private final Typeface mGeneralBoldTypeface; private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator, boolean hasFlash) { super(itemView); final Context context = itemView.getContext(); - mPrefs = getDefaultSharedPreferences(context); mHasVibrator = hasVibrator; mHasFlash = hasFlash; + mPrefs = com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences(context); + String fontPath = com.best.deskclock.data.SettingsDAO.getGeneralFont(mPrefs); + mGeneralTypeface = com.best.deskclock.utils.ThemeUtils.loadFont(fontPath); + mGeneralBoldTypeface = com.best.deskclock.utils.ThemeUtils.boldTypeface(fontPath); editLabelIcon = itemView.findViewById(R.id.edit_label_icon); editLabel = itemView.findViewById(R.id.edit_label); repeatDays = itemView.findViewById(R.id.repeat_days); - emptyView = itemView.findViewById(R.id.empty_view); scheduleAlarm = itemView.findViewById(R.id.schedule_alarm); selectedDate = itemView.findViewById(R.id.selected_date); addDate = itemView.findViewById(R.id.add_date); removeDate = itemView.findViewById(R.id.remove_date); ringtone = itemView.findViewById(R.id.choose_ringtone); vibrate = itemView.findViewById(R.id.vibrate_onoff); + vibrationPatternTitle = itemView.findViewById(R.id.vibration_pattern_title); + vibrationPatternValue = itemView.findViewById(R.id.vibration_pattern_value); flash = itemView.findViewById(R.id.flash_onoff); deleteOccasionalAlarmAfterUse = itemView.findViewById(R.id.delete_occasional_alarm_after_use); autoSilenceDurationTitle = itemView.findViewById(R.id.auto_silence_duration_title); autoSilenceDurationValue = itemView.findViewById(R.id.auto_silence_duration_value); snoozeDurationTitle = itemView.findViewById(R.id.snooze_duration_title); snoozeDurationValue = itemView.findViewById(R.id.snooze_duration_value); + missedAlarmRepeatLimitTitle = itemView.findViewById(R.id.missed_alarm_repeat_limit_title); + missedAlarmRepeatLimitValue = itemView.findViewById(R.id.missed_alarm_repeat_limit_value); crescendoDurationTitle = itemView.findViewById(R.id.crescendo_duration_title); crescendoDurationValue = itemView.findViewById(R.id.crescendo_duration_value); alarmVolumeTitle = itemView.findViewById(R.id.alarm_volume_title); @@ -144,9 +154,6 @@ private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator, boolean hasF editLabel.setOnClickListener(view -> getAlarmTimeClickHandler().onEditLabelClicked(getItemHolder().item)); - holidayOption.setOnClickListener(view -> - getAlarmTimeClickHandler().onHolidayOptionClicked(getItemHolder().item)); - // Build button for each day. final LayoutInflater inflater = LayoutInflater.from(context); final List weekdays = SettingsDAO.getWeekdayOrder(mPrefs).getCalendarDays(); @@ -195,6 +202,14 @@ private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator, boolean hasF getAlarmTimeClickHandler().setAlarmVibrationEnabled( getItemHolder().item, ((CheckBox) v).isChecked())); + // Vibration pattern handler + vibrationPatternTitle.setOnClickListener(v -> + getAlarmTimeClickHandler().setVibrationPattern(getItemHolder().item)); + + // Vibration pattern handler + vibrationPatternValue.setOnClickListener(v -> + getAlarmTimeClickHandler().setVibrationPattern(getItemHolder().item)); + // Flash checkbox handler flash.setOnClickListener(v -> getAlarmTimeClickHandler().setAlarmFlashEnabled( @@ -221,6 +236,14 @@ private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator, boolean hasF snoozeDurationValue.setOnClickListener(v -> getAlarmTimeClickHandler().setSnoozeDuration(getItemHolder().item)); + // Missed alarm repeat limit handler + missedAlarmRepeatLimitTitle.setOnClickListener(v -> + getAlarmTimeClickHandler().setMissedAlarmRepeatLimit(getItemHolder().item)); + + // Missed alarm repeat limit handler + missedAlarmRepeatLimitValue.setOnClickListener(v -> + getAlarmTimeClickHandler().setMissedAlarmRepeatLimit(getItemHolder().item)); + // Crescendo duration handler crescendoDurationTitle.setOnClickListener(v -> getAlarmTimeClickHandler().setCrescendoDuration(getItemHolder().item)); @@ -244,6 +267,9 @@ private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator, boolean hasF }); // Duplicate alarm handler + holidayOption.setOnClickListener(v -> + getAlarmTimeClickHandler().onHolidayOptionClicked(getItemHolder().item)); + duplicate.setOnClickListener(v -> { getAlarmTimeClickHandler().onDuplicateClicked(getItemHolder()); v.announceForAccessibility(context.getString(R.string.alarm_created)); @@ -260,17 +286,20 @@ protected void onBindItemView(final AlarmItemHolder itemHolder) { final Context context = itemView.getContext(); bindEditLabel(context, alarm); bindDaysOfWeekButtons(alarm, context); - bindScheduleAlarm(alarm); + bindScheduleAlarm(); bindSelectedDate(alarm); bindRingtone(context, alarm); - bindVibrator(alarm); + bindVibrator(context, alarm); bindFlash(alarm); bindDeleteOccasionalAlarmAfterUse(alarm); bindEditLabelAnnotations(alarm); bindAutoSilenceValue(context, alarm); bindSnoozeValue(context, alarm); + bindMissedAlarmRepeatLimit(context, alarm); bindCrescendoValue(context, alarm); bindAlarmVolume(context, alarm); + bindDeleteAndDuplicateButtons(); + bindHolidayOption(context, alarm); // If this view is bound without coming from a CollapsedAlarmViewHolder (e.g. // when calling expand() before this alarm was visible in it's collapsed state), @@ -285,7 +314,6 @@ protected void onBindItemView(final AlarmItemHolder itemHolder) { // and to avoid flickering when turning the alarm on/off final boolean labelIsEmpty = alarm.label == null || alarm.label.isEmpty(); editLabel.setAlpha(labelIsEmpty || alarm.enabled ? 1f : editLabel.getAlpha()); - holidayOption.setAlpha(1f); repeatDays.setAlpha(1f); scheduleAlarm.setAlpha(1f); selectedDate.setAlpha(1f); @@ -296,12 +324,17 @@ protected void onBindItemView(final AlarmItemHolder itemHolder) { autoSilenceDurationValue.setAlpha(1f); snoozeDurationTitle.setAlpha(1f); snoozeDurationValue.setAlpha(1f); + missedAlarmRepeatLimitTitle.setAlpha(1f); + missedAlarmRepeatLimitValue.setAlpha(1f); crescendoDurationTitle.setAlpha(1f); crescendoDurationValue.setAlpha(1f); alarmVolumeTitle.setAlpha(1f); alarmVolumeValue.setAlpha(1f); preemptiveDismissButton.setAlpha(1f); vibrate.setAlpha(1f); + holidayOption.setAlpha(1f); + vibrationPatternTitle.setAlpha(1f); + vibrationPatternValue.setAlpha(1f); flash.setAlpha(1f); deleteOccasionalAlarmAfterUse.setAlpha(1f); delete.setAlpha(1f); @@ -312,84 +345,165 @@ private void bindEditLabel(Context context, Alarm alarm) { final boolean alarmLabelIsEmpty = alarm.label == null || alarm.label.isEmpty(); editLabel.setText(alarm.label); - editLabel.setTypeface(alarmLabelIsEmpty || !alarm.enabled ? Typeface.DEFAULT : Typeface.DEFAULT_BOLD); + + Typeface typeface = alarm.enabled + ? mGeneralBoldTypeface + : mGeneralTypeface; + + editLabel.setTypeface(typeface); + editLabel.setContentDescription(alarmLabelIsEmpty ? context.getString(R.string.no_label_specified) : context.getString(R.string.label_description) + " " + alarm.label); } private void bindAutoSilenceValue(Context context, Alarm alarm) { - int autoSilenceDuration = alarm.autoSilenceDuration; + if (SettingsDAO.isPerAlarmAutoSilenceEnabled(mPrefs)) { + int autoSilenceDuration = alarm.autoSilenceDuration; + + int m = autoSilenceDuration / 60; + int s = autoSilenceDuration % 60; + + if (m > 0 && s > 0) { + String minutesString = context.getResources().getQuantityString(R.plurals.minutes_short, m, m); + String secondsString = s + " " + context.getString(R.string.seconds_label); + autoSilenceDurationValue.setText(String.format("%s %s", minutesString, secondsString)); + } else if (m > 0) { + autoSilenceDurationValue.setText(context.getResources().getQuantityString(R.plurals.minutes_short, m, m)); + } else if (autoSilenceDuration == TIMEOUT_NEVER) { + autoSilenceDurationValue.setText(context.getString(R.string.label_never)); + } else if (autoSilenceDuration == TIMEOUT_END_OF_RINGTONE) { + autoSilenceDurationValue.setText(context.getString(R.string.auto_silence_end_of_ringtone)); + } else { + String secondsString = s + " " + context.getString(R.string.seconds_label); + autoSilenceDurationValue.setText(secondsString); + } + + autoSilenceDurationTitle.setTypeface(mGeneralTypeface); + autoSilenceDurationValue.setTypeface(mGeneralTypeface); - if (autoSilenceDuration == ALARM_TIMEOUT_NEVER) { - autoSilenceDurationValue.setText(context.getString(R.string.auto_silence_never)); - } else if (autoSilenceDuration == ALARM_TIMEOUT_END_OF_RINGTONE) { - autoSilenceDurationValue.setText(context.getString(R.string.auto_silence_end_of_ringtone)); + autoSilenceDurationTitle.setVisibility(VISIBLE); + autoSilenceDurationValue.setVisibility(VISIBLE); } else { - autoSilenceDurationValue.setText(context.getResources().getQuantityString( - R.plurals.minutes_short, autoSilenceDuration, autoSilenceDuration)); + autoSilenceDurationTitle.setVisibility(GONE); + autoSilenceDurationValue.setVisibility(GONE); } } private void bindSnoozeValue(Context context, Alarm alarm) { - int snoozeDuration = alarm.snoozeDuration; - - int h = snoozeDuration / 60; - int m = snoozeDuration % 60; - - if (h > 0 && m > 0) { - String hoursString = context.getResources().getQuantityString(R.plurals.hours_short, h, h); - String minutesString = context.getResources().getQuantityString(R.plurals.minutes_short, m, m); - snoozeDurationValue.setText(String.format("%s %s", hoursString, minutesString)); - } else if (h > 0) { - snoozeDurationValue.setText(context.getResources().getQuantityString(R.plurals.hours_short, h, h)); - } else if (snoozeDuration == ALARM_SNOOZE_DURATION_DISABLED) { - snoozeDurationValue.setText(context.getString(R.string.snooze_duration_none)); + if (SettingsDAO.isPerAlarmSnoozeDurationEnabled(mPrefs)) { + int snoozeDuration = alarm.snoozeDuration; + + int h = snoozeDuration / 60; + int m = snoozeDuration % 60; + + if (h > 0 && m > 0) { + String hoursString = context.getResources().getQuantityString(R.plurals.hours_short, h, h); + String minutesString = context.getResources().getQuantityString(R.plurals.minutes_short, m, m); + snoozeDurationValue.setText(String.format("%s %s", hoursString, minutesString)); + } else if (h > 0) { + snoozeDurationValue.setText(context.getResources().getQuantityString(R.plurals.hours_short, h, h)); + } else if (snoozeDuration == ALARM_SNOOZE_DURATION_DISABLED) { + snoozeDurationValue.setText(context.getString(R.string.snooze_duration_none)); + } else { + snoozeDurationValue.setText(context.getResources().getQuantityString(R.plurals.minutes_short, m, m)); + } + + snoozeDurationTitle.setTypeface(mGeneralTypeface); + snoozeDurationValue.setTypeface(mGeneralTypeface); + + snoozeDurationTitle.setVisibility(VISIBLE); + snoozeDurationValue.setVisibility(VISIBLE); } else { - snoozeDurationValue.setText(context.getResources().getQuantityString(R.plurals.minutes_short, m, m)); + snoozeDurationTitle.setVisibility(GONE); + snoozeDurationValue.setVisibility(GONE); + } + } + + private void bindMissedAlarmRepeatLimit(Context context, Alarm alarm) { + boolean isDeleteAfterUse = !alarm.daysOfWeek.isRepeating() && alarm.deleteAfterUse; + if (SettingsDAO.isPerAlarmMissedRepeatLimitEnabled(mPrefs) + && alarm.autoSilenceDuration != TIMEOUT_NEVER + && !isDeleteAfterUse) { + + int missedAlarmRepeatLimit = alarm.missedAlarmRepeatLimit; + switch (missedAlarmRepeatLimit) { + case 1 -> + missedAlarmRepeatLimitValue.setText(context.getString(R.string.missed_alarm_repeat_limit_1_time)); + case 3 -> + missedAlarmRepeatLimitValue.setText(context.getString(R.string.missed_alarm_repeat_limit_3_times)); + case 5 -> + missedAlarmRepeatLimitValue.setText(context.getString(R.string.missed_alarm_repeat_limit_5_times)); + case 10 -> + missedAlarmRepeatLimitValue.setText(context.getString(R.string.missed_alarm_repeat_limit_10_times)); + default -> missedAlarmRepeatLimitValue.setText(context.getString(R.string.label_never)); + } + + missedAlarmRepeatLimitTitle.setTypeface(mGeneralTypeface); + missedAlarmRepeatLimitValue.setTypeface(mGeneralTypeface); + + missedAlarmRepeatLimitTitle.setVisibility(VISIBLE); + missedAlarmRepeatLimitValue.setVisibility(VISIBLE); + } else { + missedAlarmRepeatLimitTitle.setVisibility(GONE); + missedAlarmRepeatLimitValue.setVisibility(GONE); } } private void bindCrescendoValue(Context context, Alarm alarm) { - int crescendoDuration = alarm.crescendoDuration; - - int m = crescendoDuration / 60; - int s = crescendoDuration % 60; - - if (m > 0 && s > 0) { - String minutesString = context.getResources().getQuantityString(R.plurals.minutes_short, m, m); - String secondsString = s + " " + context.getString(R.string.seconds_label); - crescendoDurationValue.setText(String.format("%s %s", minutesString, secondsString)); - } else if (m > 0) { - crescendoDurationValue.setText(context.getResources().getQuantityString(R.plurals.minutes_short, m, m)); - } else if (crescendoDuration == DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION) { - crescendoDurationValue.setText(context.getString(R.string.label_off)); + if (SettingsDAO.isPerAlarmCrescendoDurationEnabled(mPrefs)) { + int crescendoDuration = alarm.crescendoDuration; + + int m = crescendoDuration / 60; + int s = crescendoDuration % 60; + + if (m > 0 && s > 0) { + String minutesString = context.getResources().getQuantityString(R.plurals.minutes_short, m, m); + String secondsString = s + " " + context.getString(R.string.seconds_label); + crescendoDurationValue.setText(String.format("%s %s", minutesString, secondsString)); + } else if (m > 0) { + crescendoDurationValue.setText(context.getResources().getQuantityString(R.plurals.minutes_short, m, m)); + } else if (crescendoDuration == DEFAULT_VOLUME_CRESCENDO_DURATION) { + crescendoDurationValue.setText(context.getString(R.string.label_off)); + } else { + String secondsString = s + " " + context.getString(R.string.seconds_label); + crescendoDurationValue.setText(secondsString); + } + + crescendoDurationTitle.setTypeface(mGeneralTypeface); + crescendoDurationValue.setTypeface(mGeneralTypeface); + + crescendoDurationTitle.setVisibility(VISIBLE); + crescendoDurationValue.setVisibility(VISIBLE); } else { - String secondsString = s + " " + context.getString(R.string.seconds_label); - crescendoDurationValue.setText(secondsString); + crescendoDurationTitle.setVisibility(GONE); + crescendoDurationValue.setVisibility(GONE); } } private void bindAlarmVolume(Context context, Alarm alarm) { final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); final int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM); - final int currentVolume = alarm.alarmVolume; + final int currentVolume = Math.min(alarm.alarmVolume, maxVolume); if (SettingsDAO.isPerAlarmVolumeEnabled(mPrefs)) { - alarmVolumeTitle.setVisibility(VISIBLE); - alarmVolumeValue.setVisibility(VISIBLE); - int volumePercent = (int) (((float) currentVolume / maxVolume) * 100); String formatted = String.format(Locale.getDefault(), "%d%%", volumePercent); alarmVolumeValue.setText(formatted); - Drawable icon = ContextCompat.getDrawable(context, volumePercent < 50 + Drawable icon = AppCompatResources.getDrawable(context, volumePercent < 50 ? R.drawable.ic_volume_down : R.drawable.ic_volume_up); if (icon != null) { alarmVolumeTitle.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); } + + alarmVolumeTitle.setTypeface(mGeneralTypeface); + alarmVolumeValue.setTypeface(mGeneralTypeface); + + alarmVolumeTitle.setVisibility(VISIBLE); + alarmVolumeValue.setVisibility(VISIBLE); } else { alarmVolumeTitle.setVisibility(GONE); alarmVolumeValue.setVisibility(GONE); @@ -405,73 +519,50 @@ private void bindDaysOfWeekButtons(Alarm alarm, Context context) { dayButton.setTextColor(MaterialColors.getColor( context, com.google.android.material.R.attr.colorOnSurfaceInverse, Color.BLACK)); - selectedDate.setVisibility(GONE); } else { dayButton.setChecked(false); dayButton.setTextColor(MaterialColors.getColor( context, com.google.android.material.R.attr.colorSurfaceInverse, Color.BLACK)); - - selectedDate.setVisibility(VISIBLE); } + + dayButton.setTypeface(mGeneralTypeface); } } - private void bindScheduleAlarm(Alarm alarm) { - if (alarm.daysOfWeek.isRepeating()) { - scheduleAlarm.setVisibility(GONE); - } else { - scheduleAlarm.setVisibility(VISIBLE); - } + private void bindScheduleAlarm() { + scheduleAlarm.setTypeface(mGeneralTypeface); } private void bindSelectedDate(Alarm alarm) { - int year = alarm.year; - int month = alarm.month; - int dayOfMonth = alarm.day; - Calendar calendar = Calendar.getInstance(); - boolean isCurrentYear = year == calendar.get(Calendar.YEAR); - - calendar.set(year, month, dayOfMonth); - - String pattern = DateFormat.getBestDateTimePattern( - Locale.getDefault(), isCurrentYear ? "MMMMd" : "yyyyMMMMd"); - SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.getDefault()); - String formattedDate = dateFormat.format(calendar.getTime()); - if (alarm.daysOfWeek.isRepeating()) { - repeatDays.setVisibility(VISIBLE); - emptyView.setVisibility(GONE); - selectedDate.setVisibility(GONE); - addDate.setVisibility(GONE); - removeDate.setVisibility(GONE); - } else { - if (alarm.isSpecifiedDate()) { - if (alarm.isDateInThePast()) { - repeatDays.setVisibility(VISIBLE); - emptyView.setVisibility(GONE); - selectedDate.setVisibility(GONE); - addDate.setVisibility(VISIBLE); - removeDate.setVisibility(GONE); - } else { - repeatDays.setVisibility(GONE); - emptyView.setVisibility(VISIBLE); - selectedDate.setText(formattedDate); - addDate.setVisibility(GONE); - removeDate.setVisibility(VISIBLE); - } + clearSelectedDate(); + return; + } + + if (alarm.isSpecifiedDate()) { + if (alarm.isDateInThePast()) { + clearSelectedDate(); } else { - repeatDays.setVisibility(VISIBLE); - emptyView.setVisibility(GONE); - selectedDate.setVisibility(GONE); - addDate.setVisibility(VISIBLE); - removeDate.setVisibility(GONE); + selectedDate.setText(formatAlarmDate(alarm)); + selectedDate.setTypeface(mGeneralTypeface); + addDate.setVisibility(GONE); + removeDate.setVisibility(VISIBLE); } + } else { + clearSelectedDate(); } } + private void clearSelectedDate() { + selectedDate.setText(null); + addDate.setVisibility(VISIBLE); + removeDate.setVisibility(GONE); + } + private void bindRingtone(Context context, Alarm alarm) { final String title = DataModel.getDataModel().getRingtoneTitle(alarm.alert); ringtone.setText(title); + ringtone.setTypeface(mGeneralTypeface); final String description = context.getString(R.string.ringtone_description); ringtone.setContentDescription(description + " " + title); @@ -489,12 +580,41 @@ private void bindRingtone(Context context, Alarm alarm) { ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(iconRingtone, null, null, null); } - private void bindVibrator(Alarm alarm) { + private void bindVibrator(Context context, Alarm alarm) { if (mHasVibrator) { + vibrate.setTypeface(mGeneralTypeface); vibrate.setVisibility(VISIBLE); vibrate.setChecked(alarm.vibrate); + + if (alarm.vibrate && SettingsDAO.isPerAlarmVibrationPatternEnabled(mPrefs)) { + vibrationPatternTitle.setTypeface(mGeneralTypeface); + vibrationPatternValue.setTypeface(mGeneralTypeface); + + String vibrationPatternText = alarm.vibrationPattern; + switch (vibrationPatternText) { + case VIBRATION_PATTERN_SOFT -> + vibrationPatternValue.setText(context.getString(R.string.vibration_pattern_soft)); + case VIBRATION_PATTERN_STRONG -> + vibrationPatternValue.setText(context.getString(R.string.vibration_pattern_strong)); + case VIBRATION_PATTERN_HEARTBEAT -> + vibrationPatternValue.setText(context.getString(R.string.vibration_pattern_heartbeat)); + case VIBRATION_PATTERN_ESCALATING -> + vibrationPatternValue.setText(context.getString(R.string.vibration_pattern_escalating)); + case VIBRATION_PATTERN_TICK_TOCK -> + vibrationPatternValue.setText(context.getString(R.string.vibration_pattern_tick_tock)); + default -> vibrationPatternValue.setText(context.getString(R.string.label_default)); + } + + vibrationPatternTitle.setVisibility(VISIBLE); + vibrationPatternValue.setVisibility(VISIBLE); + } else { + vibrationPatternTitle.setVisibility(GONE); + vibrationPatternValue.setVisibility(GONE); + } } else { vibrate.setVisibility(GONE); + vibrationPatternTitle.setVisibility(GONE); + vibrationPatternValue.setVisibility(GONE); } } @@ -502,6 +622,7 @@ private void bindFlash(Alarm alarm) { if (mHasFlash) { flash.setVisibility(VISIBLE); flash.setChecked(alarm.flash); + flash.setTypeface(mGeneralTypeface); } else { flash.setVisibility(GONE); } @@ -513,9 +634,15 @@ private void bindDeleteOccasionalAlarmAfterUse(Alarm alarm) { } else { deleteOccasionalAlarmAfterUse.setVisibility(VISIBLE); deleteOccasionalAlarmAfterUse.setChecked(alarm.deleteAfterUse); + deleteOccasionalAlarmAfterUse.setTypeface(mGeneralTypeface); } } + private void bindDeleteAndDuplicateButtons() { + delete.setTypeface(mGeneralBoldTypeface); + duplicate.setTypeface(mGeneralBoldTypeface); + } + private void bindEditLabelAnnotations(Alarm alarm) { final boolean labelIsEmpty = alarm.label == null || alarm.label.isEmpty(); final float labelAlpha = labelIsEmpty ? 1f : editLabel.getAlpha(); @@ -559,6 +686,23 @@ public void onAnimationEnd(Animator animator) { } } + + private void bindHolidayOption(Context context, Alarm alarm) { + holidayOption.setVisibility(VISIBLE); + holidayOption.setTypeface(mGeneralTypeface); + switch (alarm.holidayOption) { + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_SKIP_HOLIDAY -> + holidayOption.setText(context.getString(R.string.holiday_option_skip_holiday)); + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_BIG_SMALL_DA -> + holidayOption.setText(context.getString(R.string.holiday_option_big_small_da)); + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_BIG_SMALL_XIAO -> + holidayOption.setText(context.getString(R.string.holiday_option_big_small_xiao)); + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_SINGLE_DAY_OFF -> + holidayOption.setText(context.getString(R.string.holiday_option_single_day_off)); + default -> holidayOption.setText(context.getString(R.string.holiday_option_none)); + } + } + @Override public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder, long duration) { if (!(oldHolder instanceof AlarmItemViewHolder) || !(newHolder instanceof AlarmItemViewHolder)) { @@ -633,6 +777,12 @@ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long du final Animator vibrateAnimation = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f) .setDuration(shortDuration); + final Animator vibrationPatternTitleAnimation = ObjectAnimator.ofFloat( + vibrationPatternTitle, View.ALPHA, 0f).setDuration(shortDuration); + + final Animator vibrationPatternValueAnimation = ObjectAnimator.ofFloat( + vibrationPatternValue, View.ALPHA, 0f).setDuration(shortDuration); + final Animator flashAnimation = ObjectAnimator.ofFloat(flash, View.ALPHA, 0f) .setDuration(shortDuration); @@ -651,18 +801,24 @@ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long du final Animator snoozeDurationValueAnimation = ObjectAnimator.ofFloat( snoozeDurationValue, View.ALPHA, 0f).setDuration(shortDuration); + final Animator missedAlarmRepeatLimitTitleAnimation = ObjectAnimator.ofFloat( + missedAlarmRepeatLimitTitle, View.ALPHA, 0f).setDuration(shortDuration); + + final Animator missedAlarmRepeatLimitValueAnimation = ObjectAnimator.ofFloat( + missedAlarmRepeatLimitValue, View.ALPHA, 0f).setDuration(shortDuration); + final Animator crescendoDurationTitleAnimation = ObjectAnimator.ofFloat( crescendoDurationTitle, View.ALPHA, 0f).setDuration(shortDuration); + final Animator crescendoDurationValueAnimation = ObjectAnimator.ofFloat( + crescendoDurationValue, View.ALPHA, 0f).setDuration(shortDuration); + final Animator alarmVolumeTitleAnimation = ObjectAnimator.ofFloat( alarmVolumeTitle, View.ALPHA, 0f).setDuration(shortDuration); final Animator alarmVolumeValueAnimation = ObjectAnimator.ofFloat( alarmVolumeValue, View.ALPHA, 0f).setDuration(shortDuration); - final Animator crescendoDurationValueAnimation = ObjectAnimator.ofFloat( - crescendoDurationValue, View.ALPHA, 0f).setDuration(shortDuration); - final Animator dismissAnimation = ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 0f).setDuration(shortDuration); @@ -679,9 +835,14 @@ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long du final int numberOfItems = countNumberOfItems(); final long delayIncrement = (long) (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER) / (numberOfItems - 1); final boolean vibrateVisible = vibrate.getVisibility() == VISIBLE; + final boolean vibrationPatternVisible = vibrationPatternTitle.getVisibility() == VISIBLE; final boolean flashVisible = flash.getVisibility() == VISIBLE; final boolean deleteOccasionalAlarmAfterUseVisible = deleteOccasionalAlarmAfterUse.getVisibility() == VISIBLE; - final boolean isAlarmVolumeTitleVisible = alarmVolumeTitle.getVisibility() == VISIBLE; + final boolean autoSilenceDurationTitleVisible = autoSilenceDurationTitle.getVisibility() == VISIBLE; + final boolean snoozeDurationTitleVisible = snoozeDurationTitle.getVisibility() == VISIBLE; + final boolean missedAlarmRepeatLimitTitleVisible = missedAlarmRepeatLimitTitle.getVisibility() == VISIBLE; + final boolean crescendoDurationTitleVisible = crescendoDurationTitle.getVisibility() == VISIBLE; + final boolean alarmVolumeTitleVisible = alarmVolumeTitle.getVisibility() == VISIBLE; final boolean preemptiveDismissButtonVisible = preemptiveDismissButton.getVisibility() == VISIBLE; editLabelIconAnimation.setStartDelay(startDelay); @@ -697,23 +858,35 @@ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long du dismissAnimation.setStartDelay(startDelay); } - if (isAlarmVolumeTitleVisible) { + if (alarmVolumeTitleVisible) { startDelay += delayIncrement; alarmVolumeTitleAnimation.setStartDelay(startDelay); alarmVolumeValueAnimation.setStartDelay(startDelay); } - crescendoDurationTitleAnimation.setStartDelay(startDelay); - - crescendoDurationValueAnimation.setStartDelay(startDelay); - - snoozeDurationTitleAnimation.setStartDelay(startDelay); + if (crescendoDurationTitleVisible) { + startDelay += delayIncrement; + crescendoDurationTitleAnimation.setStartDelay(startDelay); + crescendoDurationValueAnimation.setStartDelay(startDelay); + } - snoozeDurationValueAnimation.setStartDelay(startDelay); + if (missedAlarmRepeatLimitTitleVisible) { + startDelay += delayIncrement; + missedAlarmRepeatLimitTitleAnimation.setStartDelay(startDelay); + missedAlarmRepeatLimitValueAnimation.setStartDelay(startDelay); + } - silenceAfterDurationTitleAnimation.setStartDelay(startDelay); + if (snoozeDurationTitleVisible) { + startDelay += delayIncrement; + snoozeDurationTitleAnimation.setStartDelay(startDelay); + snoozeDurationValueAnimation.setStartDelay(startDelay); + } - silenceAfterDurationTitleAnimation.setStartDelay(startDelay); + if (autoSilenceDurationTitleVisible) { + startDelay += delayIncrement; + silenceAfterDurationTitleAnimation.setStartDelay(startDelay); + silenceAfterDurationTitleAnimation.setStartDelay(startDelay); + } if (deleteOccasionalAlarmAfterUseVisible) { startDelay += delayIncrement; @@ -725,6 +898,12 @@ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long du flashAnimation.setStartDelay(startDelay); } + if (vibrationPatternVisible) { + startDelay += delayIncrement; + vibrationPatternTitleAnimation.setStartDelay(startDelay); + vibrationPatternValueAnimation.setStartDelay(startDelay); + } + if (vibrateVisible) { startDelay += delayIncrement; vibrateAnimation.setStartDelay(startDelay); @@ -751,8 +930,9 @@ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long du addDateAnimation, removeDateAnimation, snoozeDurationTitleAnimation, snoozeDurationValueAnimation, crescendoDurationTitleAnimation, crescendoDurationValueAnimation, silenceAfterDurationTitleAnimation, - silenceAfterDurationValueAnimation, alarmVolumeTitleAnimation, - alarmVolumeValueAnimation); + silenceAfterDurationValueAnimation, missedAlarmRepeatLimitTitleAnimation, + missedAlarmRepeatLimitValueAnimation, alarmVolumeTitleAnimation, + alarmVolumeValueAnimation, vibrationPatternTitleAnimation, vibrationPatternValueAnimation); animatorSet.addListener(new AnimatorListenerAdapter() { @@ -790,12 +970,16 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur ringtone.setAlpha(0f); preemptiveDismissButton.setAlpha(0f); vibrate.setAlpha(0f); + vibrationPatternTitle.setAlpha(0f); + vibrationPatternValue.setAlpha(0f); flash.setAlpha(0f); deleteOccasionalAlarmAfterUse.setAlpha(0f); autoSilenceDurationTitle.setAlpha(0f); autoSilenceDurationValue.setAlpha(0f); snoozeDurationTitle.setAlpha(0f); snoozeDurationValue.setAlpha(0f); + missedAlarmRepeatLimitTitle.setAlpha(0f); + missedAlarmRepeatLimitValue.setAlpha(0f); crescendoDurationTitle.setAlpha(0f); crescendoDurationValue.setAlpha(0f); alarmVolumeTitle.setAlpha(0f); @@ -843,6 +1027,12 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur final Animator vibrateAnimation = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f) .setDuration(longDuration); + final Animator vibrationPatternTitleAnimation = ObjectAnimator.ofFloat( + vibrationPatternTitle, View.ALPHA, 1f).setDuration(longDuration); + + final Animator vibrationPatternValueAnimation = ObjectAnimator.ofFloat( + vibrationPatternValue, View.ALPHA, 1f).setDuration(longDuration); + final Animator flashAnimation = ObjectAnimator.ofFloat(flash, View.ALPHA, 1f) .setDuration(longDuration); @@ -861,6 +1051,12 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur final Animator snoozeDurationValueAnimation = ObjectAnimator.ofFloat( snoozeDurationValue, View.ALPHA, 1f).setDuration(longDuration); + final Animator missedAlarmRepeatLimitTitleAnimation = ObjectAnimator.ofFloat( + missedAlarmRepeatLimitTitle, View.ALPHA, 1f).setDuration(longDuration); + + final Animator missedAlarmRepeatLimitValueAnimation = ObjectAnimator.ofFloat( + missedAlarmRepeatLimitValue, View.ALPHA, 1f).setDuration(longDuration); + final Animator crescendoDurationTitleAnimation = ObjectAnimator.ofFloat( crescendoDurationTitle, View.ALPHA, 1f).setDuration(longDuration); @@ -893,9 +1089,14 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur final int numberOfItems = countNumberOfItems(); final long delayIncrement = (long) (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER) / (numberOfItems - 1); final boolean vibrateVisible = vibrate.getVisibility() == VISIBLE; + final boolean vibrationPatternVisible = vibrationPatternTitle.getVisibility() == VISIBLE; final boolean flashVisible = flash.getVisibility() == VISIBLE; final boolean deleteOccasionalAlarmAfterUseVisible = deleteOccasionalAlarmAfterUse.getVisibility() == VISIBLE; - final boolean isAlarmVolumeTitleVisible = alarmVolumeTitle.getVisibility() == VISIBLE; + final boolean autoSilenceDurationTitleVisible = autoSilenceDurationTitle.getVisibility() == VISIBLE; + final boolean snoozeDurationTitleVisible = snoozeDurationTitle.getVisibility() == VISIBLE; + final boolean missedAlarmRepeatLimitTitleVisible = missedAlarmRepeatLimitTitle.getVisibility() == VISIBLE; + final boolean crescendoDurationTitleVisible = crescendoDurationTitle.getVisibility() == VISIBLE; + final boolean alarmVolumeTitleVisible = alarmVolumeTitle.getVisibility() == VISIBLE; final boolean preemptiveDismissButtonVisible = preemptiveDismissButton.getVisibility() == VISIBLE; editLabelIconAnimation.setStartDelay(startDelay); @@ -919,6 +1120,12 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur startDelay += delayIncrement; } + if (vibrationPatternVisible) { + vibrationPatternTitleAnimation.setStartDelay(startDelay); + vibrationPatternValueAnimation.setStartDelay(startDelay); + startDelay += delayIncrement; + } + if (flashVisible) { flashAnimation.setStartDelay(startDelay); startDelay += delayIncrement; @@ -929,19 +1136,31 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur startDelay += delayIncrement; } - silenceAfterDurationTitleAnimation.setStartDelay(startDelay); - - silenceAfterDurationValueAnimation.setStartDelay(startDelay); - - snoozeDurationTitleAnimation.setStartDelay(startDelay); + if (autoSilenceDurationTitleVisible) { + silenceAfterDurationTitleAnimation.setStartDelay(startDelay); + silenceAfterDurationValueAnimation.setStartDelay(startDelay); + startDelay += delayIncrement; + } - snoozeDurationValueAnimation.setStartDelay(startDelay); + if (snoozeDurationTitleVisible) { + snoozeDurationTitleAnimation.setStartDelay(startDelay); + snoozeDurationValueAnimation.setStartDelay(startDelay); + startDelay += delayIncrement; + } - crescendoDurationTitleAnimation.setStartDelay(startDelay); + if (missedAlarmRepeatLimitTitleVisible) { + missedAlarmRepeatLimitTitleAnimation.setStartDelay(startDelay); + missedAlarmRepeatLimitValueAnimation.setStartDelay(startDelay); + startDelay += delayIncrement; + } - crescendoDurationValueAnimation.setStartDelay(startDelay); + if (crescendoDurationTitleVisible) { + crescendoDurationTitleAnimation.setStartDelay(startDelay); + crescendoDurationValueAnimation.setStartDelay(startDelay); + startDelay += delayIncrement; + } - if (isAlarmVolumeTitleVisible) { + if (alarmVolumeTitleVisible) { alarmVolumeTitleAnimation.setStartDelay(startDelay); alarmVolumeValueAnimation.setStartDelay(startDelay); startDelay += delayIncrement; @@ -965,7 +1184,9 @@ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long dur snoozeDurationTitleAnimation, snoozeDurationValueAnimation, crescendoDurationTitleAnimation, crescendoDurationValueAnimation, silenceAfterDurationTitleAnimation, silenceAfterDurationValueAnimation, - alarmVolumeTitleAnimation, alarmVolumeValueAnimation); + missedAlarmRepeatLimitTitleAnimation, missedAlarmRepeatLimitValueAnimation, + alarmVolumeTitleAnimation, alarmVolumeValueAnimation, vibrationPatternTitleAnimation, + vibrationPatternValueAnimation); animatorSet.addListener(new AnimatorListenerAdapter() { @@ -993,6 +1214,14 @@ private int countNumberOfItems() { numberOfItems++; } + if (vibrationPatternTitle.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (vibrationPatternValue.getVisibility() == VISIBLE) { + numberOfItems++; + } + if (flash.getVisibility() == VISIBLE) { numberOfItems++; } @@ -1001,6 +1230,38 @@ private int countNumberOfItems() { numberOfItems++; } + if (autoSilenceDurationTitle.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (autoSilenceDurationValue.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (snoozeDurationTitle.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (snoozeDurationValue.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (missedAlarmRepeatLimitTitle.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (missedAlarmRepeatLimitValue.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (crescendoDurationTitle.getVisibility() == VISIBLE) { + numberOfItems++; + } + + if (crescendoDurationValue.getVisibility() == VISIBLE) { + numberOfItems++; + } + if (alarmVolumeTitle.getVisibility() == VISIBLE) { numberOfItems++; } @@ -1017,10 +1278,13 @@ public static class Factory implements ItemAdapter.ItemViewHolder.Factory { private final LayoutInflater mLayoutInflater; private final boolean mHasVibrator; private final boolean mHasFlash; + private final SharedPreferences mPrefs; + private final Typeface mGeneralTypeface; + private final Typeface mGeneralBoldTypeface; public Factory(Context context) { mLayoutInflater = LayoutInflater.from(context); - mHasVibrator = Utils.hasVibrator(context); + mHasVibrator = DeviceUtils.hasVibrator(context); mHasFlash = AlarmUtils.hasBackFlash(context); } diff --git a/app/src/main/java/com/best/deskclock/data/SettingsDAO.java b/app/src/main/java/com/best/deskclock/data/SettingsDAO.java index 05a4bb2cc..06fe46ae9 100644 --- a/app/src/main/java/com/best/deskclock/data/SettingsDAO.java +++ b/app/src/main/java/com/best/deskclock/data/SettingsDAO.java @@ -65,11 +65,6 @@ public final class SettingsDAO { */ private static final String KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id"; - /** - * Key to a preference that indicates whether restore (of backup and restore) has completed. - */ - private static final String KEY_RESTORE_BACKUP_FINISHED = "restore_finished"; - /** * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones */ @@ -103,6 +98,22 @@ static void toggleCitySort(SharedPreferences prefs) { prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal()).apply(); } + /** + * @return sorting of cities by time zone in ascending order, by name or manually. + */ + public static String getCitySorting(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getString(KEY_SORT_CITIES, DEFAULT_SORT_CITIES_BY_ASCENDING_TIME_ZONE); + } + + /** + * @return {@code true} if if a note can be added to the cities; {@code false} otherwise. + */ + public static boolean isCityNoteEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getBoolean(KEY_ENABLE_CITY_NOTE, DEFAULT_ENABLE_CITY_NOTE); + } + /** * @return {@code true} if a clock for the user's home timezone should be automatically * displayed when it doesn't match the current timezone @@ -132,7 +143,7 @@ public static boolean getShowHomeClock(Context context, SharedPreferences prefs) /** * @return the user's home timezone */ - static TimeZone getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ) { + public static TimeZone getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ) { String timeZoneId = prefs.getString(KEY_HOME_TIME_ZONE, DEFAULT_HOME_TIME_ZONE); // If the recorded home timezone is legal, use it. @@ -167,6 +178,53 @@ public static ClockStyle getClockStyle(SharedPreferences prefs) { return getClockStyle(prefs, KEY_CLOCK_STYLE); } + /** + * @return the clock dial applied in the Clock tab. + */ + public static String getClockDial(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getString(KEY_CLOCK_DIAL, DEFAULT_CLOCK_DIAL); + } + + /** + * @return the material clock dial applied in the Clock tab. + */ + public static String getClockDialMaterial(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getString(KEY_CLOCK_DIAL_MATERIAL, DEFAULT_CLOCK_DIAL_MATERIAL); + } + + /** + * @return the analog clock size applied in the Clock tab. + */ + public static int getAnalogClockSize(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getInt(KEY_ANALOG_CLOCK_SIZE, DEFAULT_ANALOG_CLOCK_SIZE); + } + + /** + * @return the clock second hand applied in the Clock tab. + */ + public static String getClockSecondHand(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getString(KEY_CLOCK_SECOND_HAND, DEFAULT_CLOCK_SECOND_HAND); + } + + /** + * @return the font applied to the digital clock in the Clock tab. + */ + public static String getDigitalClockFont(SharedPreferences prefs) { + return prefs.getString(KEY_DIGITAL_CLOCK_FONT, null); + } + + /** + * @return the font size applied in the Clock tab. + */ + public static int getDigitalClockFontSize(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getInt(KEY_DIGITAL_CLOCK_FONT_SIZE, DEFAULT_DIGITAL_CLOCK_FONT_SIZE); + } + /** * @return the theme applied. */ @@ -207,6 +265,13 @@ public static String getDarkMode(SharedPreferences prefs) { return prefs.getString(KEY_DARK_MODE, DEFAULT_DARK_MODE); } + /** + * @return the font applied to the app. + */ + public static String getGeneralFont(SharedPreferences prefs) { + return prefs.getString(KEY_GENERAL_FONT, null); + } + /** * @return {@code true} if the background should be displayed in a view. {@code false} otherwise. */ @@ -302,6 +367,43 @@ public static ClockStyle getScreensaverClockStyle(SharedPreferences prefs) { return getClockStyle(prefs, KEY_SCREENSAVER_CLOCK_STYLE); } + /** + * @return the clock dial applied for the screensaver. + */ + public static String getScreensaverClockDial(SharedPreferences prefs) { + return prefs.getString(KEY_SCREENSAVER_CLOCK_DIAL, DEFAULT_CLOCK_DIAL); + } + + /** + * @return the material clock dial applied for the screensaver. + */ + public static String getScreensaverClockDialMaterial(SharedPreferences prefs) { + return prefs.getString(KEY_SCREENSAVER_CLOCK_DIAL_MATERIAL, DEFAULT_CLOCK_DIAL_MATERIAL); + } + + /** + * @return the font applied to the digital clock in the screensaver. + */ + public static String getScreensaverDigitalClockFont(SharedPreferences prefs) { + return prefs.getString(KEY_SCREENSAVER_DIGITAL_CLOCK_FONT, null); + } + + /** + * @return the clock second hand applied for the screensaver. + */ + public static String getScreensaverClockSecondHand(SharedPreferences prefs) { + return prefs.getString(KEY_SCREENSAVER_CLOCK_SECOND_HAND, DEFAULT_CLOCK_SECOND_HAND); + } + + /** + * @return {@code true} if the screensaver battery level is displayed. + * {@code false} otherwise. + */ + public static boolean isScreensaverBatteryDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_clock.xml + return prefs.getBoolean(KEY_DISPLAY_SCREENSAVER_BATTERY, DEFAULT_DISPLAY_SCREENSAVER_BATTERY); + } + /** * @return {@code true} if dynamic colors are applied to analog or digital clock. * {@code false} otherwise. @@ -312,7 +414,7 @@ public static boolean areScreensaverClockDynamicColors(SharedPreferences prefs) } /** - * @return a value indicating the color of the clock of the screensaver + * @return a value indicating the screensaver clock color. */ public static int getScreensaverClockColorPicker(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml @@ -320,7 +422,15 @@ public static int getScreensaverClockColorPicker(SharedPreferences prefs) { } /** - * @return a value indicating the color of the date of the screensaver + * @return a value indicating the screensaver battery level color. + */ + public static int getScreensaverBatteryColorPicker(SharedPreferences prefs) { + // Default value must match the one in res/xml/screensaver_settings.xml + return prefs.getInt(KEY_SCREENSAVER_BATTERY_COLOR_PICKER, DEFAULT_SCREENSAVER_CUSTOM_COLOR); + } + + /** + * @return a value indicating the screensaver date color. */ public static int getScreensaverDateColorPicker(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml @@ -328,7 +438,7 @@ public static int getScreensaverDateColorPicker(SharedPreferences prefs) { } /** - * @return a value indicating the color of the next alarm of the screensaver + * @return a value indicating the screensaver next alarm color. */ public static int getScreensaverNextAlarmColorPicker(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml @@ -343,6 +453,13 @@ public static int getScreensaverBrightness(SharedPreferences prefs) { return prefs.getInt(KEY_SCREENSAVER_BRIGHTNESS, DEFAULT_SCREENSAVER_BRIGHTNESS); } + /** + * @return the analog clock size applied to the screensaver. + */ + public static int getScreensaverAnalogClockSize(SharedPreferences prefs) { + return prefs.getInt(KEY_SCREENSAVER_ANALOG_CLOCK_SIZE, DEFAULT_ANALOG_CLOCK_SIZE); + } + /** * @return {@code true} if the seconds are displayed on the analog or digital clock in the screensaver. * {@code false} otherwise. @@ -352,12 +469,20 @@ public static boolean areScreensaverClockSecondsDisplayed(SharedPreferences pref return prefs.getBoolean(KEY_DISPLAY_SCREENSAVER_CLOCK_SECONDS, DEFAULT_DISPLAY_SCREENSAVER_CLOCK_SECONDS); } + /** + * @return the font size applied to the alarm digital clock. + */ + public static int getScreensaverDigitalClockFontSize(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SCREENSAVER_DIGITAL_CLOCK_FONT_SIZE, DEFAULT_DIGITAL_CLOCK_FONT_SIZE); + } + /** * @return {@code true} if the screensaver should show the clock in bold. {@code false} otherwise. */ public static boolean isScreensaverDigitalClockInBold(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml - return prefs.getBoolean(KEY_SCREENSAVER_DIGITAL_CLOCK_IN_BOLD, DEFAULT_SCREENSAVER_DIGITAL_CLOCK_IN_BOLD); + return prefs.getBoolean(KEY_SCREENSAVER_DIGITAL_CLOCK_IN_BOLD, DEFAULT_SCREENSAVER_FORMATTING); } /** @@ -365,7 +490,25 @@ public static boolean isScreensaverDigitalClockInBold(SharedPreferences prefs) { */ public static boolean isScreensaverDigitalClockInItalic(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml - return prefs.getBoolean(KEY_SCREENSAVER_DIGITAL_CLOCK_IN_ITALIC, DEFAULT_SCREENSAVER_DIGITAL_CLOCK_IN_ITALIC); + return prefs.getBoolean(KEY_SCREENSAVER_DIGITAL_CLOCK_IN_ITALIC, DEFAULT_SCREENSAVER_FORMATTING); + } + + /** + * @return {@code true} if the screensaver should show the battery level in bold. + * {@code false} otherwise. + */ + public static boolean isScreensaverBatteryInBold(SharedPreferences prefs) { + // Default value must match the one in res/xml/screensaver_settings.xml + return prefs.getBoolean(KEY_SCREENSAVER_BATTERY_IN_BOLD, DEFAULT_SCREENSAVER_FORMATTING); + } + + /** + * @return {@code true} if the screensaver should show the battery level in italic. + * {@code false} otherwise. + */ + public static boolean isScreensaverBatteryInItalic(SharedPreferences prefs) { + // Default value must match the one in res/xml/screensaver_settings.xml + return prefs.getBoolean(KEY_SCREENSAVER_BATTERY_IN_ITALIC, DEFAULT_SCREENSAVER_FORMATTING); } /** @@ -373,15 +516,15 @@ public static boolean isScreensaverDigitalClockInItalic(SharedPreferences prefs) */ public static boolean isScreensaverDateInBold(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml - return prefs.getBoolean(KEY_SCREENSAVER_DATE_IN_BOLD, DEFAULT_SCREENSAVER_DATE_IN_BOLD); + return prefs.getBoolean(KEY_SCREENSAVER_DATE_IN_BOLD, DEFAULT_SCREENSAVER_FORMATTING); } /** - * @return {@code true} if the screensaver should show the date in italic. {@code false} otherwise. + * @return {@code true} if the screensaver should show the date in italics. {@code false} otherwise. */ public static boolean isScreensaverDateInItalic(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml - return prefs.getBoolean(KEY_SCREENSAVER_DATE_IN_ITALIC, DEFAULT_SCREENSAVER_DATE_IN_ITALIC); + return prefs.getBoolean(KEY_SCREENSAVER_DATE_IN_ITALIC, DEFAULT_SCREENSAVER_FORMATTING); } /** @@ -389,7 +532,7 @@ public static boolean isScreensaverDateInItalic(SharedPreferences prefs) { */ public static boolean isScreensaverNextAlarmInBold(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml - return prefs.getBoolean(KEY_SCREENSAVER_NEXT_ALARM_IN_BOLD, DEFAULT_SCREENSAVER_NEXT_ALARM_IN_BOLD); + return prefs.getBoolean(KEY_SCREENSAVER_NEXT_ALARM_IN_BOLD, DEFAULT_SCREENSAVER_FORMATTING); } /** @@ -397,7 +540,31 @@ public static boolean isScreensaverNextAlarmInBold(SharedPreferences prefs) { */ public static boolean isScreensaverNextAlarmInItalic(SharedPreferences prefs) { // Default value must match the one in res/xml/screensaver_settings.xml - return prefs.getBoolean(KEY_SCREENSAVER_NEXT_ALARM_IN_ITALIC, DEFAULT_SCREENSAVER_NEXT_ALARM_IN_ITALIC); + return prefs.getBoolean(KEY_SCREENSAVER_NEXT_ALARM_IN_ITALIC, DEFAULT_SCREENSAVER_FORMATTING); + } + + /** + * @return the URI of the image to be displayed in the screensaver. + */ + public static String getScreensaverBackgroundImage(SharedPreferences prefs) { + return prefs.getString(KEY_SCREENSAVER_BACKGROUND_IMAGE, null); + } + + /** + * @return {@code true} if a blur effect should be applied to the screensaver image. + * {@code false} otherwise. + */ + public static boolean isScreensaverBlurEffectEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getBoolean(KEY_ENABLE_SCREENSAVER_BLUR_EFFECT, DEFAULT_ENABLE_BLUR_EFFECT); + } + + /** + * @return the blur intensity applied to the screensaver image. + */ + public static int getScreensaverBlurIntensity(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SCREENSAVER_BLUR_INTENSITY, DEFAULT_BLUR_INTENSITY); } /** @@ -412,10 +579,8 @@ static Uri getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri) { /** * @return the duration for which a timer can ring before expiring and being reset. */ - static long getTimerAutoSilenceDuration(SharedPreferences prefs) { - // Default value must match the one in res/xml/settings_timer.xml - final String string = prefs.getString(KEY_TIMER_AUTO_SILENCE, DEFAULT_TIMER_AUTO_SILENCE); - return Long.parseLong(string); + static int getTimerAutoSilenceDuration(SharedPreferences prefs) { + return prefs.getInt(KEY_TIMER_AUTO_SILENCE_DURATION, DEFAULT_TIMER_AUTO_SILENCE_DURATION); } /** @@ -426,6 +591,72 @@ public static boolean isTimerVibrate(SharedPreferences prefs) { return prefs.getBoolean(KEY_TIMER_VIBRATE, DEFAULT_TIMER_VIBRATE); } + /** + * @return {@code true} if the ringtone title should be displayed on the lock screen + * when the timer is expired. {@code false} otherwise. + */ + public static boolean isTimerRingtoneTitleDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer.xml + return prefs.getBoolean(KEY_DISPLAY_TIMER_RINGTONE_TITLE, DEFAULT_DISPLAY_RINGTONE_TITLE); + } + + /** + * @return a value indicating the timer ringtone title color. + */ + public static int getTimerRingtoneTitleColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer.xml + return prefs.getInt(KEY_TIMER_RINGTONE_TITLE_COLOR, DEFAULT_TIMER_RINGTONE_TITLE_COLOR); + } + + /** + * @return {@code true} if a shadow is displayed on the texts of the expired timer. + * {@code false} otherwise. + */ + public static boolean isTimerTextShadowDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer.xml + return prefs.getBoolean(KEY_TIMER_DISPLAY_TEXT_SHADOW, DEFAULT_DISPLAY_TEXT_SHADOW); + } + + /** + * @return a value indicating the shadow color displayed on the expired timer texts. + */ + public static int getTimerShadowColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getInt(KEY_TIMER_SHADOW_COLOR, DEFAULT_TIMER_SHADOW_COLOR); + } + + /** + * @return a value indicating the shadow offset for the expired timer texts. + */ + public static int getTimerShadowOffset(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getInt(KEY_TIMER_SHADOW_OFFSET, DEFAULT_SHADOW_OFFSET); + } + + /** + * @return the URI of the image to be displayed on the lock screen when the timer is expired. + */ + public static String getTimerBackgroundImage(SharedPreferences prefs) { + return prefs.getString(KEY_TIMER_BACKGROUND_IMAGE, null); + } + + /** + * @return {@code true} if a blur effect should be applied to the image when the timer is expired. + * {@code false} otherwise. + */ + public static boolean isTimerBlurEffectEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getBoolean(KEY_ENABLE_TIMER_BLUR_EFFECT, DEFAULT_ENABLE_BLUR_EFFECT); + } + + /** + * @return the blur intensity applied to the image when the timer is expired. + */ + public static int getTimerBlurIntensity(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getInt(KEY_TIMER_BLUR_INTENSITY, DEFAULT_BLUR_INTENSITY); + } + /** * @return {@code true} if the expired timer is reset with the volume buttons. {@code false} otherwise. */ @@ -475,12 +706,10 @@ public static String getTimerSortingPreference(SharedPreferences prefs) { } /** - * @return the default minutes in seconds to add to timer when the "Add Minute" button is clicked. + * @return the default duration in seconds to add to timer when the "Add Minute" button is clicked. */ public static int getDefaultTimeToAddToTimer(SharedPreferences prefs) { - // Default value must match the one in res/xml/settings_timer.xml - final String string = prefs.getString(KEY_DEFAULT_TIME_TO_ADD_TO_TIMER, DEFAULT_TIME_TO_ADD_TO_TIMER); - return Integer.parseInt(string) * 60; + return prefs.getInt(KEY_TIMER_ADD_TIME_BUTTON_VALUE, DEFAULT_TIMER_ADD_TIME_BUTTON_VALUE); } /** @@ -491,14 +720,62 @@ public static String getTimerCreationViewStyle(SharedPreferences prefs) { return prefs.getString(KEY_TIMER_CREATION_VIEW_STYLE, DEFAULT_TIMER_CREATION_VIEW_STYLE); } + /** + * @return the font applied to the timer duration. + */ + public static String getTimerDurationFont(SharedPreferences prefs) { + return prefs.getString(KEY_TIMER_DURATION_FONT, null); + } + + /** + * @return {@code true} if active timers should remain compact. {@code false} otherwise. + */ + public static boolean isCompactTimersDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getBoolean(KEY_DISPLAY_COMPACT_TIMERS, DEFAULT_DISPLAY_COMPACT_TIMERS); + } + /** * @return {@code true} if the timer background must be transparent. {@code false} otherwise. */ public static boolean isTimerBackgroundTransparent(SharedPreferences prefs) { - // Default value must match the one in res/xml/settings_timer.xml + // Default value must match the one in res/xml/settings_timer_display.xml return prefs.getBoolean(KEY_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER, DEFAULT_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER); } + /** + * @return {@code true} if the vertical scrollbar is displayed in the timer list. + * {@code false} otherwise. + */ + public static boolean isTimerStateIndicatorDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getBoolean(KEY_DISPLAY_TIMER_STATE_INDICATOR, DEFAULT_DISPLAY_TIMER_STATE_INDICATOR); + } + + /** + * @return a value indicating the running timer indicator color. + */ + public static int getRunningTimerIndicatorColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getInt(KEY_RUNNING_TIMER_INDICATOR_COLOR, DEFAULT_RUNNING_TIMER_INDICATOR_COLOR); + } + + /** + * @return a value indicating the paused timer indicator color. + */ + public static int getPausedTimerIndicatorColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getInt(KEY_PAUSED_TIMER_INDICATOR_COLOR, DEFAULT_PAUSED_TIMER_INDICATOR_COLOR); + } + + /** + * @return a value indicating the expired timer indicator color. + */ + public static int getExpiredTimerIndicatorColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer_display.xml + return prefs.getInt(KEY_EXPIRED_TIMER_INDICATOR_COLOR, DEFAULT_EXPIRED_TIMER_INDICATOR_COLOR); + } + /** * @return {@code true} if a warning is displayed before deleting a timer. {@code false} otherwise. */ @@ -545,12 +822,22 @@ public static boolean isPerAlarmVolumeEnabled(SharedPreferences prefs) { return prefs.getBoolean(KEY_ENABLE_PER_ALARM_VOLUME, DEFAULT_ENABLE_PER_ALARM_VOLUME); } + /** + * @return {@code true} if a custom volume increase duration can be set for each alarm. + * {@code false} otherwise. + */ + public static boolean isPerAlarmCrescendoDurationEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION, + DEFAULT_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION); + } + /** * @return the duration, in seconds, of the crescendo to apply to alarm ringtone playback; * {@code 0} implies no crescendo should be applied. */ public static int getAlarmVolumeCrescendoDuration(SharedPreferences prefs) { - return prefs.getInt(KEY_ALARM_VOLUME_CRESCENDO_DURATION, DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION); + return prefs.getInt(KEY_ALARM_VOLUME_CRESCENDO_DURATION, DEFAULT_VOLUME_CRESCENDO_DURATION); } /** @@ -563,12 +850,12 @@ public static boolean isAdvancedAudioPlaybackEnabled(SharedPreferences prefs) { } /** - * @return {@code true} if the ringtone should be automatically routed to Bluetooth devices. - * {@code false} otherwise. + * @return {@code true} if the ringtone should be automatically routed to external audio devices + * (Bluetooth A2DP/SCO or wired headphones/headset). {@code false} otherwise. */ - public static boolean isAutoRoutingToBluetoothDeviceEnabled(SharedPreferences prefs) { + public static boolean isAutoRoutingToExternalAudioDevice(SharedPreferences prefs) { // Default value must match the one in res/xml/settings_alarm.xml - return prefs.getBoolean(KEY_AUTO_ROUTING_TO_BLUETOOTH_DEVICE, DEFAULT_AUTO_ROUTING_TO_BLUETOOTH_DEVICE); + return prefs.getBoolean(KEY_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE, DEFAULT_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE); } /** @@ -581,11 +868,11 @@ public static boolean shouldUseCustomMediaVolume(SharedPreferences prefs) { } /** - * @return the volume applied to the ringtone when a Bluetooth device is connected. + * @return the volume applied to the ringtone when an external audio device is connected. */ - public static int getBluetoothVolumeValue(SharedPreferences prefs) { + public static int getExternalAudioDeviceVolumeValue(SharedPreferences prefs) { // Default value must match the one in res/xml/settings_alarm.xml - return prefs.getInt(KEY_BLUETOOTH_VOLUME, DEFAULT_BLUETOOTH_VOLUME); + return prefs.getInt(KEY_EXTERNAL_AUDIO_DEVICE_VOLUME, DEFAULT_EXTERNAL_AUDIO_DEVICE_VOLUME); } /** @@ -593,7 +880,7 @@ public static int getBluetoothVolumeValue(SharedPreferences prefs) { * {@code 0} implies no crescendo should be applied. */ public static int getTimerVolumeCrescendoDuration(SharedPreferences prefs) { - return prefs.getInt(KEY_TIMER_VOLUME_CRESCENDO_DURATION, DEFAULT_TIMER_VOLUME_CRESCENDO_DURATION); + return prefs.getInt(KEY_TIMER_VOLUME_CRESCENDO_DURATION, DEFAULT_VOLUME_CRESCENDO_DURATION); } /** @@ -604,6 +891,30 @@ public static boolean isSwipeActionEnabled(SharedPreferences pref) { return pref.getBoolean(KEY_SWIPE_ACTION, DEFAULT_SWIPE_ACTION); } + /** + * @return the alarm sorting by time, by time of next alarm and by name. + */ + public static String getAlarmSorting(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer.xml + return prefs.getString(KEY_SORT_ALARM, DEFAULT_SORT_BY_ALARM_TIME); + } + + /** + * @return {@code true} if the enabled alarms are displayed first; {@code false} otherwise. + */ + public static boolean areEnabledAlarmsDisplayedFirst(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_DISPLAY_ENABLED_ALARMS_FIRST, DEFAULT_DISPLAY_ENABLED_ALARMS_FIRST); + } + + /** + * @return {@code true} if the long press on the alarm FAB is enabled; {@code false} otherwise. + */ + public static boolean isAlarmFabLongPressEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_ENABLE_ALARM_FAB_LONG_PRESS, DEFAULT_ENABLE_ALARM_FAB_LONG_PRESS); + } + /** * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY}, * {@link Calendar#SUNDAY} or {@link Calendar#MONDAY} @@ -619,24 +930,6 @@ public static Weekdays.Order getWeekdayOrder(SharedPreferences prefs) { }; } - /** - * @return {@code true} if the restore process (of backup and restore) has completed. {@code false} otherwise. - */ - public static boolean isRestoreBackupFinished(SharedPreferences prefs) { - return prefs.getBoolean(KEY_RESTORE_BACKUP_FINISHED, false); - } - - /** - * @param finished {@code true} means the restore process (of backup and restore) has completed - */ - public static void setRestoreBackupFinished(SharedPreferences prefs, boolean finished) { - if (finished) { - prefs.edit().putBoolean(KEY_RESTORE_BACKUP_FINISHED, true).apply(); - } else { - prefs.edit().remove(KEY_RESTORE_BACKUP_FINISHED).apply(); - } - } - /** * @return the behavior to execute when volume button is pressed while firing an alarm */ @@ -666,6 +959,15 @@ public static PowerButtonBehavior getAlarmPowerButtonBehavior(SharedPreferences }; } + /** + * @return {@code true} if a custom auto silence duration can be set for each alarm. + * {@code false} otherwise. + */ + public static boolean isPerAlarmAutoSilenceEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_ENABLE_PER_ALARM_AUTO_SILENCE, DEFAULT_ENABLE_PER_ALARM_AUTO_SILENCE); + } + /** * @return the number of minutes an alarm may ring before it has timed out */ @@ -673,6 +975,15 @@ public static int getAlarmTimeout(SharedPreferences prefs) { return prefs.getInt(KEY_AUTO_SILENCE_DURATION, DEFAULT_AUTO_SILENCE_DURATION); } + /** + * @return {@code true} if a custom snooze duration can be set for each alarm. + * {@code false} otherwise. + */ + public static boolean isPerAlarmSnoozeDurationEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_ENABLE_PER_ALARM_SNOOZE_DURATION, DEFAULT_ENABLE_PER_ALARM_SNOOZE_DURATION); + } + /** * @return the number of minutes an alarm will remain snoozed before it rings again */ @@ -680,6 +991,24 @@ public static int getSnoozeLength(SharedPreferences prefs) { return prefs.getInt(KEY_ALARM_SNOOZE_DURATION, DEFAULT_ALARM_SNOOZE_DURATION); } + /** + * @return {@code true} if a custom repeat limit can be set for each missed alarm. + * {@code false} otherwise. + */ + public static boolean isPerAlarmMissedRepeatLimitEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT, DEFAULT_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT); + } + + /** + * @return the number of times an alarm can ring before being missed. + */ + public static int getMissedAlarmRepeatLimit(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + final String string = prefs.getString(KEY_REPEAT_MISSED_ALARM, DEFAULT_MISSED_ALARM_REPEAT_LIMIT); + return Integer.parseInt(string); + } + /** * @param currentTime timezone offsets created relative to this time * @return a description of the time zones available for selection @@ -746,6 +1075,15 @@ public static int getShakeIntensity(SharedPreferences pref) { return pref.getInt(KEY_SHAKE_INTENSITY, DEFAULT_SHAKE_INTENSITY); } + /** + * @return {@code true} if the Dismiss button should appear as soon as the alarm is enabled. + * {@code false} otherwise. + */ + public static boolean isDismissButtonDisplayedWhenAlarmEnabled(SharedPreferences pref) { + // Default value must match the one in res/xml/settings_alarm.xml + return pref.getBoolean(KEY_DISPLAY_DISMISS_BUTTON, DEFAULT_DISPLAY_DISMISS_BUTTON); + } + /** * @return the number of minutes before the upcoming alarm notification appears */ @@ -755,6 +1093,29 @@ public static int getAlarmNotificationReminderTime(SharedPreferences prefs) { return Integer.parseInt(string); } + /** + * @return {@code true} if a custom vibration pattern can be set for each alarm. + * {@code false} otherwise. + */ + public static boolean isPerAlarmVibrationPatternEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm.xml + return prefs.getBoolean(KEY_ENABLE_PER_ALARM_VIBRATION_PATTERN, DEFAULT_ENABLE_PER_ALARM_VIBRATION_PATTERN); + } + + /** + * @return the vibration pattern applied to alarms. + */ + public static String getVibrationPattern(SharedPreferences prefs) { + return prefs.getString(KEY_VIBRATION_PATTERN, DEFAULT_VIBRATION_PATTERN); + } + + /** + * @return the vibration start delay applied to alarms. + */ + public static int getVibrationStartDelay(SharedPreferences prefs) { + return prefs.getInt(KEY_VIBRATION_START_DELAY, DEFAULT_VIBRATION_START_DELAY); + } + /** * @return {@code true} if alarm vibrations are enabled when creating alarms. {@code false} otherwise. */ @@ -812,13 +1173,57 @@ public static ClockStyle getAlarmClockStyle(SharedPreferences prefs) { return getClockStyle(prefs, KEY_ALARM_CLOCK_STYLE); } + /** + * @return the clock dial applied for alarms. + */ + public static String getAlarmClockDial(SharedPreferences prefs) { + return prefs.getString(KEY_ALARM_CLOCK_DIAL, DEFAULT_CLOCK_DIAL); + } + + /** + * @return the clock second hand applied for alarms. + */ + public static String getAlarmClockSecondHand(SharedPreferences prefs) { + return prefs.getString(KEY_ALARM_CLOCK_SECOND_HAND, DEFAULT_CLOCK_SECOND_HAND); + } + + /** + * @return the material clock dial applied for alarms. + */ + public static String getAlarmClockDialMaterial(SharedPreferences prefs) { + return prefs.getString(KEY_ALARM_CLOCK_DIAL_MATERIAL, DEFAULT_CLOCK_DIAL_MATERIAL); + } + + /** + * @return the analog clock size for alarms. + */ + public static int getAlarmAnalogClockSize(SharedPreferences prefs) { + return prefs.getInt(KEY_ALARM_ANALOG_CLOCK_SIZE, DEFAULT_ANALOG_CLOCK_SIZE); + } + /** * @return {@code true} if the second hand is displayed on analog clock for the alarm. * {@code false} otherwise. */ - public static boolean isAlarmSecondsHandDisplayed(SharedPreferences prefs) { + public static boolean isAlarmSecondHandDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getBoolean(KEY_DISPLAY_ALARM_SECOND_HAND, DEFAULT_DISPLAY_ALARM_SECOND_HAND); + } + + /** + * @return {@code true} if the snooze selector is displayed on the triggered alarm view. + * {@code false} otherwise. + */ + public static boolean isSnoozeSelectorDisplayed(SharedPreferences prefs) { // Default value must match the one in res/xml/settings_alarm_display.xml - return prefs.getBoolean(KEY_DISPLAY_ALARM_SECONDS_HAND, DEFAULT_DISPLAY_ALARM_SECONDS_HAND); + return prefs.getBoolean(KEY_DISPLAY_SNOOZE_SELECTOR, DEFAULT_DISPLAY_SNOOZE_SELECTOR); + } + + /** + * @return the font applied to the alarm. + */ + public static String getAlarmFont(SharedPreferences prefs) { + return prefs.getString(KEY_ALARM_FONT, null); } /** @@ -846,11 +1251,11 @@ public static int getAlarmClockColor(SharedPreferences prefs) { } /** - * @return a value indicating the alarm seconds hand color. + * @return a value indicating the alarm second hand color. */ - public static int getAlarmSecondsHandColor(SharedPreferences prefs, Context context) { + public static int getAlarmSecondHandColor(SharedPreferences prefs, Context context) { // Default value must match the one in res/xml/settings_alarm_display.xml - return prefs.getInt(KEY_ALARM_SECONDS_HAND_COLOR, getDefaultAlarmInversePrimaryColor(context)); + return prefs.getInt(KEY_ALARM_SECOND_HAND_COLOR, getDefaultAlarmInversePrimaryColor(context)); } /** @@ -869,6 +1274,14 @@ public static int getSlideZoneColor(SharedPreferences prefs) { return prefs.getInt(KEY_SLIDE_ZONE_COLOR, DEFAULT_SLIDE_ZONE_COLOR); } + /** + * @return a value indicating the alarm button color. + */ + public static int getAlarmButtonColor(SharedPreferences prefs, Context context) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_ALARM_BUTTON_COLOR, getDefaultAlarmInversePrimaryColor(context)); + } + /** * @return a value indicating the color of "Snooze" title. */ @@ -902,11 +1315,51 @@ public static int getDismissButtonColor(SharedPreferences prefs, Context context } /** - * @return a value indicating the alarm button color. + * @return a value indicating the snooze zone color. */ - public static int getAlarmButtonColor(SharedPreferences prefs, Context context) { + public static int getSnoozeZoneColor(SharedPreferences prefs) { // Default value must match the one in res/xml/settings_alarm_display.xml - return prefs.getInt(KEY_ALARM_BUTTON_COLOR, getDefaultAlarmInversePrimaryColor(context)); + return prefs.getInt(KEY_SNOOZE_ZONE_COLOR, DEFAULT_SNOOZE_ZONE_COLOR); + } + + /** + * @return a value indicating the snooze minus button color. + */ + public static int getSnoozeMinusButtonColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SNOOZE_MINUS_BUTTON_COLOR, DEFAULT_SNOOZE_BUTTON_COLOR); + } + + /** + * @return a value indicating the snooze plus button color. + */ + public static int getSnoozePlusButtonColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SNOOZE_PLUS_BUTTON_COLOR, DEFAULT_SNOOZE_BUTTON_COLOR); + } + + /** + * @return a value indicating the snooze selector text color. + */ + public static int getSnoozeSelectorTextColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SNOOZE_SELECTOR_TEXT_COLOR, DEFAULT_SNOOZE_TEXT_COLOR); + } + + /** + * @return a value indicating the snooze minus symbol color. + */ + public static int getSnoozeMinusSymbolColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SNOOZE_MINUS_SYMBOL_COLOR, DEFAULT_SNOOZE_TEXT_COLOR); + } + + /** + * @return a value indicating the snooze plus symbol color. + */ + public static int getSnoozePlusSymbolColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_SNOOZE_PLUS_SYMBOL_COLOR, DEFAULT_SNOOZE_TEXT_COLOR); } /** @@ -914,7 +1367,7 @@ public static int getAlarmButtonColor(SharedPreferences prefs, Context context) */ public static int getAlarmDigitalClockFontSize(SharedPreferences prefs) { // Default value must match the one in res/xml/settings_alarm_display.xml - return prefs.getInt(KEY_ALARM_DIGITAL_CLOCK_FONT_SIZE, DEFAULT_ALARM_DIGITAL_CLOCK_FONT_SIZE); + return prefs.getInt(KEY_ALARM_DIGITAL_CLOCK_FONT_SIZE, DEFAULT_DIGITAL_CLOCK_FONT_SIZE); } /** @@ -925,6 +1378,31 @@ public static int getAlarmTitleFontSize(SharedPreferences prefs) { return prefs.getInt(KEY_ALARM_TITLE_FONT_SIZE_PREF, DEFAULT_ALARM_TITLE_FONT_SIZE_PREF); } + /** + * @return {@code true} if a shadow is displayed on the texts of the triggered alarm. + * {@code false} otherwise. + */ + public static boolean isAlarmTextShadowDisplayed(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getBoolean(KEY_ALARM_DISPLAY_TEXT_SHADOW, DEFAULT_DISPLAY_TEXT_SHADOW); + } + + /** + * @return a value indicating the shadow color displayed on the triggered alarm texts. + */ + public static int getAlarmShadowColor(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_ALARM_SHADOW_COLOR, DEFAULT_ALARM_SHADOW_COLOR); + } + + /** + * @return a value indicating the shadow offset for the triggered alarm texts. + */ + public static int getAlarmShadowOffset(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_ALARM_SHADOW_OFFSET, DEFAULT_SHADOW_OFFSET); + } + /** * @return {@code true} if the ringtone title should be displayed on the lock screen when the alarm is triggered. * {@code false} otherwise. @@ -943,18 +1421,27 @@ public static int getRingtoneTitleColor(SharedPreferences prefs) { } /** - * @return the holiday data url. + * @return the URI of the image to be displayed on the lock screen when the alarm is triggered. */ - public static String getHolidayDataUrl(SharedPreferences prefs) { - // Default value must match the one in res/values/strings.xml - return prefs.getString(KEY_HOLIDAY_DATA_URL, DEFAULT_HOLIDAY_DATA_URL); + public static String getAlarmBackgroundImage(SharedPreferences prefs) { + return prefs.getString(KEY_ALARM_BACKGROUND_IMAGE, null); } /** - * @param url the holiday data url + * @return {@code true} if a blur effect should be applied to the image when the alarm is triggered. + * {@code false} otherwise. */ - public static void setHolidayDataUrl(SharedPreferences prefs, String url) { - prefs.edit().putString(KEY_HOLIDAY_DATA_URL, url).apply(); + public static boolean isAlarmBlurEffectEnabled(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getBoolean(KEY_ENABLE_ALARM_BLUR_EFFECT, DEFAULT_ENABLE_BLUR_EFFECT); + } + + /** + * @return the blur intensity applied to the image when the alarm is triggered. + */ + public static int getAlarmBlurIntensity(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_alarm_display.xml + return prefs.getInt(KEY_ALARM_BLUR_INTENSITY, DEFAULT_BLUR_INTENSITY); } private static ClockStyle getClockStyle(SharedPreferences prefs, String key) { @@ -964,6 +1451,13 @@ private static ClockStyle getClockStyle(SharedPreferences prefs, String key) { return ClockStyle.valueOf(clockStyle.toUpperCase(Locale.US)); } + /** + * @return the font applied to the stopwatch. + */ + public static String getStopwatchFont(SharedPreferences prefs) { + return prefs.getString(KEY_SW_FONT, null); + } + /** * @return the action to execute when volume up button is pressed for the stopwatch */ @@ -1024,4 +1518,19 @@ public int compareTo(@NonNull TimeZoneDescriptor other) { } } + + /** + * @return the holiday data url. + */ + public static String getHolidayDataUrl(SharedPreferences prefs) { + // Default value must match the one in res/values/strings.xml + return prefs.getString(KEY_HOLIDAY_DATA_URL, DEFAULT_HOLIDAY_DATA_URL); + } + + /** + * @param url the holiday data url + */ + public static void setHolidayDataUrl(SharedPreferences prefs, String url) { + prefs.edit().putString(KEY_HOLIDAY_DATA_URL, url).apply(); + } } diff --git a/app/src/main/java/com/best/deskclock/data/Weekdays.java b/app/src/main/java/com/best/deskclock/data/Weekdays.java index e07420b38..c1f579430 100644 --- a/app/src/main/java/com/best/deskclock/data/Weekdays.java +++ b/app/src/main/java/com/best/deskclock/data/Weekdays.java @@ -16,12 +16,20 @@ import static java.util.Calendar.WEDNESDAY; import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.util.ArrayMap; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.best.deskclock.R; +import com.google.android.material.color.MaterialColors; import java.text.DateFormatSymbols; import java.util.Arrays; @@ -34,8 +42,10 @@ * This class is responsible for encoding a weekly repeat cycle in a {@link #getBits bitset}. It * also converts between those bits and the {@link Calendar#DAY_OF_WEEK} values for easier mutation * and querying. + * + * @param mBits An encoded form of a weekly repeat schedule. */ -public final class Weekdays { +public record Weekdays(int mBits) { /** * An instance with no weekdays in the weekly repeat cycle. @@ -63,14 +73,9 @@ public final class Weekdays { sCalendarDayToBit = Collections.unmodifiableMap(map); } - /** - * An encoded form of a weekly repeat schedule. - */ - private final int mBits; - - private Weekdays(int bits) { + public Weekdays(int mBits) { // Mask off the unused bits. - mBits = ALL_DAYS & bits; + this.mBits = ALL_DAYS & mBits; } /** @@ -156,12 +161,19 @@ public int getBits() { } /** - * @return {@code true} iff at least one weekday is enabled in the repeat schedule + * @return {@code true} if at least one weekday is enabled in the repeat schedule */ public boolean isRepeating() { return mBits != 0; } + /** + * @return {@code true} if all days of the week are selected; {@code false} otherwise. + */ + public boolean isAllDaysSelected() { + return mBits == ALL_DAYS; + } + /** * Note: only the day-of-week is read from the {@code time}. The time fields * are not considered in this computation. @@ -174,8 +186,8 @@ public int getDistanceToPreviousDay(Calendar time) { int calendarDay = time.get(DAY_OF_WEEK); for (int count = 1; count <= 7; count++) { calendarDay--; - if (calendarDay < Calendar.SUNDAY) { - calendarDay = Calendar.SATURDAY; + if (calendarDay < SUNDAY) { + calendarDay = SATURDAY; } if (isBitOn(calendarDay)) { return count; @@ -201,8 +213,8 @@ public int getDistanceToNextDay(Calendar time) { } calendarDay++; - if (calendarDay > Calendar.SATURDAY) { - calendarDay = Calendar.SUNDAY; + if (calendarDay > SATURDAY) { + calendarDay = SUNDAY; } } @@ -218,11 +230,6 @@ public boolean equals(Object o) { return mBits == weekdays.mBits; } - @Override - public int hashCode() { - return mBits; - } - @NonNull @Override public String toString() { @@ -259,7 +266,7 @@ public String toString() { * @return the enabled weekdays in the given {@code order} */ public String toString(Context context, Order order) { - return toString(context, order, false /* forceLongNames */); + return toString(context, order, false); } /** @@ -269,7 +276,7 @@ public String toString(Context context, Order order) { * is most appropriate for talk-back */ public String toAccessibilityString(Context context, Order order) { - return toString(context, order, true /* forceLongNames */); + return toString(context, order, true); } @VisibleForTesting @@ -307,7 +314,7 @@ private String toString(Context context, Order order, boolean forceLongNames) { final StringBuilder builder = new StringBuilder(40); for (int calendarDay : order.getCalendarDays()) { if (isBitOn(calendarDay)) { - if (builder.length() > 0) { + if (!TextUtils.isEmpty(builder)) { builder.append(separator); } builder.append(weekdays[calendarDay]); @@ -316,6 +323,53 @@ private String toString(Context context, Order order, boolean forceLongNames) { return builder.toString(); } + /** + * Returns a styled representation of the repeating days of the week. + *

+ * This method builds a {@link SpannableStringBuilder} containing the names of the days + * on which the alarm is set to repeat. The day corresponding to the next scheduled alarm + * (if provided) will be colored and displayed in bold. + *

+ * + * @param context the context used to access resources + * @param order the preferred order of weekdays (e.g., starting on Monday or Sunday) + * @param forceLongNames whether to force the use of full weekday names + * @param nextAlarmDay the calendar day (e.g., {@link Calendar#MONDAY}) of the next alarm; + * if matched, that day will be styled in bold + * @return a {@link CharSequence} with the formatted and styled weekday names + */ + public CharSequence toStyledString(Context context, Order order, boolean forceLongNames, int nextAlarmDay) { + if (!isRepeating()) { + return ""; + } + + final boolean longNames = forceLongNames || getCount() <= 1; + final DateFormatSymbols dfs = new DateFormatSymbols(); + final String[] weekdays = longNames ? dfs.getWeekdays() : dfs.getShortWeekdays(); + final String separator = context.getString(R.string.day_concat); + + final SpannableStringBuilder builder = new SpannableStringBuilder(); + for (int calendarDay : order.getCalendarDays()) { + if (isBitOn(calendarDay)) { + if (!TextUtils.isEmpty(builder)) { + builder.append(separator); + } + + String dayName = weekdays[calendarDay]; + int start = builder.length(); + builder.append(dayName); + int end = builder.length(); + + if (calendarDay == nextAlarmDay) { + int primaryColor = MaterialColors.getColor(context, androidx.appcompat.R.attr.colorPrimary, Color.BLACK); + builder.setSpan(new ForegroundColorSpan(primaryColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + return builder; + } + /** * The preferred starting day of the week can differ by locale. This enumerated value is used to * describe the preferred ordering. diff --git a/app/src/main/java/com/best/deskclock/holiday/HolidayDao.java b/app/src/main/java/com/best/deskclock/holiday/HolidayDao.java index 946df2522..e671eee6b 100644 --- a/app/src/main/java/com/best/deskclock/holiday/HolidayDao.java +++ b/app/src/main/java/com/best/deskclock/holiday/HolidayDao.java @@ -36,4 +36,10 @@ public interface HolidayDao { @Query("SELECT * FROM holiday WHERE compDays LIKE '%' || :date || '%'") Holiday getCompDayByDate(String date); + + @Query("SELECT * FROM holiday") + List getAllHolidays(); + + @Query("DELETE FROM holiday") + void deleteAll(); } diff --git a/app/src/main/java/com/best/deskclock/holiday/HolidayDialogFragment.java b/app/src/main/java/com/best/deskclock/holiday/HolidayDialogFragment.java index 373edd22f..a7284a8ae 100644 --- a/app/src/main/java/com/best/deskclock/holiday/HolidayDialogFragment.java +++ b/app/src/main/java/com/best/deskclock/holiday/HolidayDialogFragment.java @@ -49,6 +49,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { final Alarm alarm = getArguments().getParcelable(ARG_ALARM); final CharSequence[] items = { + getString(R.string.label_off), getString(R.string.skip_holiday), getString(R.string.daxiao_da), getString(R.string.daxiao_xiao), @@ -59,6 +60,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { .setTitle(R.string.holiday_title) .setSingleChoiceItems(items, alarm.holidayOption, (dialog, which) -> { alarm.holidayOption = which; + getParentFragmentManager().setFragmentResult("holiday_option", new Bundle()); dismiss(); }) .create(); diff --git a/app/src/main/java/com/best/deskclock/holiday/HolidayRepository.java b/app/src/main/java/com/best/deskclock/holiday/HolidayRepository.java index 7e3837fbe..c3c12b30a 100644 --- a/app/src/main/java/com/best/deskclock/holiday/HolidayRepository.java +++ b/app/src/main/java/com/best/deskclock/holiday/HolidayRepository.java @@ -16,7 +16,7 @@ package com.best.deskclock.holiday; -import android.app.Application; +import android.content.Context; import com.best.deskclock.data.DataModel; @@ -33,15 +33,27 @@ public class HolidayRepository { + private static volatile HolidayRepository sInstance; private final HolidayDao mHolidayDao; private final ExecutorService mExecutorService; - public HolidayRepository(Application application) { - HolidayDatabase db = HolidayDatabase.getDatabase(application); + private HolidayRepository(Context context) { + HolidayDatabase db = HolidayDatabase.getDatabase(context); mHolidayDao = db.holidayDao(); mExecutorService = Executors.newSingleThreadExecutor(); } + public static HolidayRepository getInstance(Context context) { + if (sInstance == null) { + synchronized (HolidayRepository.class) { + if (sInstance == null) { + sInstance = new HolidayRepository(context); + } + } + } + return sInstance; + } + public void updateWorkdayData() { mExecutorService.execute(() -> { try { @@ -64,4 +76,8 @@ public Holiday getHolidayByDate(String date) { public Holiday getCompDayByDate(String date) { return mHolidayDao.getCompDayByDate(date); } + + public List getAllHolidays() { + return mHolidayDao.getAllHolidays(); + } } diff --git a/app/src/main/java/com/best/deskclock/holiday/HolidayUtils.java b/app/src/main/java/com/best/deskclock/holiday/HolidayUtils.java new file mode 100644 index 000000000..0c0c88762 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/holiday/HolidayUtils.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * 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.best.deskclock.holiday; + +import com.best.deskclock.provider.Alarm; + +import android.content.Context; + +import com.best.deskclock.data.Weekdays; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +public class HolidayUtils { + + public static final int HOLIDAY_OPTION_NONE = 0; + public static final int HOLIDAY_OPTION_SKIP_HOLIDAY = 1; + public static final int HOLIDAY_OPTION_BIG_SMALL_DA = 2; + public static final int HOLIDAY_OPTION_BIG_SMALL_XIAO = 3; + public static final int HOLIDAY_OPTION_SINGLE_DAY_OFF = 4; + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + + /** + * Determines if the alarm should ring on the given date based on the selected holiday option. + * + * @param context The context. + * @param holidayOption The holiday option selected for the alarm. + * @param daysOfWeek The repeating days of the week for the alarm. + * @param calendar The date to check. + * @return True if the alarm should ring, false otherwise. + */ + public static boolean shouldAlarmRing(Context context, int holidayOption, Weekdays daysOfWeek, Calendar calendar) { + if (holidayOption == HOLIDAY_OPTION_NONE) { + return true; + } + + String dateStr = DATE_FORMAT.format(calendar.getTime()); + HolidayRepository repo = HolidayRepository.getInstance(context); + + // Check if it's a legal holiday or compensation workday + Holiday holiday = repo.getHolidayByDate(dateStr); + Holiday compDay = repo.getCompDayByDate(dateStr); + + boolean isLegalHoliday = (holiday != null); + boolean isCompWorkday = (compDay != null); + + int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + + switch (holidayOption) { + case HOLIDAY_OPTION_SKIP_HOLIDAY: + // Normal skip holiday logic: + // 1. Ring if it's a compensation workday (even if it's a weekend) + // 2. Skip if it's a legal holiday + // 3. Otherwise, follow the alarm's set days of the week + if (isCompWorkday) return true; + if (isLegalHoliday) return false; + return daysOfWeek.isBitOn(dayOfWeek); + + case HOLIDAY_OPTION_BIG_SMALL_DA: + // Big Week: Sat don't work this week, next week Sat work. + // Usually means alternating Saturdays. + if (isCompWorkday) return true; + if (isLegalHoliday) return false; + if (dayOfWeek == Calendar.SATURDAY) { + // Check if current week is Big or Small week. + // For example, even weeks are Big weeks (Sat off), odd weeks are Small weeks (Sat work). + // This is a simplified logic. + return (calendar.get(Calendar.WEEK_OF_YEAR) % 2 == 0); + } + return (dayOfWeek != Calendar.SUNDAY); + + case HOLIDAY_OPTION_BIG_SMALL_XIAO: + // Small Week: Opposite of Big Week. + if (isCompWorkday) return true; + if (isLegalHoliday) return false; + if (dayOfWeek == Calendar.SATURDAY) { + return (calendar.get(Calendar.WEEK_OF_YEAR) % 2 != 0); + } + return (dayOfWeek != Calendar.SUNDAY); + + case HOLIDAY_OPTION_SINGLE_DAY_OFF: + // Single Day Off: Every week work Mon-Sat, Sun off. + if (isCompWorkday) return true; + if (isLegalHoliday) return false; + return (dayOfWeek != Calendar.SUNDAY); + + default: + return true; + } + } + + /** + * Calculates the absolute next alarm time, taking holidays into account. + * + * @param context The context. + * @param holidayOption The holiday option selected for the alarm. + * @param alarm The alarm object. + * @param currentTime The reference time. + * @return The next valid firing time. + */ + public static Calendar getNextWorkdayAlarmTime(Context context, int holidayOption, Alarm alarm, Calendar currentTime) { + Calendar nextInstanceTime = alarm.getNextAlarmTime(currentTime); + + if (holidayOption == HOLIDAY_OPTION_NONE) { + return nextInstanceTime; + } + + // Iterate up to 365 days to find the next valid workday + for (int i = 0; i < 365; i++) { + if (shouldAlarmRing(context, holidayOption, alarm.daysOfWeek, nextInstanceTime)) { + return nextInstanceTime; + } + // Advance to the next occurrence of the alarm + nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1); + // After adding a day, the distance logic might be needed if it's a repeating alarm + if (alarm.daysOfWeek.isRepeating()) { + int addDays = alarm.daysOfWeek.getDistanceToNextDay(nextInstanceTime); + if (addDays > 0) { + nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays); + } + } + // Ensure time is reset to the alarm time (in case of DST or multi-day jumps) + nextInstanceTime.set(Calendar.HOUR_OF_DAY, alarm.hour); + nextInstanceTime.set(Calendar.MINUTE, alarm.minutes); + nextInstanceTime.set(Calendar.SECOND, 0); + nextInstanceTime.set(Calendar.MILLISECOND, 0); + } + + return alarm.getNextAlarmTime(currentTime); // Fallback + } +} diff --git a/app/src/main/java/com/best/deskclock/provider/Alarm.java b/app/src/main/java/com/best/deskclock/provider/Alarm.java index 88d5e5af9..a284aebe4 100644 --- a/app/src/main/java/com/best/deskclock/provider/Alarm.java +++ b/app/src/main/java/com/best/deskclock/provider/Alarm.java @@ -6,11 +6,23 @@ package com.best.deskclock.provider; +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_SNOOZE_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_VOLUME; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_AUTO_SILENCE_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_MISSED_ALARM_REPEAT_LIMIT; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_SORT_BY_ALARM_TIME; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VIBRATION_PATTERN; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VOLUME_CRESCENDO_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.SORT_ALARM_BY_ASCENDING_CREATION_ORDER; +import static com.best.deskclock.settings.PreferencesDefaultValues.SORT_ALARM_BY_DESCENDING_CREATION_ORDER; + import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.database.Cursor; import android.media.RingtoneManager; import android.net.Uri; @@ -22,6 +34,7 @@ import com.best.deskclock.R; import com.best.deskclock.data.DataModel; +import com.best.deskclock.data.SettingsDAO; import com.best.deskclock.data.Weekdays; import com.best.deskclock.utils.RingtoneUtils; import com.best.deskclock.utils.SdkUtils; @@ -37,6 +50,11 @@ public final class Alarm implements Parcelable, ClockContract.AlarmsColumns { public static final long INVALID_ID = -1; public int holidayOption; + /** + * SharedPreferences key used to indicate whether the styled repeat day display is enabled + * for a specific alarm. Used to customize how repeat days are shown in the UI. + */ + private static final String KEY_SHOW_STYLED_REPEAT_DAY = "show_styled_repeat_day_"; public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() { public Alarm createFromParcel(Parcel p) { @@ -47,13 +65,52 @@ public Alarm[] newArray(int size) { return new Alarm[size]; } }; + /** * The default sort order for this table */ private static final String DEFAULT_SORT_ORDER = - ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + ", " + - ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES + " ASC" + ", " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + " ASC, " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES + " ASC, " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC"; + + /** + * The default sort order for this table with enabled alarms first + */ + private static final String DEFAULT_SORT_ORDER_WITH_ENABLED_FIRST = + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED + " DESC, " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + " ASC, " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES + " ASC, " + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC"; + + /** + * The sort order by descending ID to display oldest alarms last. + */ + private static final String SORT_ORDER_BY_DESCENDING_CREATION = + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID + " DESC"; + + /** + * The sort order that places enabled alarms first, then sorts alarms by descending ID + * with the oldest last. + */ + private static final String SORT_ORDER_BY_DESCENDING_CREATION_WITH_ENABLED_FIRST = + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED + " DESC, " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID + " DESC"; + + /** + * The sort order by ascending ID to display oldest alarms first. + */ + private static final String SORT_ORDER_BY_ASCENDING_CREATION = + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID + " ASC"; + + /** + * The sort order that places enabled alarms first, then sorts alarms by ascending ID + * with the oldest first. + */ + private static final String SORT_ORDER_BY_ASCENDING_CREATION_WITH_ENABLED_FIRST = + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED + " DESC, " + + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID + " ASC"; + private static final String[] QUERY_COLUMNS = { _ID, YEAR, @@ -64,12 +121,14 @@ public Alarm[] newArray(int size) { DAYS_OF_WEEK, ENABLED, VIBRATE, + VIBRATION_PATTERN, FLASH, LABEL, RINGTONE, DELETE_AFTER_USE, AUTO_SILENCE_DURATION, SNOOZE_DURATION, + MISSED_ALARM_REPEAT_LIMIT, CRESCENDO_DURATION, ALARM_VOLUME }; @@ -83,12 +142,14 @@ public Alarm[] newArray(int size) { ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DAYS_OF_WEEK, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATE, + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATION_PATTERN, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + FLASH, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + LABEL, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + RINGTONE, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DELETE_AFTER_USE, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AUTO_SILENCE_DURATION, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + SNOOZE_DURATION, + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MISSED_ALARM_REPEAT_LIMIT, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + CRESCENDO_DURATION, ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ALARM_VOLUME, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.ALARM_STATE, @@ -100,9 +161,12 @@ public Alarm[] newArray(int size) { ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MINUTES, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.LABEL, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATE, + ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATION_PATTERN, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.FLASH, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.AUTO_SILENCE_DURATION, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.SNOOZE_DURATION, + ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MISSED_ALARM_REPEAT_COUNT, + ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MISSED_ALARM_REPEAT_LIMIT, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.CRESCENDO_DURATION, ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.ALARM_VOLUME }; @@ -119,29 +183,34 @@ public Alarm[] newArray(int size) { private static final int DAYS_OF_WEEK_INDEX = 6; private static final int ENABLED_INDEX = 7; private static final int VIBRATE_INDEX = 8; - private static final int FLASH_INDEX = 9; - private static final int LABEL_INDEX = 10; - private static final int RINGTONE_INDEX = 11; - private static final int DELETE_AFTER_USE_INDEX = 12; - private static final int AUTO_SILENCE_DURATION_INDEX = 13; - private static final int SNOOZE_DURATION_INDEX = 14; - private static final int CRESCENDO_DURATION_INDEX = 15; - private static final int ALARM_VOLUME_INDEX = 16; - - private static final int INSTANCE_STATE_INDEX = 17; - public static final int INSTANCE_ID_INDEX = 18; - public static final int INSTANCE_YEAR_INDEX = 19; - public static final int INSTANCE_MONTH_INDEX = 20; - public static final int INSTANCE_DAY_INDEX = 21; - public static final int INSTANCE_HOUR_INDEX = 22; - public static final int INSTANCE_MINUTE_INDEX = 23; - public static final int INSTANCE_LABEL_INDEX = 24; - public static final int INSTANCE_VIBRATE_INDEX = 25; - public static final int INSTANCE_FLASH_INDEX = 26; - public static final int INSTANCE_AUTO_SILENCE_DURATION_INDEX = 27; - public static final int INSTANCE_SNOOZE_DURATION_INDEX = 28; - public static final int INSTANCE_CRESCENDO_DURATION_INDEX = 29; - public static final int INSTANCE_ALARM_VOLUME_INDEX = 30; + private static final int VIBRATION_PATTERN_INDEX = 9; + private static final int FLASH_INDEX = 10; + private static final int LABEL_INDEX = 11; + private static final int RINGTONE_INDEX = 12; + private static final int DELETE_AFTER_USE_INDEX = 13; + private static final int AUTO_SILENCE_DURATION_INDEX = 14; + private static final int SNOOZE_DURATION_INDEX = 15; + private static final int MISSED_ALARM_REPEAT_LIMIT_INDEX = 16; + private static final int CRESCENDO_DURATION_INDEX = 17; + private static final int ALARM_VOLUME_INDEX = 18; + + private static final int INSTANCE_STATE_INDEX = 19; + public static final int INSTANCE_ID_INDEX = 20; + public static final int INSTANCE_YEAR_INDEX = 21; + public static final int INSTANCE_MONTH_INDEX = 22; + public static final int INSTANCE_DAY_INDEX = 23; + public static final int INSTANCE_HOUR_INDEX = 24; + public static final int INSTANCE_MINUTE_INDEX = 25; + public static final int INSTANCE_LABEL_INDEX = 26; + public static final int INSTANCE_VIBRATE_INDEX = 27; + public static final int INSTANCE_VIBRATION_PATTERN_INDEX = 28; + public static final int INSTANCE_FLASH_INDEX = 29; + public static final int INSTANCE_AUTO_SILENCE_DURATION_INDEX = 30; + public static final int INSTANCE_SNOOZE_DURATION_INDEX = 31; + public static final int INSTANCE_MISSED_ALARM_REPEAT_COUNT_INDEX = 32; + public static final int INSTANCE_MISSED_ALARM_REPEAT_LIMIT_INDEX = 33; + public static final int INSTANCE_CRESCENDO_DURATION_INDEX = 34; + public static final int INSTANCE_ALARM_VOLUME_INDEX = 35; private static final int COLUMN_COUNT = ALARM_VOLUME_INDEX + 1; private static final int ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_ALARM_VOLUME_INDEX + 1; @@ -155,17 +224,18 @@ public Alarm[] newArray(int size) { public int minutes; public Weekdays daysOfWeek; public boolean vibrate; + public String vibrationPattern; public boolean flash; public String label; public Uri alert; public boolean deleteAfterUse; public int autoSilenceDuration; public int snoozeDuration; + public int missedAlarmRepeatLimit; public int crescendoDuration; // Alarm volume level in steps; not a percentage public int alarmVolume; public int instanceState; - public int instanceId; // Creates a default alarm at the current time. public Alarm() { @@ -184,22 +254,25 @@ public Alarm(int year, int month, int day, int hour, int minutes) { this.hour = hour; this.minutes = minutes; this.vibrate = true; + this.vibrationPattern = DEFAULT_VIBRATION_PATTERN; this.flash = true; this.daysOfWeek = Weekdays.NONE; this.label = ""; this.alert = DataModel.getDataModel().getAlarmRingtoneUriFromSettings(); this.deleteAfterUse = false; - this.autoSilenceDuration = 10; - this.snoozeDuration = 10; - this.crescendoDuration = 0; - this.alarmVolume = 11; + this.autoSilenceDuration = DEFAULT_AUTO_SILENCE_DURATION; + this.snoozeDuration = DEFAULT_ALARM_SNOOZE_DURATION; + this.missedAlarmRepeatLimit = Integer.parseInt(DEFAULT_MISSED_ALARM_REPEAT_LIMIT); + this.crescendoDuration = DEFAULT_VOLUME_CRESCENDO_DURATION; + this.alarmVolume = DEFAULT_ALARM_VOLUME; } // Used to backup/restore the alarm public Alarm(long id, boolean enabled, int year, int month, int day, int hour, int minutes, - boolean vibrate, boolean flash, Weekdays daysOfWeek, String label, String alert, - boolean deleteAfterUse, int autoSilenceDuration, int snoozeDuration, - int crescendoDuration, int alarmVolume) { + boolean vibrate, String vibrationPattern, boolean flash, Weekdays daysOfWeek, + String label, String alert, boolean deleteAfterUse, int autoSilenceDuration, + int snoozeDuration, int missedAlarmRepeatLimit, int crescendoDuration, + int alarmVolume) { this.id = id; this.enabled = enabled; @@ -209,6 +282,7 @@ public Alarm(long id, boolean enabled, int year, int month, int day, int hour, i this.hour = hour; this.minutes = minutes; this.vibrate = vibrate; + this.vibrationPattern = vibrationPattern; this.flash = flash; this.daysOfWeek = daysOfWeek; this.label = label; @@ -216,6 +290,7 @@ public Alarm(long id, boolean enabled, int year, int month, int day, int hour, i this.deleteAfterUse = deleteAfterUse; this.autoSilenceDuration = autoSilenceDuration; this.snoozeDuration = snoozeDuration; + this.missedAlarmRepeatLimit = missedAlarmRepeatLimit; this.crescendoDuration = crescendoDuration; this.alarmVolume = alarmVolume; } @@ -230,17 +305,18 @@ public Alarm(Cursor c) { minutes = c.getInt(MINUTES_INDEX); daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX)); vibrate = c.getInt(VIBRATE_INDEX) == 1; + vibrationPattern = c.getString(VIBRATION_PATTERN_INDEX); flash = c.getInt(FLASH_INDEX) == 1; label = c.getString(LABEL_INDEX); deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1; autoSilenceDuration = c.getInt(AUTO_SILENCE_DURATION_INDEX); snoozeDuration = c.getInt(SNOOZE_DURATION_INDEX); + missedAlarmRepeatLimit = c.getInt(MISSED_ALARM_REPEAT_LIMIT_INDEX); crescendoDuration = c.getInt(CRESCENDO_DURATION_INDEX); alarmVolume = c.getInt(ALARM_VOLUME_INDEX); if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) { instanceState = c.getInt(INSTANCE_STATE_INDEX); - instanceId = c.getInt(INSTANCE_ID_INDEX); } if (c.isNull(RINGTONE_INDEX)) { @@ -262,6 +338,7 @@ public Alarm(Cursor c) { minutes = p.readInt(); daysOfWeek = Weekdays.fromBits(p.readInt()); vibrate = p.readInt() == 1; + vibrationPattern = p.readString(); flash = p.readInt() == 1; label = p.readString(); alert = SdkUtils.isAtLeastAndroid13() @@ -270,41 +347,70 @@ public Alarm(Cursor c) { deleteAfterUse = p.readInt() == 1; autoSilenceDuration = p.readInt(); snoozeDuration = p.readInt(); + missedAlarmRepeatLimit = p.readInt(); crescendoDuration = p.readInt(); alarmVolume = p.readInt(); } - public static ContentValues createContentValues(Alarm alarm) { + public ContentValues createContentValues() { ContentValues values = new ContentValues(COLUMN_COUNT); - if (alarm.id != INVALID_ID) { - values.put(ClockContract.AlarmsColumns._ID, alarm.id); + if (id != INVALID_ID) { + values.put(ClockContract.AlarmsColumns._ID, id); } - values.put(ENABLED, alarm.enabled ? 1 : 0); - values.put(YEAR, alarm.year); - values.put(MONTH, alarm.month); - values.put(DAY, alarm.day); - values.put(HOUR, alarm.hour); - values.put(MINUTES, alarm.minutes); - values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBits()); - values.put(VIBRATE, alarm.vibrate ? 1 : 0); - values.put(FLASH, alarm.flash ? 1 : 0); - values.put(LABEL, alarm.label); - values.put(DELETE_AFTER_USE, alarm.deleteAfterUse ? 1 : 0); - values.put(AUTO_SILENCE_DURATION, alarm.autoSilenceDuration); - values.put(SNOOZE_DURATION, alarm.snoozeDuration); - values.put(CRESCENDO_DURATION, alarm.crescendoDuration); - values.put(ALARM_VOLUME, alarm.alarmVolume); - if (alarm.alert == null) { + values.put(ENABLED, enabled ? 1 : 0); + values.put(YEAR, year); + values.put(MONTH, month); + values.put(DAY, day); + values.put(HOUR, hour); + values.put(MINUTES, minutes); + values.put(DAYS_OF_WEEK, daysOfWeek.getBits()); + values.put(VIBRATE, vibrate ? 1 : 0); + values.put(VIBRATION_PATTERN, vibrationPattern); + values.put(FLASH, flash ? 1 : 0); + values.put(LABEL, label); + values.put(DELETE_AFTER_USE, deleteAfterUse ? 1 : 0); + values.put(AUTO_SILENCE_DURATION, autoSilenceDuration); + values.put(SNOOZE_DURATION, snoozeDuration); + values.put(MISSED_ALARM_REPEAT_LIMIT, missedAlarmRepeatLimit); + values.put(CRESCENDO_DURATION, crescendoDuration); + values.put(ALARM_VOLUME, alarmVolume); + if (alert == null) { // We want to put null, so default alarm changes values.putNull(RINGTONE); } else { - values.put(RINGTONE, alarm.alert.toString()); + values.put(RINGTONE, alert.toString()); } return values; } + public void writeToParcel(Parcel p, int flags) { + p.writeLong(id); + p.writeInt(enabled ? 1 : 0); + p.writeInt(year); + p.writeInt(month); + p.writeInt(day); + p.writeInt(hour); + p.writeInt(minutes); + p.writeInt(daysOfWeek.getBits()); + p.writeInt(vibrate ? 1 : 0); + p.writeString(vibrationPattern); + p.writeInt(flash ? 1 : 0); + p.writeString(label); + p.writeParcelable(alert, flags); + p.writeInt(deleteAfterUse ? 1 : 0); + p.writeInt(autoSilenceDuration); + p.writeInt(snoozeDuration); + p.writeInt(missedAlarmRepeatLimit); + p.writeInt(crescendoDuration); + p.writeInt(alarmVolume); + } + + public int describeContents() { + return 0; + } + public static Intent createIntent(Context context, Class cls, long alarmId) { return new Intent(context, cls).setData(getContentUri(alarmId)); } @@ -324,8 +430,38 @@ public static long getId(Uri contentUri) { * @return cursor loader with all the alarms. */ public static CursorLoader getAlarmsCursorLoader(Context context) { + final SharedPreferences prefs = getDefaultSharedPreferences(context); + boolean areEnabledAlarmsFirst = SettingsDAO.areEnabledAlarmsDisplayedFirst(prefs); + + String sortOrder = DEFAULT_SORT_ORDER; + String sortingPref = SettingsDAO.getAlarmSorting(prefs); + + switch (sortingPref) { + case DEFAULT_SORT_BY_ALARM_TIME -> { + if (areEnabledAlarmsFirst) { + sortOrder = DEFAULT_SORT_ORDER_WITH_ENABLED_FIRST; + } + } + + case SORT_ALARM_BY_DESCENDING_CREATION_ORDER -> { + if (areEnabledAlarmsFirst) { + sortOrder = SORT_ORDER_BY_DESCENDING_CREATION_WITH_ENABLED_FIRST; + } else { + sortOrder = SORT_ORDER_BY_DESCENDING_CREATION; + } + } + + case SORT_ALARM_BY_ASCENDING_CREATION_ORDER -> { + if (areEnabledAlarmsFirst) { + sortOrder = SORT_ORDER_BY_ASCENDING_CREATION_WITH_ENABLED_FIRST; + } else { + sortOrder = SORT_ORDER_BY_ASCENDING_CREATION; + } + } + } + return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI, - QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) { + QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, sortOrder) { @Override public Cursor loadInBackground() { // Prime the ringtone title cache for later access. Most alarms will refer to @@ -380,17 +516,17 @@ public static List getAlarms(ContentResolver cr, String selection, return result; } - public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) { - ContentValues values = createContentValues(alarm); + public Alarm addAlarm(ContentResolver contentResolver) { + ContentValues values = createContentValues(); Uri uri = contentResolver.insert(CONTENT_URI, values); - alarm.id = getId(uri); - return alarm; + id = getId(uri); + return this; } - public static void updateAlarm(ContentResolver contentResolver, Alarm alarm) { - if (alarm.id == Alarm.INVALID_ID) return; - ContentValues values = createContentValues(alarm); - contentResolver.update(getContentUri(alarm.id), values, null, null); + public void updateAlarm(ContentResolver contentResolver) { + if (id == Alarm.INVALID_ID) return; + ContentValues values = createContentValues(); + contentResolver.update(getContentUri(id), values, null, null); } public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) { @@ -404,19 +540,58 @@ public String getLabelOrDefault(Context context) { } /** - * Whether the alarm is in a state to show preemptive dismiss. Valid states are - * SNOOZE_STATE or NOTIFICATION_STATE. + * Determines whether the alarm is eligible to show a preemptive dismiss button. + *

+ * The behavior depends on user settings and the current alarm state:

+ *
    + *
  • If the dismiss button is configured to be shown when the alarm is enabled, + * the method returns {@code true} if the alarm is enabled or currently snoozed.
  • + *
  • Otherwise, it returns true only if the alarm is in SNOOZE_STATE or NOTIFICATION_STATE.
  • + *
+ * @param context the context used to access shared preferences + * @return {@code true} if the alarm can show a preemptive dismiss button; {@code false} otherwise. + */ + public boolean canPreemptivelyDismiss(Context context) { + if (SettingsDAO.isDismissButtonDisplayedWhenAlarmEnabled(getDefaultSharedPreferences(context))) { + return enabled || instanceState == AlarmInstance.SNOOZE_STATE; + } else { + return instanceState == AlarmInstance.SNOOZE_STATE || instanceState == AlarmInstance.NOTIFICATION_STATE; + } + } + + /** + * @return {@code true} if the styled repeat day display is enabled for this alarm; + * {@code false} otherwise. */ - public boolean canPreemptivelyDismiss() { - return instanceState == AlarmInstance.SNOOZE_STATE || instanceState == AlarmInstance.NOTIFICATION_STATE; + public boolean isRepeatDayStyleEnabled(SharedPreferences prefs) { + return prefs.getBoolean(KEY_SHOW_STYLED_REPEAT_DAY + id, false); } - public static boolean isTomorrow(Alarm alarm, Calendar now) { - if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) { + /** + * Enables the styled repeat day display for this alarm only if all days are selected. + */ + public void enableRepeatDayStyleIfAllDaysSelected(SharedPreferences prefs) { + if (!daysOfWeek.isAllDaysSelected()) { + return; + } + + prefs.edit().putBoolean(KEY_SHOW_STYLED_REPEAT_DAY + id, true).apply(); + } + + /** + * Removes the styled repeat day display preference for this alarm. + * This disables the styled repeat day behavior. + */ + public void removeRepeatDayStyle(SharedPreferences prefs) { + prefs.edit().remove(KEY_SHOW_STYLED_REPEAT_DAY + id).apply(); + } + + public boolean isTomorrow(Calendar now) { + if (instanceState == AlarmInstance.SNOOZE_STATE) { return false; } - final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes; + final int totalAlarmMinutes = hour * 60 + minutes; final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE); return totalAlarmMinutes <= totalNowMinutes; @@ -459,34 +634,25 @@ public static boolean isSpecifiedDateTomorrow(int alarmYear, int alarmMonth, int alarmDayOfMonth == tomorrow.get(Calendar.DAY_OF_MONTH); } - public void writeToParcel(Parcel p, int flags) { - p.writeLong(id); - p.writeInt(enabled ? 1 : 0); - p.writeInt(year); - p.writeInt(month); - p.writeInt(day); - p.writeInt(hour); - p.writeInt(minutes); - p.writeInt(daysOfWeek.getBits()); - p.writeInt(vibrate ? 1 : 0); - p.writeInt(flash ? 1 : 0); - p.writeString(label); - p.writeParcelable(alert, flags); - p.writeInt(deleteAfterUse ? 1 : 0); - p.writeInt(autoSilenceDuration); - p.writeInt(snoozeDuration); - p.writeInt(crescendoDuration); - p.writeInt(alarmVolume); + public boolean isTimeBeforeOrEqual(Calendar referenceTime) { + int currentHour = referenceTime.get(Calendar.HOUR_OF_DAY); + int currentMinute = referenceTime.get(Calendar.MINUTE); + + return hour < currentHour || (hour == currentHour && minutes <= currentMinute); } - public int describeContents() { - return 0; + public boolean isScheduledForToday(Calendar reference) { + int currentMonth = reference.get(Calendar.MONTH); + return year == reference.get(Calendar.YEAR) + && month == currentMonth + && day == reference.get(Calendar.DAY_OF_MONTH); } public AlarmInstance createInstanceAfter(Calendar time) { Calendar nextInstanceTime = getNextAlarmTime(time); AlarmInstance result = new AlarmInstance(nextInstanceTime, id); result.mVibrate = vibrate; + result.mVibrationPattern = vibrationPattern; result.mFlash = flash; result.mLabel = label; result.mRingtone = RingtoneUtils.isRandomRingtone(alert) @@ -496,6 +662,7 @@ public AlarmInstance createInstanceAfter(Calendar time) { : alert; result.mAutoSilenceDuration = autoSilenceDuration; result.mSnoozeDuration = snoozeDuration; + result.mMissedAlarmRepeatLimit = missedAlarmRepeatLimit; result.mCrescendoDuration = crescendoDuration; result.mAlarmVolume = alarmVolume; return result; @@ -524,6 +691,19 @@ public Calendar getPreviousAlarmTime(Calendar currentTime) { } } + /** + * Calculates the next scheduled occurrence time. + * + *

This method determines when the alarm should trigger again based on its + * configuration. It handles both repeating alarms (with specific days of the week) + * and one-time alarms (with a fixed date). Daylight Savings Time (DST) adjustments + * are also taken into account by resetting the hour and minute after shifting days. + * + * @return a {@link Calendar} instance representing the next valid alarm time. + *

- For repeating alarms: the next valid day of the week at the configured hour/minute.

+ *

- For one-time alarms: the configured date and time, or the following day if the + * specified time has already passed relative to {@code currentTime}.

+ */ public Calendar getNextAlarmTime(Calendar currentTime) { final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone()); nextInstanceTime.set(Calendar.SECOND, 0); @@ -565,6 +745,88 @@ public Calendar getNextAlarmTime(Calendar currentTime) { return nextInstanceTime; } + /** + * Returns the day of the week (as Calendar.DAY_OF_WEEK) when the alarm will next trigger. + *

+ * If a valid AlarmInstance is provided and its scheduled time is in the future, + * that time is used to determine the next alarm day. + * Otherwise, the method calculates the next scheduled alarm time based on the current time + * and the alarm's repeat settings.

+ * + * @param alarmInstance the current AlarmInstance, or null if not yet created + * @return the day of the week (e.g., Calendar.MONDAY, Calendar.TUESDAY, ...) + */ + public int getNextAlarmDayOfWeek(AlarmInstance alarmInstance) { + Calendar referenceTime = Calendar.getInstance(); + Calendar nextAlarmTime; + + if (alarmInstance != null && alarmInstance.getAlarmTime().after(referenceTime)) { + nextAlarmTime = alarmInstance.getAlarmTime(); + } else { + nextAlarmTime = getNextAlarmTime(referenceTime); + } + + return nextAlarmTime.get(Calendar.DAY_OF_WEEK); + } + + /** + * Returns the next alarm time for sorting purposes. + */ + public Calendar getSortableNextAlarmTime(AlarmInstance instance, Calendar now) { + Calendar result = Calendar.getInstance(now.getTimeZone()); + result.set(Calendar.SECOND, 0); + result.set(Calendar.MILLISECOND, 0); + + if (daysOfWeek.isRepeating()) { + // If a future instance exists (e.g. after Dismiss), use it. + // Otherwise compute the next valid occurrence from "now". + if (instance != null && instance.getAlarmTime().getTimeInMillis() > now.getTimeInMillis()) { + return instance.getAlarmTime(); + } + + return getNextAlarmTime(now); + } else { + if (isSpecifiedDate()) { + if (isDateInThePast()) { + // Expired specific date → anchor to today at the alarm's time + result.set(Calendar.YEAR, now.get(Calendar.YEAR)); + result.set(Calendar.MONTH, now.get(Calendar.MONTH)); + result.set(Calendar.DAY_OF_MONTH, now.get(Calendar.DAY_OF_MONTH)); + result.set(Calendar.HOUR_OF_DAY, hour); + result.set(Calendar.MINUTE, minutes); + + // If the time has already passed today, shift to tomorrow + if (result.getTimeInMillis() < now.getTimeInMillis()) { + result.add(Calendar.DAY_OF_YEAR, 1); + } + } else { + // Future or today’s specified date → respect the defined date/time + result.set(Calendar.YEAR, year); + result.set(Calendar.MONTH, month); + result.set(Calendar.DAY_OF_MONTH, day); + result.set(Calendar.HOUR_OF_DAY, hour); + result.set(Calendar.MINUTE, minutes); + } + + return result; + } + } + + // Alarms with no date and no repetition → today at the alarm time, + // and if the time has passed, shift to tomorrow + result.set(Calendar.YEAR, now.get(Calendar.YEAR)); + result.set(Calendar.MONTH, now.get(Calendar.MONTH)); + result.set(Calendar.DAY_OF_MONTH, now.get(Calendar.DAY_OF_MONTH)); + result.set(Calendar.HOUR_OF_DAY, hour); + result.set(Calendar.MINUTE, minutes); + + if (result.getTimeInMillis() < now.getTimeInMillis()) { + result.add(Calendar.DAY_OF_YEAR, 1); + } + + return result; + } + @Override public boolean equals(Object o) { if (!(o instanceof final Alarm other)) return false; @@ -590,11 +852,13 @@ public String toString() { ", minutes=" + minutes + ", daysOfWeek=" + daysOfWeek + ", vibrate=" + vibrate + + ", vibrationPattern=" + vibrationPattern + ", flash=" + flash + ", label='" + label + '\'' + ", deleteAfterUse=" + deleteAfterUse + ", autoSilenceDuration=" + autoSilenceDuration + ", snoozeDuration=" + snoozeDuration + + ", missedAlarmRepeatLimit=" + missedAlarmRepeatLimit + ", crescendoDuration=" + crescendoDuration + ", alarmVolume=" + alarmVolume + '}'; diff --git a/app/src/main/java/com/best/deskclock/provider/AlarmInstance.java b/app/src/main/java/com/best/deskclock/provider/AlarmInstance.java index b1a612131..1c04461ef 100644 --- a/app/src/main/java/com/best/deskclock/provider/AlarmInstance.java +++ b/app/src/main/java/com/best/deskclock/provider/AlarmInstance.java @@ -7,8 +7,14 @@ package com.best.deskclock.provider; import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_END_OF_RINGTONE; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_NEVER; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_SNOOZE_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_VOLUME; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_AUTO_SILENCE_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_MISSED_ALARM_REPEAT_LIMIT; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VIBRATION_PATTERN; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VOLUME_CRESCENDO_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; import android.content.ContentResolver; import android.content.ContentUris; @@ -41,7 +47,8 @@ public final class AlarmInstance implements ClockContract.InstancesColumns { /** * Offset from alarm time to stop showing missed notification. */ - private static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12; + public static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12; + private static final String[] QUERY_COLUMNS = { _ID, YEAR, @@ -51,12 +58,15 @@ public final class AlarmInstance implements ClockContract.InstancesColumns { MINUTES, LABEL, VIBRATE, + VIBRATION_PATTERN, FLASH, RINGTONE, ALARM_ID, ALARM_STATE, AUTO_SILENCE_DURATION, SNOOZE_DURATION, + MISSED_ALARM_REPEAT_COUNT, + MISSED_ALARM_REPEAT_LIMIT, CRESCENDO_DURATION, ALARM_VOLUME }; @@ -73,14 +83,17 @@ public final class AlarmInstance implements ClockContract.InstancesColumns { private static final int MINUTES_INDEX = 5; private static final int LABEL_INDEX = 6; private static final int VIBRATE_INDEX = 7; - private static final int FLASH_INDEX = 8; - private static final int RINGTONE_INDEX = 9; - private static final int ALARM_ID_INDEX = 10; - private static final int ALARM_STATE_INDEX = 11; - private static final int AUTO_SILENCE_DURATION_INDEX = 12; - private static final int SNOOZE_DURATION_INDEX = 13; - private static final int CRESCENDO_DURATION_INDEX = 14; - private static final int ALARM_VOLUME_INDEX = 15; + private static final int VIBRATION_PATTERN_INDEX = 8; + private static final int FLASH_INDEX = 9; + private static final int RINGTONE_INDEX = 10; + private static final int ALARM_ID_INDEX = 11; + private static final int ALARM_STATE_INDEX = 12; + private static final int AUTO_SILENCE_DURATION_INDEX = 13; + private static final int SNOOZE_DURATION_INDEX = 14; + private static final int MISSED_ALARM_REPEAT_COUNT_INDEX = 15; + private static final int MISSED_ALARM_MAX_COUNT_INDEX = 16; + private static final int CRESCENDO_DURATION_INDEX = 17; + private static final int ALARM_VOLUME_INDEX = 18; private static final int COLUMN_COUNT = ALARM_VOLUME_INDEX + 1; // Public fields @@ -92,12 +105,15 @@ public final class AlarmInstance implements ClockContract.InstancesColumns { public int mMinute; public String mLabel; public boolean mVibrate; + public String mVibrationPattern; public boolean mFlash; public Uri mRingtone; public Long mAlarmId; public int mAlarmState; public int mAutoSilenceDuration; public int mSnoozeDuration; + public int mMissedAlarmCurrentCount; + public int mMissedAlarmRepeatLimit; public int mCrescendoDuration; // Alarm volume level in steps; not a percentage public int mAlarmVolume; @@ -112,13 +128,16 @@ public AlarmInstance(Calendar calendar) { setAlarmTime(calendar); mLabel = ""; mVibrate = false; + mVibrationPattern = DEFAULT_VIBRATION_PATTERN; mFlash = false; mRingtone = null; mAlarmState = SILENT_STATE; - mAutoSilenceDuration = 10; - mSnoozeDuration = 10; - mCrescendoDuration = 0; - mAlarmVolume = 11; + mAutoSilenceDuration = DEFAULT_AUTO_SILENCE_DURATION; + mSnoozeDuration = DEFAULT_ALARM_SNOOZE_DURATION; + mMissedAlarmCurrentCount = 0; + mMissedAlarmRepeatLimit = Integer.parseInt(DEFAULT_MISSED_ALARM_REPEAT_LIMIT); + mCrescendoDuration = DEFAULT_VOLUME_CRESCENDO_DURATION; + mAlarmVolume = DEFAULT_ALARM_VOLUME; } public AlarmInstance(AlarmInstance instance) { @@ -130,12 +149,15 @@ public AlarmInstance(AlarmInstance instance) { this.mMinute = instance.mMinute; this.mLabel = instance.mLabel; this.mVibrate = instance.mVibrate; + this.mVibrationPattern = instance.mVibrationPattern; this.mFlash = instance.mFlash; this.mRingtone = instance.mRingtone; this.mAlarmId = instance.mAlarmId; this.mAlarmState = instance.mAlarmState; this.mAutoSilenceDuration = instance.mAutoSilenceDuration; this.mSnoozeDuration = instance.mSnoozeDuration; + this.mMissedAlarmCurrentCount = instance.mMissedAlarmCurrentCount; + this.mMissedAlarmRepeatLimit = instance.mMissedAlarmRepeatLimit; this.mCrescendoDuration = instance.mCrescendoDuration; this.mAlarmVolume = instance.mAlarmVolume; } @@ -150,9 +172,12 @@ public AlarmInstance(Cursor c, boolean joinedTable) { mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX); mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX); mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1; + mVibrationPattern = c.getString(Alarm.INSTANCE_VIBRATION_PATTERN_INDEX); mFlash = c.getInt(Alarm.INSTANCE_FLASH_INDEX) == 1; mAutoSilenceDuration = c.getInt(Alarm.INSTANCE_AUTO_SILENCE_DURATION_INDEX); mSnoozeDuration = c.getInt(Alarm.INSTANCE_SNOOZE_DURATION_INDEX); + mMissedAlarmCurrentCount = c.getInt(Alarm.INSTANCE_MISSED_ALARM_REPEAT_COUNT_INDEX); + mMissedAlarmRepeatLimit = c.getInt(Alarm.INSTANCE_MISSED_ALARM_REPEAT_LIMIT_INDEX); mCrescendoDuration = c.getInt(Alarm.INSTANCE_CRESCENDO_DURATION_INDEX); mAlarmVolume = c.getInt(Alarm.INSTANCE_ALARM_VOLUME_INDEX); } else { @@ -164,9 +189,12 @@ public AlarmInstance(Cursor c, boolean joinedTable) { mMinute = c.getInt(MINUTES_INDEX); mLabel = c.getString(LABEL_INDEX); mVibrate = c.getInt(VIBRATE_INDEX) == 1; + mVibrationPattern = c.getString(VIBRATION_PATTERN_INDEX); mFlash = c.getInt(FLASH_INDEX) == 1; mAutoSilenceDuration = c.getInt(AUTO_SILENCE_DURATION_INDEX); mSnoozeDuration = c.getInt(SNOOZE_DURATION_INDEX); + mMissedAlarmCurrentCount = c.getInt(MISSED_ALARM_REPEAT_COUNT_INDEX); + mMissedAlarmRepeatLimit = c.getInt(MISSED_ALARM_MAX_COUNT_INDEX); mCrescendoDuration = c.getInt(CRESCENDO_DURATION_INDEX); mAlarmVolume = c.getInt(ALARM_VOLUME_INDEX); } @@ -184,33 +212,36 @@ public AlarmInstance(Cursor c, boolean joinedTable) { mAlarmState = c.getInt(ALARM_STATE_INDEX); } - public static ContentValues createContentValues(AlarmInstance instance) { + public ContentValues createContentValues() { ContentValues values = new ContentValues(COLUMN_COUNT); - if (instance.mId != INVALID_ID) { - values.put(_ID, instance.mId); + if (mId != INVALID_ID) { + values.put(_ID, mId); } - values.put(YEAR, instance.mYear); - values.put(MONTH, instance.mMonth); - values.put(DAY, instance.mDay); - values.put(HOUR, instance.mHour); - values.put(MINUTES, instance.mMinute); - values.put(LABEL, instance.mLabel); - values.put(VIBRATE, instance.mVibrate ? 1 : 0); - values.put(FLASH, instance.mFlash ? 1 : 0); - if (instance.mRingtone == null) { + values.put(YEAR, mYear); + values.put(MONTH, mMonth); + values.put(DAY, mDay); + values.put(HOUR, mHour); + values.put(MINUTES, mMinute); + values.put(LABEL, mLabel); + values.put(VIBRATE, mVibrate ? 1 : 0); + values.put(VIBRATION_PATTERN, mVibrationPattern); + values.put(FLASH, mFlash ? 1 : 0); + if (mRingtone == null) { // We want to put null in the database, so we'll be able // to pick up on changes to the default alarm values.putNull(RINGTONE); } else { - values.put(RINGTONE, instance.mRingtone.toString()); + values.put(RINGTONE, mRingtone.toString()); } - values.put(ALARM_ID, instance.mAlarmId); - values.put(ALARM_STATE, instance.mAlarmState); - values.put(AUTO_SILENCE_DURATION, instance.mAutoSilenceDuration); - values.put(SNOOZE_DURATION, instance.mSnoozeDuration); - values.put(CRESCENDO_DURATION, instance.mCrescendoDuration); - values.put(ALARM_VOLUME, instance.mAlarmVolume); + values.put(ALARM_ID, mAlarmId); + values.put(ALARM_STATE, mAlarmState); + values.put(AUTO_SILENCE_DURATION, mAutoSilenceDuration); + values.put(SNOOZE_DURATION, mSnoozeDuration); + values.put(MISSED_ALARM_REPEAT_COUNT, mMissedAlarmCurrentCount); + values.put(MISSED_ALARM_REPEAT_LIMIT, mMissedAlarmRepeatLimit); + values.put(CRESCENDO_DURATION, mCrescendoDuration); + values.put(ALARM_VOLUME, mAlarmVolume); return values; } @@ -315,31 +346,30 @@ public static List getInstances(ContentResolver cr, String select return result; } - public static void addInstance(ContentResolver contentResolver, - AlarmInstance instance) { + public void addInstance(ContentResolver contentResolver) { // Make sure we are not adding a duplicate instances. This is not a // fix and should never happen. This is only a safe guard against bad code, and you // should fix the root issue if you see the error message. - String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId; + String dupSelector = AlarmInstance.ALARM_ID + " = " + mAlarmId; for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) { - if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) { - LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to " + instance); + if (otherInstances.getAlarmTime().equals(getAlarmTime())) { + LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to " + this); // Copy over the new instance values and update the db - instance.mId = otherInstances.mId; - updateInstance(contentResolver, instance); + mId = otherInstances.mId; + updateInstance(contentResolver); return; } } - ContentValues values = createContentValues(instance); + ContentValues values = createContentValues(); Uri uri = contentResolver.insert(CONTENT_URI, values); - instance.mId = getId(uri); + mId = getId(uri); } - public static void updateInstance(ContentResolver contentResolver, AlarmInstance instance) { - if (instance.mId == INVALID_ID) return; - ContentValues values = createContentValues(instance); - contentResolver.update(getContentUri(instance.mId), values, null, null); + public void updateInstance(ContentResolver contentResolver) { + if (mId == INVALID_ID) return; + ContentValues values = createContentValues(); + contentResolver.update(getContentUri(mId), values, null, null); } public static void deleteInstance(ContentResolver contentResolver, long instanceId) { @@ -418,16 +448,16 @@ public Calendar getMissedTimeToLive() { public Calendar getTimeout(Context context) { Calendar calendar = getAlarmTime(); - // Alarm silence has been set to "Never" - if (mAutoSilenceDuration == ALARM_TIMEOUT_NEVER) { + if (mAutoSilenceDuration == TIMEOUT_NEVER) { + // Alarm silence has been set to "Never" return null; - // Alarm silence has been set to "At the end of the ringtone" - // or "Dismiss alarm when ringtone ends" has been ticked in the expanded alarm view - } else if (mAutoSilenceDuration == ALARM_TIMEOUT_END_OF_RINGTONE) { + } else if (mAutoSilenceDuration == TIMEOUT_END_OF_RINGTONE) { + // Alarm silence has been set to "At the end of the ringtone" + // or "Dismiss alarm when ringtone ends" has been ticked in the expanded alarm view int milliSeconds = RingtoneUtils.getRingtoneDuration(context, mRingtone); calendar.add(Calendar.MILLISECOND, milliSeconds); } else { - calendar.add(Calendar.MINUTE, mAutoSilenceDuration); + calendar.add(Calendar.SECOND, mAutoSilenceDuration); } return calendar; @@ -456,12 +486,15 @@ public String toString() { ", mMinute=" + mMinute + ", mLabel=" + mLabel + ", mVibrate=" + mVibrate + + ", mVibrationPattern=" + mVibrationPattern + ", mFlash=" + mFlash + ", mRingtone=" + mRingtone + ", mAlarmId=" + mAlarmId + ", mAlarmState=" + mAlarmState + ", mAutoSilenceDuration=" + mAutoSilenceDuration + ", mSnoozeDuration=" + mSnoozeDuration + + ", mMissedAlarmCurrentCount=" + mMissedAlarmCurrentCount + + ", mMissedAlarmRepeatLimit=" + mMissedAlarmRepeatLimit + ", mCrescendoDuration=" + mCrescendoDuration + ", mAlarmVolume=" + mAlarmVolume + '}'; diff --git a/app/src/main/java/com/best/deskclock/provider/ClockContract.java b/app/src/main/java/com/best/deskclock/provider/ClockContract.java index c1e8f4848..b1a229d59 100644 --- a/app/src/main/java/com/best/deskclock/provider/ClockContract.java +++ b/app/src/main/java/com/best/deskclock/provider/ClockContract.java @@ -53,6 +53,13 @@ private interface AlarmSettingColumns extends BaseColumns { */ String VIBRATE = "vibrate"; + /** + * Alarm vibration pattern. + * + *

Type: STRING

+ */ + String VIBRATION_PATTERN = "vibrationPattern"; + /** * True if flash should turn on *

Type: BOOLEAN

@@ -87,6 +94,12 @@ private interface AlarmSettingColumns extends BaseColumns { */ String SNOOZE_DURATION = "snoozeDuration"; + /** + * Missed alarm repeat limit + *

Type: INTEGER

+ */ + String MISSED_ALARM_REPEAT_LIMIT = "missed_alarm_repeat_limit"; + /** * Alarm crescendo duration. *

Type: INTEGER

@@ -278,5 +291,11 @@ protected interface InstancesColumns extends AlarmSettingColumns, BaseColumns { *

Type: INTEGER

*/ String ALARM_STATE = "alarm_state"; + + /** + * Missed alarm repeat count + *

Type: INTEGER

+ */ + String MISSED_ALARM_REPEAT_COUNT = "missed_alarm_repeat_count"; } } diff --git a/app/src/main/java/com/best/deskclock/provider/ClockDatabaseHelper.java b/app/src/main/java/com/best/deskclock/provider/ClockDatabaseHelper.java index 169c048cf..0046a8df5 100644 --- a/app/src/main/java/com/best/deskclock/provider/ClockDatabaseHelper.java +++ b/app/src/main/java/com/best/deskclock/provider/ClockDatabaseHelper.java @@ -180,7 +180,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { // Save new version of alarm and create alarm instance for it db.insert(TEMP_ALARMS_TABLE_NAME, null, Alarm.createContentValues(alarm)); if (alarm.enabled) { - AlarmInstance newInstance = alarm.createInstanceAfter(currentTime); + AlarmInstance newInstance = alarm.createInstanceAfter(context, currentTime); db.insert(TEMP_INSTANCES_TABLE_NAME, null, AlarmInstance.createContentValues(newInstance)); } diff --git a/app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java b/app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java index 73dc120f3..cfdc02327 100644 --- a/app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java +++ b/app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java @@ -1,62 +1,99 @@ // SPDX-License-Identifier: GPL-3.0-only package com.best.deskclock.settings; +import com.best.deskclock.holiday.HolidayRepository; -import static com.best.deskclock.DeskClock.REQUEST_CHANGE_SETTINGS; -import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_TIMEOUT_END_OF_RINGTONE; +import static android.app.Activity.RESULT_OK; +import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_SNOOZE_DURATION_DISABLED; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_VOLUME; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VIBRATION_START_DELAY; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VOLUME_CRESCENDO_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ADVANCED_AUDIO_PLAYBACK; import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_DISPLAY_CUSTOMIZATION; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_FONT; import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_NOTIFICATION_REMINDER_TIME; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_SNOOZE_DURATION; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_VIBRATION_CATEGORY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_VOLUME_CRESCENDO_DURATION; import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_VOLUME_SETTING; -import static com.best.deskclock.settings.PreferencesKeys.KEY_AUTO_ROUTING_TO_BLUETOOTH_DEVICE; -import static com.best.deskclock.settings.PreferencesKeys.KEY_BLUETOOTH_VOLUME; +import static com.best.deskclock.settings.PreferencesKeys.KEY_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_AUTO_SILENCE_DURATION; import static com.best.deskclock.settings.PreferencesKeys.KEY_DEFAULT_ALARM_RINGTONE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_DISPLAY_DISMISS_BUTTON; +import static com.best.deskclock.settings.PreferencesKeys.KEY_DISPLAY_ENABLED_ALARMS_FIRST; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_ALARM_FAB_LONG_PRESS; import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_ALARM_VIBRATIONS_BY_DEFAULT; import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_DELETE_OCCASIONAL_ALARM_BY_DEFAULT; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_PER_ALARM_AUTO_SILENCE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_PER_ALARM_SNOOZE_DURATION; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_PER_ALARM_VIBRATION_PATTERN; import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_PER_ALARM_VOLUME; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION; import static com.best.deskclock.settings.PreferencesKeys.KEY_ENABLE_SNOOZED_OR_DISMISSED_ALARM_VIBRATIONS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_EXTERNAL_AUDIO_DEVICE_VOLUME; import static com.best.deskclock.settings.PreferencesKeys.KEY_FLIP_ACTION; -import static com.best.deskclock.settings.PreferencesKeys.KEY_HOLIDAY_DATA_URL; import static com.best.deskclock.settings.PreferencesKeys.KEY_MATERIAL_DATE_PICKER_STYLE; import static com.best.deskclock.settings.PreferencesKeys.KEY_MATERIAL_TIME_PICKER_STYLE; import static com.best.deskclock.settings.PreferencesKeys.KEY_POWER_BUTTON; -import static com.best.deskclock.settings.PreferencesKeys.KEY_ADVANCED_AUDIO_PLAYBACK; +import static com.best.deskclock.settings.PreferencesKeys.KEY_REPEAT_MISSED_ALARM; import static com.best.deskclock.settings.PreferencesKeys.KEY_SHAKE_ACTION; import static com.best.deskclock.settings.PreferencesKeys.KEY_SHAKE_INTENSITY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SORT_ALARM; import static com.best.deskclock.settings.PreferencesKeys.KEY_SYSTEM_MEDIA_VOLUME; import static com.best.deskclock.settings.PreferencesKeys.KEY_TURN_ON_BACK_FLASH_FOR_TRIGGERED_ALARM; -import static com.best.deskclock.settings.PreferencesKeys.KEY_UPDATE_HOLIDAY_DATA; +import static com.best.deskclock.settings.PreferencesKeys.KEY_VIBRATION_PATTERN; import static com.best.deskclock.settings.PreferencesKeys.KEY_VOLUME_BUTTONS; import static com.best.deskclock.settings.PreferencesKeys.KEY_WEEK_START; -import android.app.Application; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.hardware.Sensor; import android.hardware.SensorManager; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; import androidx.preference.ListPreference; import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; import androidx.preference.SwitchPreferenceCompat; import com.best.deskclock.AlarmSnoozeDurationDialogFragment; import com.best.deskclock.AutoSilenceDurationDialogFragment; import com.best.deskclock.R; +import com.best.deskclock.VibrationPatternDialogFragment; +import com.best.deskclock.VibrationStartDelayDialogFragment; import com.best.deskclock.VolumeCrescendoDurationDialogFragment; import com.best.deskclock.alarms.AlarmUpdateHandler; import com.best.deskclock.data.DataModel; import com.best.deskclock.data.SettingsDAO; import com.best.deskclock.data.Weekdays; -import com.best.deskclock.holiday.HolidayRepository; import com.best.deskclock.provider.Alarm; import com.best.deskclock.ringtone.RingtonePickerActivity; +import com.best.deskclock.settings.custompreference.AlarmSnoozeDurationPreference; +import com.best.deskclock.settings.custompreference.AlarmVolumePreference; +import com.best.deskclock.settings.custompreference.AutoSilenceDurationPreference; +import com.best.deskclock.settings.custompreference.CustomSliderPreference; +import com.best.deskclock.settings.custompreference.VibrationPatternPreference; +import com.best.deskclock.settings.custompreference.VibrationStartDelayPreference; +import com.best.deskclock.settings.custompreference.VolumeCrescendoDurationPreference; +import com.best.deskclock.uicomponents.CustomDialog; +import com.best.deskclock.uicomponents.toast.CustomToast; import com.best.deskclock.utils.AlarmUtils; +import com.best.deskclock.utils.DeviceUtils; import com.best.deskclock.utils.RingtoneUtils; import com.best.deskclock.utils.Utils; @@ -67,21 +104,39 @@ public class AlarmSettingsFragment extends ScreenFragment private AudioManager mAudioManager; private AudioDeviceCallback mAudioDeviceCallback; + private AlarmUpdateHandler mAlarmUpdateHandler; + private List mAlarmList; + Preference mAlarmFontPref; Preference mAlarmRingtonePref; + SwitchPreferenceCompat mEnablePerAlarmAutoSilencePref; + AutoSilenceDurationPreference mAlarmAutoSilencePref; + SwitchPreferenceCompat mEnablePerAlarmSnoozeDurationPref; + AlarmSnoozeDurationPreference mAlarmSnoozeDurationPref; + SwitchPreferenceCompat mEnablePerAlarmMissedRepeatLimitPref; + ListPreference mRepeatMissedAlarmPref; SwitchPreferenceCompat mEnablePerAlarmVolumePref; AlarmVolumePreference mAlarmVolumePref; + SwitchPreferenceCompat mEnablePerAlarmVolumeCrescendoDurationPref; + VolumeCrescendoDurationPreference mAlarmVolumeCrescendoDurationPref; SwitchPreferenceCompat mAdvancedAudioPlaybackPref; - SwitchPreferenceCompat mAutoRoutingToBluetoothDevicePref; + SwitchPreferenceCompat mAutoRoutingToExternalAudioDevicePref; SwitchPreferenceCompat mSystemMediaVolume; - CustomSeekbarPreference mBluetoothVolumePref; + CustomSliderPreference mExternalAudioDeviceVolumePref; + PreferenceCategory mAlarmVibrationCategory; ListPreference mVolumeButtonsPref; ListPreference mPowerButtonPref; ListPreference mFlipActionPref; ListPreference mShakeActionPref; - CustomSeekbarPreference mShakeIntensityPref; + CustomSliderPreference mShakeIntensityPref; + ListPreference mSortAlarmPref; + SwitchPreferenceCompat mDisplayEnabledAlarmsFirstPref; + SwitchPreferenceCompat mEnableAlarmFabLongPressPref; ListPreference mWeekStartPref; + SwitchPreferenceCompat mDisplayDismissButtonPref; ListPreference mAlarmNotificationReminderTimePref; + SwitchPreferenceCompat mEnablePerAlarmVibrationPatternPref; + VibrationPatternPreference mVibrationPatternPref; SwitchPreferenceCompat mEnableAlarmVibrationsByDefaultPref; SwitchPreferenceCompat mEnableSnoozedOrDismissedAlarmVibrationsPref; SwitchPreferenceCompat mTurnOnBackFlashForTriggeredAlarmPref; @@ -89,9 +144,42 @@ public class AlarmSettingsFragment extends ScreenFragment ListPreference mMaterialTimePickerStylePref; ListPreference mMaterialDatePickerStylePref; Preference mAlarmDisplayCustomizationPref; - Preference mUpdateHolidayDataPref; - Preference mHolidayDataUrlPref; - private HolidayRepository mHolidayRepository; + + private final ActivityResultLauncher fontPickerLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() != RESULT_OK) { + return; + } + + Intent intent = result.getData(); + final Uri sourceUri = intent == null ? null : intent.getData(); + if (sourceUri == null) { + return; + } + + // Take persistent permission + requireActivity().getContentResolver().takePersistableUriPermission( + sourceUri, Intent.FLAG_GRANT_READ_URI_PERMISSION + ); + + String safeTitle = Utils.toSafeFileName("alarm_font"); + + // Delete the old font if it exists + clearFile(mPrefs.getString(KEY_ALARM_FONT, null)); + + Uri copiedUri = Utils.copyFileToDeviceProtectedStorage(requireContext(), sourceUri, safeTitle); + + // Save the new path + if (copiedUri != null) { + mPrefs.edit().putString(KEY_ALARM_FONT, copiedUri.getPath()).apply(); + mAlarmFontPref.setTitle(getString(R.string.custom_font_title_variant)); + + CustomToast.show(requireContext(), R.string.custom_font_toast_message_selected); + } else { + CustomToast.show(requireContext(), "Error importing font"); + mAlarmFontPref.setTitle(getString(R.string.custom_font_title)); + } + }); @Override protected String getFragmentTitle() { @@ -102,34 +190,49 @@ protected String getFragmentTitle() { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.settings_alarm); + final ContentResolver contentResolver = requireContext().getContentResolver(); + mAlarmUpdateHandler = new AlarmUpdateHandler(requireContext(), null, null); + mAlarmList = Alarm.getAlarms(contentResolver, null); - final Application application = requireActivity().getApplication(); - mHolidayRepository = new HolidayRepository(application); + addPreferencesFromResource(R.xml.settings_alarm); + mAlarmDisplayCustomizationPref = findPreference(KEY_ALARM_DISPLAY_CUSTOMIZATION); + mAlarmFontPref = findPreference(KEY_ALARM_FONT); + mMaterialTimePickerStylePref = findPreference(KEY_MATERIAL_TIME_PICKER_STYLE); + mMaterialDatePickerStylePref = findPreference(KEY_MATERIAL_DATE_PICKER_STYLE); mAlarmRingtonePref = findPreference(KEY_DEFAULT_ALARM_RINGTONE); + mEnablePerAlarmAutoSilencePref = findPreference(KEY_ENABLE_PER_ALARM_AUTO_SILENCE); + mAlarmAutoSilencePref = findPreference(KEY_AUTO_SILENCE_DURATION); + mEnablePerAlarmSnoozeDurationPref = findPreference(KEY_ENABLE_PER_ALARM_SNOOZE_DURATION); + mAlarmSnoozeDurationPref = findPreference(KEY_ALARM_SNOOZE_DURATION); + mEnablePerAlarmMissedRepeatLimitPref = findPreference(KEY_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT); + mRepeatMissedAlarmPref = findPreference(KEY_REPEAT_MISSED_ALARM); mEnablePerAlarmVolumePref = findPreference(KEY_ENABLE_PER_ALARM_VOLUME); mAlarmVolumePref = findPreference(KEY_ALARM_VOLUME_SETTING); + mEnablePerAlarmVolumeCrescendoDurationPref = findPreference(KEY_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION); + mAlarmVolumeCrescendoDurationPref = findPreference(KEY_ALARM_VOLUME_CRESCENDO_DURATION); mAdvancedAudioPlaybackPref = findPreference(KEY_ADVANCED_AUDIO_PLAYBACK); - mAutoRoutingToBluetoothDevicePref = findPreference(KEY_AUTO_ROUTING_TO_BLUETOOTH_DEVICE); + mAutoRoutingToExternalAudioDevicePref = findPreference(KEY_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE); mSystemMediaVolume = findPreference(KEY_SYSTEM_MEDIA_VOLUME); - mBluetoothVolumePref = findPreference(KEY_BLUETOOTH_VOLUME); + mExternalAudioDeviceVolumePref = findPreference(KEY_EXTERNAL_AUDIO_DEVICE_VOLUME); + mAlarmVibrationCategory = findPreference(KEY_ALARM_VIBRATION_CATEGORY); mVolumeButtonsPref = findPreference(KEY_VOLUME_BUTTONS); mPowerButtonPref = findPreference(KEY_POWER_BUTTON); mFlipActionPref = findPreference(KEY_FLIP_ACTION); mShakeActionPref = findPreference(KEY_SHAKE_ACTION); mShakeIntensityPref = findPreference(KEY_SHAKE_INTENSITY); + mSortAlarmPref = findPreference(KEY_SORT_ALARM); + mDisplayEnabledAlarmsFirstPref = findPreference(KEY_DISPLAY_ENABLED_ALARMS_FIRST); + mEnableAlarmFabLongPressPref = findPreference(KEY_ENABLE_ALARM_FAB_LONG_PRESS); mWeekStartPref = findPreference(KEY_WEEK_START); + mDisplayDismissButtonPref = findPreference(KEY_DISPLAY_DISMISS_BUTTON); mAlarmNotificationReminderTimePref = findPreference(KEY_ALARM_NOTIFICATION_REMINDER_TIME); + mEnablePerAlarmVibrationPatternPref = findPreference(KEY_ENABLE_PER_ALARM_VIBRATION_PATTERN); + mVibrationPatternPref = findPreference(KEY_VIBRATION_PATTERN); mEnableAlarmVibrationsByDefaultPref = findPreference(KEY_ENABLE_ALARM_VIBRATIONS_BY_DEFAULT); mEnableSnoozedOrDismissedAlarmVibrationsPref = findPreference(KEY_ENABLE_SNOOZED_OR_DISMISSED_ALARM_VIBRATIONS); mTurnOnBackFlashForTriggeredAlarmPref = findPreference(KEY_TURN_ON_BACK_FLASH_FOR_TRIGGERED_ALARM); mDeleteOccasionalAlarmByDefaultPref = findPreference(KEY_ENABLE_DELETE_OCCASIONAL_ALARM_BY_DEFAULT); - mMaterialTimePickerStylePref = findPreference(KEY_MATERIAL_TIME_PICKER_STYLE); - mMaterialDatePickerStylePref = findPreference(KEY_MATERIAL_DATE_PICKER_STYLE); - mAlarmDisplayCustomizationPref = findPreference(KEY_ALARM_DISPLAY_CUSTOMIZATION); - mUpdateHolidayDataPref = findPreference(KEY_UPDATE_HOLIDAY_DATA); - mHolidayDataUrlPref = findPreference(KEY_HOLIDAY_DATA_URL); setupPreferences(); } @@ -140,12 +243,12 @@ public void onResume() { mAlarmRingtonePref.setSummary(DataModel.getDataModel().getAlarmRingtoneTitle()); - if (RingtoneUtils.hasBluetoothDeviceConnected(requireContext(), mPrefs)) { - mAlarmVolumePref.setTitle(R.string.disconnect_bluetooth_device_title); - mBluetoothVolumePref.setTitle(R.string.bluetooth_volume_title); + if (RingtoneUtils.hasExternalAudioDeviceConnected(requireContext(), mPrefs)) { + mAlarmVolumePref.setTitle(R.string.disconnect_external_audio_device_title); + mExternalAudioDeviceVolumePref.setTitle(R.string.external_audio_device_volume_title); } else { mAlarmVolumePref.setTitle(R.string.alarm_volume_title); - mBluetoothVolumePref.setTitle(R.string.connect_bluetooth_device_title); + mExternalAudioDeviceVolumePref.setTitle(R.string.connect_external_audio_device_title); } if (mAudioDeviceCallback == null) { @@ -168,46 +271,172 @@ public void onStop() { @Override public boolean onPreferenceChange(Preference pref, Object newValue) { switch (pref.getKey()) { - case KEY_ENABLE_ALARM_VIBRATIONS_BY_DEFAULT, + case KEY_DISPLAY_ENABLED_ALARMS_FIRST, KEY_ENABLE_ALARM_FAB_LONG_PRESS, + KEY_DISPLAY_DISMISS_BUTTON, KEY_ENABLE_ALARM_VIBRATIONS_BY_DEFAULT, KEY_ENABLE_SNOOZED_OR_DISMISSED_ALARM_VIBRATIONS, KEY_TURN_ON_BACK_FLASH_FOR_TRIGGERED_ALARM, KEY_ENABLE_DELETE_OCCASIONAL_ALARM_BY_DEFAULT -> Utils.setVibrationTime(requireContext(), 50); + case KEY_ENABLE_PER_ALARM_AUTO_SILENCE -> { + Utils.setVibrationTime(requireContext(), 50); + + if ((boolean) newValue) { + mAlarmAutoSilencePref.setVisible(false); + + for (Alarm alarm : mAlarmList) { + alarm.autoSilenceDuration = SettingsDAO.getAlarmTimeout(mPrefs); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } else { + showDisablePerAlarmSettingDialog(R.string.enable_per_alarm_auto_silence_dialog_message, + KEY_ENABLE_PER_ALARM_AUTO_SILENCE, mEnablePerAlarmAutoSilencePref, + mAlarmAutoSilencePref, alarm -> + alarm.autoSilenceDuration = SettingsDAO.getAlarmTimeout(mPrefs)); + + return false; + } + } + + case KEY_ENABLE_PER_ALARM_SNOOZE_DURATION -> { + Utils.setVibrationTime(requireContext(), 50); + + if ((boolean) newValue) { + mAlarmSnoozeDurationPref.setVisible(false); + + for (Alarm alarm : mAlarmList) { + alarm.snoozeDuration = SettingsDAO.getSnoozeLength(mPrefs); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } else { + showDisablePerAlarmSettingDialog(R.string.enable_per_alarm_snooze_duration_dialog_message, + KEY_ENABLE_PER_ALARM_SNOOZE_DURATION, mEnablePerAlarmSnoozeDurationPref, + mAlarmSnoozeDurationPref, alarm -> + alarm.snoozeDuration = SettingsDAO.getSnoozeLength(mPrefs)); + + return false; + } + } + + case KEY_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT -> { + Utils.setVibrationTime(requireContext(), 50); + + if ((boolean) newValue) { + mRepeatMissedAlarmPref.setVisible(false); + + for (Alarm alarm : mAlarmList) { + alarm.missedAlarmRepeatLimit = SettingsDAO.getMissedAlarmRepeatLimit(mPrefs); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } else { + showDisablePerAlarmSettingDialog(R.string.enable_per_alarm_missed_repeat_limit_dialog_message, + KEY_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT, mEnablePerAlarmMissedRepeatLimitPref, + mRepeatMissedAlarmPref, alarm -> + alarm.missedAlarmRepeatLimit = SettingsDAO.getMissedAlarmRepeatLimit(mPrefs)); + + return false; + } + } + + case KEY_REPEAT_MISSED_ALARM -> { + final int index = mRepeatMissedAlarmPref.findIndexOfValue((String) newValue); + mRepeatMissedAlarmPref.setSummary(mRepeatMissedAlarmPref.getEntries()[index]); + + for (Alarm alarm : mAlarmList) { + alarm.missedAlarmRepeatLimit = Integer.parseInt((String) newValue); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } + case KEY_ENABLE_PER_ALARM_VOLUME -> { stopRingtonePreview(); - mAlarmVolumePref.setVisible(!(boolean) newValue); + + Utils.setVibrationTime(requireContext(), 50); + + if ((boolean) newValue) { + mAlarmVolumePref.setVisible(false); + + for (Alarm alarm : mAlarmList) { + alarm.alarmVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } else { + showDisablePerAlarmSettingDialog(R.string.enable_per_alarm_volume_dialog_message, + KEY_ENABLE_PER_ALARM_VOLUME, mEnablePerAlarmVolumePref, mAlarmVolumePref, + alarm -> alarm.alarmVolume = DEFAULT_ALARM_VOLUME); + + return false; + } + } + + case KEY_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION -> { + Utils.setVibrationTime(requireContext(), 50); + + if ((boolean) newValue) { + mAlarmVolumeCrescendoDurationPref.setVisible(false); + + for (Alarm alarm : mAlarmList) { + alarm.crescendoDuration = SettingsDAO.getAlarmVolumeCrescendoDuration(mPrefs); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } else { + showDisablePerAlarmSettingDialog(R.string.enable_per_alarm_crescendo_duration_dialog_message, + KEY_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION, mEnablePerAlarmVolumeCrescendoDurationPref, + mAlarmVolumeCrescendoDurationPref, alarm -> + alarm.crescendoDuration = SettingsDAO.getAlarmVolumeCrescendoDuration(mPrefs)); + + return false; + } + } + + case KEY_ENABLE_PER_ALARM_VIBRATION_PATTERN -> { Utils.setVibrationTime(requireContext(), 50); - // Set result so DeskClock knows to refresh itself - requireActivity().setResult(REQUEST_CHANGE_SETTINGS); + + if ((boolean) newValue) { + mVibrationPatternPref.setVisible(false); + + for (Alarm alarm : mAlarmList) { + alarm.vibrationPattern = SettingsDAO.getVibrationPattern(mPrefs); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } else { + showDisablePerAlarmSettingDialog(R.string.enable_per_alarm_vibration_pattern_dialog_message, + KEY_ENABLE_PER_ALARM_VIBRATION_PATTERN, mEnablePerAlarmVibrationPatternPref, + mVibrationPatternPref, alarm -> + alarm.vibrationPattern = SettingsDAO.getVibrationPattern(mPrefs)); + + return false; + } } case KEY_ADVANCED_AUDIO_PLAYBACK -> { stopRingtonePreview(); - mAutoRoutingToBluetoothDevicePref.setVisible((boolean) newValue); + mAutoRoutingToExternalAudioDevicePref.setVisible((boolean) newValue); mSystemMediaVolume.setVisible((boolean) newValue - && SettingsDAO.isAutoRoutingToBluetoothDeviceEnabled(mPrefs)); - mBluetoothVolumePref.setVisible((boolean) newValue - && SettingsDAO.isAutoRoutingToBluetoothDeviceEnabled(mPrefs) + && SettingsDAO.isAutoRoutingToExternalAudioDevice(mPrefs)); + mExternalAudioDeviceVolumePref.setVisible((boolean) newValue + && SettingsDAO.isAutoRoutingToExternalAudioDevice(mPrefs) && SettingsDAO.shouldUseCustomMediaVolume(mPrefs)); Utils.setVibrationTime(requireContext(), 50); } - case KEY_AUTO_ROUTING_TO_BLUETOOTH_DEVICE -> { + case KEY_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE -> { stopRingtonePreview(); mSystemMediaVolume.setVisible((boolean) newValue); - mBluetoothVolumePref.setVisible((boolean) newValue && SettingsDAO.shouldUseCustomMediaVolume(mPrefs)); + mExternalAudioDeviceVolumePref.setVisible((boolean) newValue + && SettingsDAO.shouldUseCustomMediaVolume(mPrefs)); Utils.setVibrationTime(requireContext(), 50); } case KEY_SYSTEM_MEDIA_VOLUME -> { stopRingtonePreview(); - mBluetoothVolumePref.setVisible(!(boolean) newValue); + mExternalAudioDeviceVolumePref.setVisible(!(boolean) newValue); Utils.setVibrationTime(requireContext(), 50); } case KEY_VOLUME_BUTTONS, KEY_POWER_BUTTON, KEY_FLIP_ACTION, - KEY_MATERIAL_TIME_PICKER_STYLE, KEY_MATERIAL_DATE_PICKER_STYLE -> { + KEY_MATERIAL_TIME_PICKER_STYLE, KEY_MATERIAL_DATE_PICKER_STYLE, + KEY_SORT_ALARM, KEY_VIBRATION_PATTERN -> { final ListPreference preference = (ListPreference) pref; final int index = preference.findIndexOfValue((String) newValue); preference.setSummary(preference.getEntries()[index]); @@ -217,13 +446,9 @@ public boolean onPreferenceChange(Preference pref, Object newValue) { final int index = mAlarmNotificationReminderTimePref.findIndexOfValue((String) newValue); mAlarmNotificationReminderTimePref.setSummary(mAlarmNotificationReminderTimePref.getEntries()[index]); - ContentResolver cr = requireContext().getContentResolver(); - AlarmUpdateHandler alarmUpdateHandler = new AlarmUpdateHandler(requireContext(), null, null); - List alarms = Alarm.getAlarms(cr, null); - - for (Alarm alarm : alarms) { + for (Alarm alarm : mAlarmList) { if (alarm.enabled) { - alarmUpdateHandler.asyncUpdateAlarm(alarm, false, false); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, false); } } } @@ -238,14 +463,6 @@ public boolean onPreferenceChange(Preference pref, Object newValue) { case KEY_WEEK_START -> { final int index = mWeekStartPref.findIndexOfValue((String) newValue); mWeekStartPref.setSummary(mWeekStartPref.getEntries()[index]); - // Set result so DeskClock knows to refresh itself - requireActivity().setResult(REQUEST_CHANGE_SETTINGS); - } - - case KEY_HOLIDAY_DATA_URL -> { - final String url = (String) newValue; - DataModel.getDataModel().setHolidayDataUrl(url); - pref.setSummary(url); } } @@ -260,12 +477,13 @@ public boolean onPreferenceClick(@NonNull Preference pref) { } switch (pref.getKey()) { - case KEY_DEFAULT_ALARM_RINGTONE -> - startActivity(RingtonePickerActivity.createAlarmRingtonePickerIntentForSettings(context)); - case KEY_ALARM_DISPLAY_CUSTOMIZATION -> animateAndShowFragment(new AlarmDisplayCustomizationFragment()); - case KEY_UPDATE_HOLIDAY_DATA -> mHolidayRepository.updateWorkdayData(); + case KEY_ALARM_FONT -> selectCustomFile(mAlarmFontPref, fontPickerLauncher, + SettingsDAO.getAlarmFont(mPrefs), KEY_ALARM_FONT, true, null); + + case KEY_DEFAULT_ALARM_RINGTONE -> + startActivity(RingtonePickerActivity.createAlarmRingtonePickerIntentForSettings(context)); } return true; @@ -277,18 +495,32 @@ public void onDisplayPreferenceDialog(@NonNull Preference pref) { int currentValue = autoSilenceDurationPreference.getAutoSilenceDuration(); AutoSilenceDurationDialogFragment dialogFragment = AutoSilenceDurationDialogFragment.newInstance(pref.getKey(), currentValue, - currentValue == ALARM_TIMEOUT_END_OF_RINGTONE); + currentValue == TIMEOUT_END_OF_RINGTONE, + currentValue == TIMEOUT_NEVER); AutoSilenceDurationDialogFragment.show(getParentFragmentManager(), dialogFragment); } else if (pref instanceof AlarmSnoozeDurationPreference alarmSnoozeDurationPreference) { int currentValue = alarmSnoozeDurationPreference.getSnoozeDuration(); AlarmSnoozeDurationDialogFragment dialogFragment = - AlarmSnoozeDurationDialogFragment.newInstance(pref.getKey(), currentValue); + AlarmSnoozeDurationDialogFragment.newInstance(pref.getKey(), currentValue, + currentValue == ALARM_SNOOZE_DURATION_DISABLED); AlarmSnoozeDurationDialogFragment.show(getParentFragmentManager(), dialogFragment); } else if (pref instanceof VolumeCrescendoDurationPreference volumeCrescendoDurationPreference) { int currentValue = volumeCrescendoDurationPreference.getVolumeCrescendoDuration(); VolumeCrescendoDurationDialogFragment dialogFragment = - VolumeCrescendoDurationDialogFragment.newInstance(pref.getKey(), currentValue); + VolumeCrescendoDurationDialogFragment.newInstance(pref.getKey(), currentValue, + currentValue == DEFAULT_VOLUME_CRESCENDO_DURATION); VolumeCrescendoDurationDialogFragment.show(getParentFragmentManager(), dialogFragment); + } else if (pref instanceof VibrationPatternPreference vibrationPatternPreference) { + String currentValue = vibrationPatternPreference.getPattern(); + VibrationPatternDialogFragment dialogFragment = + VibrationPatternDialogFragment.newInstance(pref.getKey(), currentValue); + VibrationPatternDialogFragment.show(getParentFragmentManager(), dialogFragment); + } else if (pref instanceof VibrationStartDelayPreference vibrationStartDelayPreference) { + int currentValue = vibrationStartDelayPreference.getVibrationStartDelay(); + VibrationStartDelayDialogFragment dialogFragment = + VibrationStartDelayDialogFragment.newInstance(pref.getKey(), currentValue, + currentValue == DEFAULT_VIBRATION_START_DELAY); + VibrationStartDelayDialogFragment.show(getParentFragmentManager(), dialogFragment); } else { super.onDisplayPreferenceDialog(pref); } @@ -297,9 +529,25 @@ public void onDisplayPreferenceDialog(@NonNull Preference pref) { private void setupPreferences() { mAudioManager = (AudioManager) requireContext().getSystemService(Context.AUDIO_SERVICE); + mAlarmDisplayCustomizationPref.setOnPreferenceClickListener(this); + + mAlarmFontPref.setTitle(getString(SettingsDAO.getAlarmFont(mPrefs) == null + ? R.string.custom_font_title + : R.string.custom_font_title_variant)); + mAlarmFontPref.setOnPreferenceClickListener(this); + + mMaterialTimePickerStylePref.setOnPreferenceChangeListener(this); + mMaterialTimePickerStylePref.setSummary(mMaterialTimePickerStylePref.getEntry()); + + mMaterialDatePickerStylePref.setOnPreferenceChangeListener(this); + mMaterialDatePickerStylePref.setSummary(mMaterialDatePickerStylePref.getEntry()); + mAlarmRingtonePref.setOnPreferenceClickListener(this); + mEnablePerAlarmAutoSilencePref.setOnPreferenceChangeListener(this); + // Alarm auto silence duration preference + mAlarmAutoSilencePref.setVisible(!SettingsDAO.isPerAlarmAutoSilenceEnabled(mPrefs)); getParentFragmentManager().setFragmentResultListener(AutoSilenceDurationDialogFragment.REQUEST_KEY, this, (requestKey, bundle) -> { String key = bundle.getString(AutoSilenceDurationDialogFragment.RESULT_PREF_KEY); @@ -310,11 +558,21 @@ private void setupPreferences() { if (pref != null) { pref.setAutoSilenceDuration(newValue); pref.setSummary(pref.getSummary()); + mEnablePerAlarmMissedRepeatLimitPref.setVisible(newValue != TIMEOUT_NEVER); + mRepeatMissedAlarmPref.setVisible(newValue != TIMEOUT_NEVER + && !SettingsDAO.isPerAlarmMissedRepeatLimitEnabled(mPrefs)); + for (Alarm alarm : mAlarmList) { + alarm.autoSilenceDuration = newValue; + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } } } }); + mEnablePerAlarmSnoozeDurationPref.setOnPreferenceChangeListener(this); + // Alarm snooze duration preference + mAlarmSnoozeDurationPref.setVisible(!SettingsDAO.isPerAlarmSnoozeDurationEnabled(mPrefs)); getParentFragmentManager().setFragmentResultListener(AlarmSnoozeDurationDialogFragment.REQUEST_KEY, this, (requestKey, bundle) -> { String key = bundle.getString(AlarmSnoozeDurationDialogFragment.RESULT_PREF_KEY); @@ -325,18 +583,33 @@ private void setupPreferences() { if (pref != null) { pref.setSnoozeDuration(newValue); pref.setSummary(pref.getSummary()); + for (Alarm alarm : mAlarmList) { + alarm.snoozeDuration = newValue; + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } } } }); + mEnablePerAlarmMissedRepeatLimitPref.setVisible(SettingsDAO.getAlarmTimeout(mPrefs) != TIMEOUT_NEVER); + mEnablePerAlarmMissedRepeatLimitPref.setOnPreferenceChangeListener(this); + + mRepeatMissedAlarmPref.setVisible(SettingsDAO.getAlarmTimeout(mPrefs) != TIMEOUT_NEVER + && !SettingsDAO.isPerAlarmMissedRepeatLimitEnabled(mPrefs)); + mRepeatMissedAlarmPref.setOnPreferenceChangeListener(this); + mRepeatMissedAlarmPref.setSummary(mRepeatMissedAlarmPref.getEntry()); + mEnablePerAlarmVolumePref.setOnPreferenceChangeListener(this); mAlarmVolumePref.setVisible(!SettingsDAO.isPerAlarmVolumeEnabled(mPrefs)); if (mAlarmVolumePref.isVisible()) { - mAlarmVolumePref.setEnabled(!RingtoneUtils.hasBluetoothDeviceConnected(requireContext(), mPrefs)); + mAlarmVolumePref.setEnabled(!RingtoneUtils.hasExternalAudioDeviceConnected(requireContext(), mPrefs)); } + mEnablePerAlarmVolumeCrescendoDurationPref.setOnPreferenceChangeListener(this); + // Alarm volume crescendo duration preference + mAlarmVolumeCrescendoDurationPref.setVisible(!SettingsDAO.isPerAlarmCrescendoDurationEnabled(mPrefs)); getParentFragmentManager().setFragmentResultListener(VolumeCrescendoDurationDialogFragment.REQUEST_KEY, this, (requestKey, bundle) -> { String key = bundle.getString(VolumeCrescendoDurationDialogFragment.RESULT_PREF_KEY); @@ -347,24 +620,70 @@ private void setupPreferences() { if (pref != null) { pref.setVolumeCrescendoDuration(newValue); pref.setSummary(pref.getSummary()); + for (Alarm alarm : mAlarmList) { + alarm.crescendoDuration = newValue; + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } } } }); mAdvancedAudioPlaybackPref.setOnPreferenceChangeListener(this); - mAutoRoutingToBluetoothDevicePref.setVisible(SettingsDAO.isAdvancedAudioPlaybackEnabled(mPrefs)); - mAutoRoutingToBluetoothDevicePref.setOnPreferenceChangeListener(this); + mAutoRoutingToExternalAudioDevicePref.setVisible(SettingsDAO.isAdvancedAudioPlaybackEnabled(mPrefs)); + mAutoRoutingToExternalAudioDevicePref.setOnPreferenceChangeListener(this); mSystemMediaVolume.setVisible(SettingsDAO.isAdvancedAudioPlaybackEnabled(mPrefs) - && SettingsDAO.isAutoRoutingToBluetoothDeviceEnabled(mPrefs)); + && SettingsDAO.isAutoRoutingToExternalAudioDevice(mPrefs)); mSystemMediaVolume.setOnPreferenceChangeListener(this); - mBluetoothVolumePref.setVisible(SettingsDAO.isAdvancedAudioPlaybackEnabled(mPrefs) - && SettingsDAO.isAutoRoutingToBluetoothDeviceEnabled(mPrefs) + mExternalAudioDeviceVolumePref.setVisible(SettingsDAO.isAdvancedAudioPlaybackEnabled(mPrefs) + && SettingsDAO.isAutoRoutingToExternalAudioDevice(mPrefs) && SettingsDAO.shouldUseCustomMediaVolume(mPrefs)); - mBluetoothVolumePref.setEnabled(mBluetoothVolumePref.isVisible() - && RingtoneUtils.hasBluetoothDeviceConnected(requireContext(), mPrefs)); + mExternalAudioDeviceVolumePref.setEnabled(mExternalAudioDeviceVolumePref.isVisible() + && RingtoneUtils.hasExternalAudioDeviceConnected(requireContext(), mPrefs)); + + mAlarmVibrationCategory.setVisible(DeviceUtils.hasVibrator(requireContext())); + + mEnablePerAlarmVibrationPatternPref.setOnPreferenceChangeListener(this); + + mVibrationPatternPref.setVisible(!SettingsDAO.isPerAlarmVibrationPatternEnabled(mPrefs)); + getParentFragmentManager().setFragmentResultListener(VibrationPatternDialogFragment.REQUEST_KEY, + this, (requestKey, bundle) -> { + String key = bundle.getString(VibrationPatternDialogFragment.RESULT_PREF_KEY); + String newValue = bundle.getString(VibrationPatternDialogFragment.RESULT_PATTERN_KEY); + + if (key != null) { + VibrationPatternPreference pref = findPreference(key); + if (pref != null) { + pref.setPattern(newValue); + pref.setSummary(pref.getSummary()); + for (Alarm alarm : mAlarmList) { + alarm.vibrationPattern = newValue; + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } + } + } + }); + + // Vibration start delay preference + getParentFragmentManager().setFragmentResultListener(VibrationStartDelayDialogFragment.REQUEST_KEY, + this, (requestKey, bundle) -> { + String key = bundle.getString(VibrationStartDelayDialogFragment.RESULT_PREF_KEY); + int newValue = bundle.getInt(VibrationStartDelayDialogFragment.VIBRATION_DELAY_VALUE); + + if (key != null) { + VibrationStartDelayPreference pref = findPreference(key); + if (pref != null) { + pref.setVibrationStartDelay(newValue); + pref.setSummary(pref.getSummary()); + } + } + }); + + mEnableAlarmVibrationsByDefaultPref.setOnPreferenceChangeListener(this); + + mEnableSnoozedOrDismissedAlarmVibrationsPref.setOnPreferenceChangeListener(this); mVolumeButtonsPref.setOnPreferenceChangeListener(this); mVolumeButtonsPref.setSummary(mVolumeButtonsPref.getEntry()); @@ -372,9 +691,6 @@ private void setupPreferences() { mPowerButtonPref.setOnPreferenceChangeListener(this); mPowerButtonPref.setSummary(mPowerButtonPref.getEntry()); - mEnableAlarmVibrationsByDefaultPref.setVisible(Utils.hasVibrator(requireContext())); - mEnableSnoozedOrDismissedAlarmVibrationsPref.setVisible(Utils.hasVibrator(requireContext())); - SensorManager sensorManager = (SensorManager) requireActivity().getSystemService(Context.SENSOR_SERVICE); if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) == null) { mFlipActionPref.setValue("0"); @@ -392,6 +708,13 @@ private void setupPreferences() { mShakeIntensityPref.setVisible(shakeActionIndex != 2); } + mSortAlarmPref.setOnPreferenceChangeListener(this); + mSortAlarmPref.setSummary(mSortAlarmPref.getEntry()); + + mDisplayEnabledAlarmsFirstPref.setOnPreferenceChangeListener(this); + + mEnableAlarmFabLongPressPref.setOnPreferenceChangeListener(this); + // Set the default first day of the week programmatically final Weekdays.Order weekdayOrder = SettingsDAO.getWeekdayOrder(mPrefs); final Integer firstDay = weekdayOrder.getCalendarDays().get(0); @@ -401,30 +724,56 @@ private void setupPreferences() { mWeekStartPref.setSummary(mWeekStartPref.getEntries()[index]); mWeekStartPref.setOnPreferenceChangeListener(this); + mDisplayDismissButtonPref.setOnPreferenceChangeListener(this); + mAlarmNotificationReminderTimePref.setOnPreferenceChangeListener(this); mAlarmNotificationReminderTimePref.setSummary(mAlarmNotificationReminderTimePref.getEntry()); - mEnableAlarmVibrationsByDefaultPref.setOnPreferenceChangeListener(this); - - mEnableSnoozedOrDismissedAlarmVibrationsPref.setOnPreferenceChangeListener(this); - mTurnOnBackFlashForTriggeredAlarmPref.setVisible(AlarmUtils.hasBackFlash(requireContext())); mTurnOnBackFlashForTriggeredAlarmPref.setOnPreferenceChangeListener(this); mDeleteOccasionalAlarmByDefaultPref.setOnPreferenceChangeListener(this); - mMaterialTimePickerStylePref.setOnPreferenceChangeListener(this); - mMaterialTimePickerStylePref.setSummary(mMaterialTimePickerStylePref.getEntry()); + Preference updateHolidayDataPref = findPreference(KEY_UPDATE_HOLIDAY_DATA); + if (updateHolidayDataPref != null) { + updateHolidayDataPref.setOnPreferenceClickListener(preference -> { + HolidayRepository.getInstance(requireContext()).updateWorkdayData(); + return true; + }); + } - mMaterialDatePickerStylePref.setOnPreferenceChangeListener(this); - mMaterialDatePickerStylePref.setSummary(mMaterialDatePickerStylePref.getEntry()); + mHolidayDataUrlPref = findPreference(KEY_HOLIDAY_DATA_URL); + if (mHolidayDataUrlPref != null) { + mHolidayDataUrlPref.setSummary(SettingsDAO.getHolidayDataUrl(mPrefs)); + mHolidayDataUrlPref.setOnPreferenceChangeListener(this); + } + } - mAlarmDisplayCustomizationPref.setOnPreferenceClickListener(this); + private void showDisablePerAlarmSettingDialog(@StringRes int messageResId, String prefKey, + SwitchPreferenceCompat switchPref, Preference dependentPref, + AlarmUpdater alarmUpdater) { + + String confirmAction = requireContext().getString(R.string.confirm_action_prompt); + + AlertDialog dialog = CustomDialog.createSimpleDialog( + requireContext(), + R.drawable.ic_error, + R.string.warning, + getString(messageResId, confirmAction), + android.R.string.ok, + (d, w) -> { + for (Alarm alarm : mAlarmList) { + alarmUpdater.update(alarm); + mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true); + } - mUpdateHolidayDataPref.setOnPreferenceClickListener(this); + mPrefs.edit().putBoolean(prefKey, false).apply(); + switchPref.setChecked(false); + dependentPref.setVisible(true); + } + ); - mHolidayDataUrlPref.setOnPreferenceChangeListener(this); - mHolidayDataUrlPref.setSummary(DataModel.getDataModel().getHolidayDataUrl()); + dialog.show(); } private void initAudioDeviceCallback() { @@ -440,26 +789,25 @@ public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { mAlarmVolumePref.stopRingtonePreview(); for (AudioDeviceInfo device : addedDevices) { - if (RingtoneUtils.isBluetoothDevice(device)) { + if (RingtoneUtils.isExternalAudioDevice(device)) { mAlarmVolumePref.setEnabled(false); - mAlarmVolumePref.setTitle(R.string.disconnect_bluetooth_device_title); - mBluetoothVolumePref.setEnabled(true); - mBluetoothVolumePref.setTitle(R.string.bluetooth_volume_title); + mAlarmVolumePref.setTitle(R.string.disconnect_external_audio_device_title); + mExternalAudioDeviceVolumePref.setEnabled(true); + mExternalAudioDeviceVolumePref.setTitle(R.string.external_audio_device_volume_title); } } } @Override public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { - - mBluetoothVolumePref.stopRingtonePreviewForBluetoothDevices(); + mExternalAudioDeviceVolumePref.stopRingtonePreviewForExternalAudioDevices(); for (AudioDeviceInfo device : removedDevices) { - if (RingtoneUtils.isBluetoothDevice(device)) { + if (RingtoneUtils.isExternalAudioDevice(device)) { mAlarmVolumePref.setEnabled(true); mAlarmVolumePref.setTitle(R.string.alarm_volume_title); - mBluetoothVolumePref.setEnabled(false); - mBluetoothVolumePref.setTitle(R.string.connect_bluetooth_device_title); + mExternalAudioDeviceVolumePref.setEnabled(false); + mExternalAudioDeviceVolumePref.setTitle(R.string.connect_external_audio_device_title); } } } @@ -469,11 +817,19 @@ public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { } private void stopRingtonePreview() { - if (RingtoneUtils.hasBluetoothDeviceConnected(requireContext(), mPrefs)) { - mBluetoothVolumePref.stopRingtonePreviewForBluetoothDevices(); + if (RingtoneUtils.hasExternalAudioDeviceConnected(requireContext(), mPrefs)) { + mExternalAudioDeviceVolumePref.stopRingtonePreviewForExternalAudioDevices(); } else { mAlarmVolumePref.stopRingtonePreview(); } } + /** + * Interface for updating alarm properties when pressing the OK button in the dialog box + * that appears when the "per alarm" settings are disabled. + */ + private interface AlarmUpdater { + void update(Alarm alarm); + } + } diff --git a/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java b/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java index 4f654c266..1fc9280bb 100644 --- a/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java +++ b/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java @@ -10,6 +10,7 @@ import java.util.Calendar; public class PreferencesDefaultValues { + public static final String DEFAULT_HOLIDAY_DATA_URL = "https://raw.githubusercontent.com/lanceliao/china-holiday-calender/master/holidayAPI.json"; // ************** // ** SETTINGS ** @@ -41,6 +42,7 @@ public class PreferencesDefaultValues { public static final boolean DEFAULT_CARD_BACKGROUND = true; public static final boolean DEFAULT_CARD_BORDER = false; public static final String DEFAULT_SYSTEM_LANGUAGE_CODE = "system_language_code"; + public static final String DEBUG_LANGUAGE_CODE = "en_US"; public static final String DEFAULT_TAB_TO_DISPLAY = "-1"; public static final boolean DEFAULT_VIBRATIONS = false; public static final boolean DEFAULT_TOOLBAR_TITLE = true; @@ -53,17 +55,25 @@ public class PreferencesDefaultValues { // Clock public static final String DEFAULT_CLOCK_STYLE = "digital"; public static final boolean DEFAULT_DISPLAY_CLOCK_SECONDS = false; + public static final String DEFAULT_SORT_CITIES_BY_ASCENDING_TIME_ZONE = "0"; + public static final String SORT_CITIES_BY_DESCENDING_TIME_ZONE = "1"; + public static final String SORT_CITIES_BY_NAME = "2"; + public static final String SORT_CITIES_MANUALLY = "3"; + public static final boolean DEFAULT_ENABLE_CITY_NOTE = false; public static final boolean DEFAULT_AUTO_HOME_CLOCK = true; public static final String DEFAULT_HOME_TIME_ZONE = null; // Alarm - public static final int DEFAULT_AUTO_SILENCE_DURATION = 10; - public static final int ALARM_TIMEOUT_NEVER = -1; - public static final int ALARM_TIMEOUT_END_OF_RINGTONE = -2; + public static final boolean DEFAULT_ENABLE_PER_ALARM_AUTO_SILENCE = true; + public static final int DEFAULT_AUTO_SILENCE_DURATION = 600; + public static final boolean DEFAULT_ENABLE_PER_ALARM_SNOOZE_DURATION = true; public static final int DEFAULT_ALARM_SNOOZE_DURATION = 10; public static final int ALARM_SNOOZE_DURATION_DISABLED = -1; - public static final int DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION = 0; + public static final boolean DEFAULT_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT = true; + public static final String DEFAULT_MISSED_ALARM_REPEAT_LIMIT = "-1"; + public static final boolean DEFAULT_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION = true; public static final boolean DEFAULT_ENABLE_PER_ALARM_VOLUME = false; + public static final int DEFAULT_ALARM_VOLUME = 5; public static final boolean DEFAULT_ADVANCED_AUDIO_PLAYBACK = false; public static final boolean DEFAULT_AUTO_ROUTING_TO_BLUETOOTH_DEVICE = false; public static final boolean DEFAULT_SYSTEM_MEDIA_VOLUME = true; @@ -79,8 +89,24 @@ public class PreferencesDefaultValues { public static final String DEFAULT_FLIP_ACTION = "0"; public static final String DEFAULT_SHAKE_ACTION = "0"; public static final int DEFAULT_SHAKE_INTENSITY = 16; + public static final String DEFAULT_SORT_BY_ALARM_TIME = "0"; + public static final String SORT_ALARM_BY_NEXT_ALARM_TIME = "1"; + public static final String SORT_ALARM_BY_NAME = "2"; + public static final String SORT_ALARM_BY_DESCENDING_CREATION_ORDER = "3"; + public static final String SORT_ALARM_BY_ASCENDING_CREATION_ORDER = "4"; + public static final boolean DEFAULT_DISPLAY_ENABLED_ALARMS_FIRST = false; + public static final boolean DEFAULT_ENABLE_ALARM_FAB_LONG_PRESS = false; public static final String DEFAULT_WEEK_START = String.valueOf(Calendar.getInstance().getFirstDayOfWeek()); + public static final boolean DEFAULT_DISPLAY_DISMISS_BUTTON = false; public static final String DEFAULT_ALARM_NOTIFICATION_REMINDER_TIME = "30"; + public static final boolean DEFAULT_ENABLE_PER_ALARM_VIBRATION_PATTERN = false; + public static final String DEFAULT_VIBRATION_PATTERN = "default"; + public static final String VIBRATION_PATTERN_SOFT = "soft"; + public static final String VIBRATION_PATTERN_STRONG = "strong"; + public static final String VIBRATION_PATTERN_HEARTBEAT = "heartbeat"; + public static final String VIBRATION_PATTERN_ESCALATING = "escalating"; + public static final String VIBRATION_PATTERN_TICK_TOCK = "tick_tock"; + public static final int DEFAULT_VIBRATION_START_DELAY = 0; public static final boolean DEFAULT_ENABLE_ALARM_VIBRATIONS_BY_DEFAULT = false; public static final boolean DEFAULT_ENABLE_SNOOZED_OR_DISMISSED_ALARM_VIBRATIONS = false; public static final boolean DEFAULT_TURN_ON_BACK_FLASH_FOR_TRIGGERED_ALARM = false; @@ -89,11 +115,9 @@ public class PreferencesDefaultValues { public static final String SPINNER_TIME_PICKER_STYLE = "spinner"; public static final String DEFAULT_DATE_PICKER_STYLE = "calendar"; public static final String SPINNER_DATE_PICKER_STYLE = "spinner"; - public static final String DEFAULT_HOLIDAY_DATA_URL = "https://raw.githubusercontent.com/lanceliao/china-holiday-calender/master/holidayAPI.json"; - // Alarm Display Customization - public static final boolean DEFAULT_DISPLAY_ALARM_SECONDS_HAND = true; + public static final boolean DEFAULT_DISPLAY_ALARM_SECOND_HAND = true; public static final int DEFAULT_ALARM_BACKGROUND_COLOR = Color.parseColor("#FF191C1E"); public static final int DEFAULT_ALARM_BACKGROUND_AMOLED_COLOR = Color.BLACK; public static final int DEFAULT_SLIDE_ZONE_COLOR = Color.parseColor("#FF2E3337"); @@ -101,20 +125,32 @@ public class PreferencesDefaultValues { public static final int DEFAULT_ALARM_TITLE_COLOR = Color.WHITE; public static final int DEFAULT_SNOOZE_TITLE_COLOR = Color.WHITE; public static final int DEFAULT_DISMISS_TITLE_COLOR = Color.WHITE; - public static final int DEFAULT_ALARM_DIGITAL_CLOCK_FONT_SIZE = 70; public static final int DEFAULT_ALARM_TITLE_FONT_SIZE_PREF = 30; - public static final boolean DEFAULT_DISPLAY_RINGTONE_TITLE = false; + public static final int DEFAULT_ALARM_SHADOW_COLOR = Color.parseColor("#80FFFFFF"); public static final int DEFAULT_RINGTONE_TITLE_COLOR = Color.WHITE; + public static final boolean DEFAULT_ENABLE_BLUR_EFFECT = false; + public static final int DEFAULT_BLUR_INTENSITY = 20; public static int getDefaultAlarmInversePrimaryColor(Context context) { return MaterialColors.getColor(context, com.google.android.material.R.attr.colorPrimaryInverse, Color.BLACK); - } + public static final int DEFAULT_ALARM_DIGITAL_CLOCK_FONT_SIZE = 70; + public static final String DEFAULT_TIME_TO_ADD_TO_TIMER = "1"; + public static final int DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION = 0; + public static final int DEFAULT_TIMER_VOLUME_CRESCENDO_DURATION = 0; +} // Timer public static final String DEFAULT_TIMER_CREATION_VIEW_STYLE = "keypad"; public static final String TIMER_CREATION_VIEW_SPINNER_STYLE = "spinner"; - public static final String DEFAULT_TIMER_AUTO_SILENCE = "30"; - public static final int DEFAULT_TIMER_VOLUME_CRESCENDO_DURATION = 0; + public static final boolean DEFAULT_DISPLAY_COMPACT_TIMERS = false; + public static final boolean DEFAULT_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER = false; + public static final boolean DEFAULT_DISPLAY_TIMER_STATE_INDICATOR = false; + public static final int DEFAULT_RUNNING_TIMER_INDICATOR_COLOR = Color.parseColor("#FF99CC00"); + public static final int DEFAULT_PAUSED_TIMER_INDICATOR_COLOR = Color.parseColor("#FFFFBB33"); + public static final int DEFAULT_EXPIRED_TIMER_INDICATOR_COLOR = Color.parseColor("#FFFF4444"); + public static final int DEFAULT_TIMER_AUTO_SILENCE_DURATION = 30; // 30 seconds public static final boolean DEFAULT_TIMER_VIBRATE = false; + public static final int DEFAULT_TIMER_RINGTONE_TITLE_COLOR = Color.GRAY; + public static final int DEFAULT_TIMER_SHADOW_COLOR = Color.parseColor("#BF888888"); public static final boolean DEFAULT_TIMER_VOLUME_BUTTONS_ACTION = false; public static final boolean DEFAULT_TIMER_POWER_BUTTON_ACTION = false; public static final boolean DEFAULT_TIMER_FLIP_ACTION = false; @@ -124,8 +160,7 @@ public static int getDefaultAlarmInversePrimaryColor(Context context) { public static final String SORT_TIMER_BY_ASCENDING_DURATION = "1"; public static final String SORT_TIMER_BY_DESCENDING_DURATION = "2"; public static final String SORT_TIMER_BY_NAME = "3"; - public static final String DEFAULT_TIME_TO_ADD_TO_TIMER = "1"; - public static final boolean DEFAULT_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER = false; + public static final int DEFAULT_TIMER_ADD_TIME_BUTTON_VALUE = 60; public static final boolean DEFAULT_DISPLAY_WARNING_BEFORE_DELETING_TIMER = false; // Stopwatch @@ -137,29 +172,40 @@ public static int getDefaultAlarmInversePrimaryColor(Context context) { // Screensaver public static final boolean DEFAULT_DISPLAY_SCREENSAVER_CLOCK_SECONDS = false; + public static final boolean DEFAULT_DISPLAY_SCREENSAVER_BATTERY = false; public static final boolean DEFAULT_SCREENSAVER_CLOCK_DYNAMIC_COLORS = false; public static final int DEFAULT_SCREENSAVER_CUSTOM_COLOR = Color.WHITE; public static final int DEFAULT_SCREENSAVER_BRIGHTNESS = 40; - public static final boolean DEFAULT_SCREENSAVER_DIGITAL_CLOCK_IN_BOLD = false; - public static final boolean DEFAULT_SCREENSAVER_DIGITAL_CLOCK_IN_ITALIC = false; - public static final boolean DEFAULT_SCREENSAVER_DATE_IN_BOLD = true; - public static final boolean DEFAULT_SCREENSAVER_DATE_IN_ITALIC = false; - public static final boolean DEFAULT_SCREENSAVER_NEXT_ALARM_IN_BOLD = true; - public static final boolean DEFAULT_SCREENSAVER_NEXT_ALARM_IN_ITALIC = false; + public static final boolean DEFAULT_SCREENSAVER_FORMATTING = false; + + // Common settings values + public static final String DEFAULT_CLOCK_DIAL = "dial_with_numbers"; + public static final String DEFAULT_CLOCK_DIAL_MATERIAL = "dial_sun"; + public static final String DEFAULT_CLOCK_SECOND_HAND = "default"; + public static final int TIMEOUT_NEVER = -1; + public static final int TIMEOUT_END_OF_RINGTONE = -2; + public static final int DEFAULT_VOLUME_CRESCENDO_DURATION = 0; + public static final boolean DEFAULT_DISPLAY_RINGTONE_TITLE = false; + public static final boolean DEFAULT_DISPLAY_TEXT_SHADOW = false; + public static final int DEFAULT_SHADOW_OFFSET = 10; + public static final int DEFAULT_DIGITAL_CLOCK_FONT_SIZE = 70; + public static final int DEFAULT_ANALOG_CLOCK_SIZE = 70; // ************** // ** WIDGETS ** // ************** // Analog Widget - public static final boolean DEFAULT_ANALOG_WIDGET_WITH_SECOND_HAND = false; + public static final String DEFAULT_ANALOG_WIDGET_CLOCK_DIAL = "default"; + public static final String ANALOG_WIDGET_CLOCK_DIAL_WITH_NUMBERS = "dial_with_numbers"; + public static final String ANALOG_WIDGET_CLOCK_DIAL_WITHOUT_NUMBERS = "dial_without_numbers"; // DigitalWidgetSettingsFragment public static final boolean DEFAULT_DIGITAL_WIDGET_DISPLAY_SECONDS = false; public static final boolean DEFAULT_DIGITAL_WIDGET_HIDE_AM_PM = false; + public static final boolean DEFAULT_DIGITAL_WIDGET_DISPLAY_BACKGROUND = false; public static final boolean DEFAULT_DIGITAL_WIDGET_DISPLAY_DATE = true; public static final boolean DEFAULT_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = true; - public static final boolean DEFAULT_DIGITAL_WIDGET_DISPLAY_BACKGROUND = false; public static final boolean DEFAULT_DIGITAL_WIDGET_WORLD_CITIES_DISPLAYED = true; // NextAlarmWidgetSettingsFragment @@ -171,11 +217,17 @@ public static int getDefaultAlarmInversePrimaryColor(Context context) { public static final boolean DEFAULT_VERTICAL_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = true; // Material You Analog Widget - public static final boolean DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_WITH_SECOND_HAND = false; + public static final String DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_CLOCK_DIAL = "dial_sun"; + public static final String MATERIAL_YOU_ANALOG_WIDGET_CLOCK_DIAL_FLOWER = "dial_flower"; + public static final int DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_DIAL_COLOR = Color.parseColor("#EEF0FF"); + public static final int DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_HOUR_HAND_COLOR = Color.parseColor("#575E71"); + public static final int DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_MINUTE_HAND_COLOR = Color.parseColor("#475D92"); + public static final int DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_SECOND_HAND_COLOR = Color.parseColor("#725572"); // Material You Digital Widget public static final boolean DEFAULT_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_SECONDS = false; public static final boolean DEFAULT_MATERIAL_YOU_DIGITAL_WIDGET_HIDE_AM_PM = false; + public static final boolean DEFAULT_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_BACKGROUND = true; public static final boolean DEFAULT_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_DATE = true; public static final boolean DEFAULT_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = true; public static final boolean DEFAULT_MATERIAL_YOU_DIGITAL_WIDGET_WORLD_CITIES_DISPLAYED = true; @@ -185,9 +237,19 @@ public static int getDefaultAlarmInversePrimaryColor(Context context) { public static final boolean DEFAULT_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = true; // Common widget values + public static final boolean DEFAULT_WIDGET_TEXT_UPPERCASE_DISPLAYED = true; + public static final boolean DEFAULT_WIDGET_TEXT_SHADOW_DISPLAYED = true; + public static final boolean DEFAULT_ANALOG_WIDGET_WITH_SECOND_HAND = false; public static final boolean DEFAULT_WIDGETS_DEFAULT_COLOR = true; public static final int DEFAULT_WIDGETS_BACKGROUND_COLOR = Color.parseColor("#70000000"); + public static final boolean DEFAULT_WIDGETS_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = false; + public static final int DEFAULT_WIDGET_BACKGROUND_CORNER_RADIUS = 24; + public static final int DEFAULT_MATERIAL_YOU_WIDGET_BACKGROUND_CORNER_RADIUS = 80; public static final int DEFAULT_WIDGETS_CUSTOM_COLOR = Color.WHITE; public static final int DEFAULT_WIDGETS_FONT_SIZE = 70; public static final boolean DEFAULT_WIDGETS_APPLY_HORIZONTAL_PADDING = true; + public static final int DEFAULT_ALARM_DIGITAL_CLOCK_FONT_SIZE = 70; + public static final String DEFAULT_TIME_TO_ADD_TO_TIMER = "1"; + public static final int DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION = 0; + public static final int DEFAULT_TIMER_VOLUME_CRESCENDO_DURATION = 0; } diff --git a/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java b/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java index 8a808adfe..0702fcfc5 100644 --- a/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java +++ b/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java @@ -10,6 +10,7 @@ public class PreferencesKeys { // Settings public static final String KEY_PERMISSION_MESSAGE = "key_permission_message"; + public static final String KEY_ESSENTIAL_PERMISSIONS_GRANTED = "key_essential_permissions_granted"; public static final String KEY_INTERFACE_CUSTOMIZATION = "key_interface_customization"; public static final String KEY_CLOCK_SETTINGS = "key_clock_settings"; public static final String KEY_ALARM_SETTINGS = "key_alarm_settings"; @@ -19,6 +20,7 @@ public class PreferencesKeys { public static final String KEY_WIDGETS_SETTINGS = "key_widgets_settings"; public static final String KEY_PERMISSIONS_MANAGEMENT = "key_permissions_management"; public static final String KEY_BACKUP_RESTORE_PREFERENCES = "key_backup_restore_preferences"; + public static final String KEY_WIDGET_COLOR_CATEGORY = "key_widget_color_category"; // About public static final String KEY_ABOUT_TITLE = "key_about_title"; @@ -28,12 +30,6 @@ public class PreferencesKeys { public static final String KEY_ABOUT_VIEW_ON_GITHUB = "key_about_view_on_github"; public static final String KEY_ABOUT_TRANSLATE = "key_about_translate"; public static final String KEY_ABOUT_READ_LICENCE = "key_about_read_licence"; - public static final String KEY_ABOUT_BLACKYHAWKY = "key_about_blackyhawky"; - public static final String KEY_ABOUT_QW123WH = "key_about_qw123wh"; - public static final String KEY_ABOUT_ODMFL = "key_about_odmfl"; - public static final String KEY_ABOUT_NILSU11 = "key_about_nilsu11"; - public static final String KEY_ABOUT_LINEAGEOS = "key_about_lineageos"; - public static final String KEY_ABOUT_CRDROID = "key_about_crdroid"; public static final String KEY_DISPLAY_DEBUG_SETTINGS = "key_display_debug_settings"; public static final String KEY_DEBUG_CATEGORY = "key_debug_category"; public static final String KEY_ENABLE_LOCAL_LOGGING = "key_enable_local_logging"; @@ -41,6 +37,7 @@ public class PreferencesKeys { // Interface public static final String KEY_THEME = "key_theme"; public static final String KEY_DARK_MODE = "key_dark_mode"; + public static final String KEY_GENERAL_FONT = "key_general_font"; public static final String KEY_ACCENT_COLOR = "key_accent_color"; public static final String KEY_AUTO_NIGHT_ACCENT_COLOR = "key_auto_night_accent_color"; public static final String KEY_NIGHT_ACCENT_COLOR = "key_night_accent_color"; @@ -57,30 +54,53 @@ public class PreferencesKeys { // Clock public static final String KEY_CLOCK_STYLE = "key_clock_style"; + public static final String KEY_CLOCK_DIAL = "key_clock_dial"; + public static final String KEY_CLOCK_DIAL_MATERIAL = "key_clock_dial_material"; + public static final String KEY_ANALOG_CLOCK_SIZE = "key_analog_clock_size"; + public static final String KEY_FONT_CATEGORY = "key_font_category"; + public static final String KEY_DIGITAL_CLOCK_FONT_SIZE = "key_digital_clock_font_size"; public static final String KEY_DISPLAY_CLOCK_SECONDS = "key_display_clock_seconds"; + public static final String KEY_CLOCK_SECOND_HAND = "key_clock_second_hand"; + public static final String KEY_DIGITAL_CLOCK_FONT = "key_digital_clock_font"; + public static final String KEY_SORT_CITIES = "key_sort_cities"; + public static final String KEY_ENABLE_CITY_NOTE = "key_enable_city_note"; + public static final String KEY_CITY_NOTE = "key_city_note_"; public static final String KEY_AUTO_HOME_CLOCK = "key_automatic_home_clock"; public static final String KEY_HOME_TIME_ZONE = "key_home_time_zone"; public static final String KEY_DATE_TIME = "key_date_time"; // Alarm public static final String KEY_DEFAULT_ALARM_RINGTONE = "key_default_alarm_ringtone"; + public static final String KEY_ENABLE_PER_ALARM_AUTO_SILENCE = "key_enable_per_alarm_auto_silence"; public static final String KEY_AUTO_SILENCE_DURATION = "key_auto_silence_duration"; + public static final String KEY_ENABLE_PER_ALARM_SNOOZE_DURATION = "key_enable_per_alarm_snooze_duration"; public static final String KEY_ALARM_SNOOZE_DURATION = "key_alarm_snooze_duration"; + public static final String KEY_ENABLE_PER_ALARM_MISSED_REPEAT_LIMIT = "key_enable_per_alarm_missed_repeat_limit"; + public static final String KEY_MISSED_ALARM_REPEAT_LIMIT = "key_missed_alarm_repeat_limit"; public static final String KEY_ALARM_VOLUME_SETTING = "key_volume_setting"; + public static final String KEY_ENABLE_PER_ALARM_VOLUME_CRESCENDO_DURATION = "key_enable_per_alarm_volume_crescendo_duration"; public static final String KEY_ALARM_VOLUME_CRESCENDO_DURATION = "key_alarm_volume_crescendo_duration"; public static final String KEY_ENABLE_PER_ALARM_VOLUME = "key_enable_per_alarm_volume"; public static final String KEY_ADVANCED_AUDIO_PLAYBACK = "key_advanced_audio_playback"; public static final String KEY_AUTO_ROUTING_TO_BLUETOOTH_DEVICE = "key_auto_routing_to_bluetooth_device"; public static final String KEY_SYSTEM_MEDIA_VOLUME = "key_system_media_volume"; public static final String KEY_BLUETOOTH_VOLUME = "key_bluetooth_volume"; + public static final String KEY_ALARM_VIBRATION_CATEGORY = "key_alarm_vibration_category"; public static final String KEY_SWIPE_ACTION = "key_swipe_action"; public static final String KEY_VOLUME_BUTTONS = "key_volume_button_setting"; public static final String KEY_POWER_BUTTON = "key_power_button"; public static final String KEY_FLIP_ACTION = "key_flip_action"; public static final String KEY_SHAKE_ACTION = "key_shake_action"; public static final String KEY_SHAKE_INTENSITY = "key_shake_intensity"; + public static final String KEY_SORT_ALARM = "key_sort_alarm"; + public static final String KEY_DISPLAY_ENABLED_ALARMS_FIRST = "key_display_enabled_alarms_first"; + public static final String KEY_ENABLE_ALARM_FAB_LONG_PRESS = "key_enable_alarm_fab_long_press"; public static final String KEY_WEEK_START = "key_week_start"; + public static final String KEY_DISPLAY_DISMISS_BUTTON = "key_display_dismiss_button"; public static final String KEY_ALARM_NOTIFICATION_REMINDER_TIME = "key_alarm_notification_reminder_time"; + public static final String KEY_ENABLE_PER_ALARM_VIBRATION_PATTERN = "key_enable_per_alarm_vibration_pattern"; + public static final String KEY_VIBRATION_PATTERN = "key_vibration_pattern"; + public static final String KEY_VIBRATION_START_DELAY = "key_vibration_start_delay"; public static final String KEY_ENABLE_ALARM_VIBRATIONS_BY_DEFAULT = "key_enable_alarm_vibrations_by_default"; public static final String KEY_ENABLE_SNOOZED_OR_DISMISSED_ALARM_VIBRATIONS = "key_enable_snoozed_or_dismissed_alarm_vibrations"; public static final String KEY_TURN_ON_BACK_FLASH_FOR_TRIGGERED_ALARM = "key_turn_on_back_flash_for_triggered_alarm"; @@ -88,17 +108,20 @@ public class PreferencesKeys { public static final String KEY_MATERIAL_TIME_PICKER_STYLE = "key_material_time_picker_style"; public static final String KEY_MATERIAL_DATE_PICKER_STYLE = "key_material_date_picker_style"; public static final String KEY_ALARM_DISPLAY_CUSTOMIZATION = "key_alarm_display_customization"; - public static final String KEY_UPDATE_HOLIDAY_DATA = "key_update_holiday_data"; - public static final String KEY_HOLIDAY_DATA_URL = "key_holiday_data_url"; // Alarm Display Customization public static final String KEY_ALARM_CLOCK_STYLE = "key_alarm_clock_style"; - public static final String KEY_DISPLAY_ALARM_SECONDS_HAND = "key_display_alarm_seconds_hand"; + public static final String KEY_ALARM_CLOCK_DIAL = "key_alarm_clock_dial"; + public static final String KEY_ALARM_CLOCK_DIAL_MATERIAL = "key_alarm_clock_dial_material"; + public static final String KEY_ALARM_ANALOG_CLOCK_SIZE = "key_alarm_analog_clock_size"; + public static final String KEY_DISPLAY_ALARM_SECOND_HAND = "key_display_alarm_second_hand"; + public static final String KEY_ALARM_CLOCK_SECOND_HAND = "key_alarm_clock_second_hand"; + public static final String KEY_ALARM_FONT = "key_alarm_font"; public static final String KEY_ALARM_BACKGROUND_COLOR = "key_alarm_background_color"; public static final String KEY_ALARM_BACKGROUND_AMOLED_COLOR = "key_alarm_background_amoled_color"; public static final String KEY_SLIDE_ZONE_COLOR = "key_slide_zone_color"; public static final String KEY_ALARM_CLOCK_COLOR = "key_alarm_clock_color"; - public static final String KEY_ALARM_SECONDS_HAND_COLOR = "key_alarm_seconds_hand_color"; + public static final String KEY_ALARM_SECOND_HAND_COLOR = "key_alarm_second_hand_color"; public static final String KEY_ALARM_TITLE_COLOR = "key_alarm_title_color"; public static final String KEY_SNOOZE_TITLE_COLOR = "key_snooze_title_color"; public static final String KEY_SNOOZE_BUTTON_COLOR = "key_snooze_button_color"; @@ -107,14 +130,21 @@ public class PreferencesKeys { public static final String KEY_ALARM_BUTTON_COLOR = "key_alarm_button_color"; public static final String KEY_ALARM_DIGITAL_CLOCK_FONT_SIZE = "key_alarm_digital_clock_font_size"; public static final String KEY_ALARM_TITLE_FONT_SIZE_PREF = "key_alarm_title_font_size_pref"; + public static final String KEY_ALARM_DISPLAY_TEXT_SHADOW = "key_alarm_display_text_shadow"; + public static final String KEY_ALARM_SHADOW_COLOR = "key_alarm_shadow_color"; + public static final String KEY_ALARM_SHADOW_OFFSET = "key_alarm_shadow_offset"; public static final String KEY_DISPLAY_RINGTONE_TITLE = "key_display_ringtone_title"; public static final String KEY_RINGTONE_TITLE_COLOR = "key_ringtone_title_color"; - public static final String KEY_PREVIEW_ALARM = "key_preview_alarm"; + public static final String KEY_ALARM_BACKGROUND_IMAGE = "key_alarm_background_image"; + public static final String KEY_ENABLE_ALARM_BLUR_EFFECT = "key_enable_alarm_blur_effect"; + public static final String KEY_ALARM_BLUR_INTENSITY = "key_alarm_blur_intensity"; + public static final String KEY_ALARM_PREVIEW = "key_alarm_preview"; // Timer + public static final String KEY_TIMER_DISPLAY_CUSTOMIZATION = "key_timer_display_customization"; public static final String KEY_TIMER_CREATION_VIEW_STYLE = "key_timer_creation_view_style"; public static final String KEY_TIMER_RINGTONE = "key_timer_ringtone"; - public static final String KEY_TIMER_AUTO_SILENCE = "key_timer_auto_silence"; + public static final String KEY_TIMER_AUTO_SILENCE_DURATION = "key_timer_auto_silence_duration"; public static final String KEY_TIMER_VOLUME_CRESCENDO_DURATION = "key_timer_volume_crescendo_duration"; public static final String KEY_TIMER_VIBRATE = "key_timer_vibrate"; public static final String KEY_TIMER_VOLUME_BUTTONS_ACTION = "key_timer_volume_buttons_action"; @@ -123,11 +153,31 @@ public class PreferencesKeys { public static final String KEY_TIMER_SHAKE_ACTION = "key_timer_shake_action"; public static final String KEY_TIMER_SHAKE_INTENSITY = "key_timer_shake_intensity"; public static final String KEY_SORT_TIMER = "key_sort_timer"; - public static final String KEY_DEFAULT_TIME_TO_ADD_TO_TIMER = "key_default_time_to_add_to_timer"; - public static final String KEY_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER = "key_transparent_background_for_expired_timer"; + public static final String KEY_TIMER_ADD_TIME_BUTTON_VALUE = "key_timer_add_time_button_value"; public static final String KEY_DISPLAY_WARNING_BEFORE_DELETING_TIMER = "key_display_warning_before_deleting_timer"; + // Timer Display Customization + public static final String KEY_TIMER_DURATION_FONT = "key_timer_duration_font"; + public static final String KEY_DISPLAY_COMPACT_TIMERS = "key_display_compact_timers"; + public static final String KEY_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER = "key_transparent_background_for_expired_timer"; + public static final String KEY_DISPLAY_TIMER_STATE_INDICATOR = "key_display_timer_state_indicator"; + public static final String KEY_DISPLAY_TIMER_RINGTONE_TITLE = "key_display_timer_ringtone_title"; + public static final String KEY_TIMER_COLOR_CATEGORY = "key_timer_color_category"; + public static final String KEY_RUNNING_TIMER_INDICATOR_COLOR = "key_running_timer_indicator_color"; + public static final String KEY_PAUSED_TIMER_INDICATOR_COLOR = "key_paused_timer_indicator_color"; + public static final String KEY_EXPIRED_TIMER_INDICATOR_COLOR = "key_expired_timer_indicator_color"; + public static final String KEY_TIMER_RINGTONE_TITLE_COLOR = "key_timer_ringtone_title_color"; + public static final String KEY_TIMER_FONT_CATEGORY = "key_timer_font_category"; + public static final String KEY_TIMER_DISPLAY_TEXT_SHADOW = "key_timer_display_text_shadow"; + public static final String KEY_TIMER_SHADOW_COLOR = "key_timer_shadow_color"; + public static final String KEY_TIMER_SHADOW_OFFSET = "key_timer_shadow_offset"; + public static final String KEY_TIMER_BACKGROUND_IMAGE = "key_timer_background_image"; + public static final String KEY_ENABLE_TIMER_BLUR_EFFECT = "key_enable_timer_blur_effect"; + public static final String KEY_TIMER_BLUR_INTENSITY = "key_timer_blur_intensity"; + public static final String KEY_TIMER_PREVIEW = "key_timer_preview"; + // Stopwatch + public static final String KEY_SW_FONT = "key_sw_font"; public static final String KEY_SW_VOLUME_UP_ACTION = "key_sw_volume_up_action"; public static final String KEY_SW_VOLUME_UP_ACTION_AFTER_LONG_PRESS = "key_sw_volume_up_action_after_long_press"; public static final String KEY_SW_VOLUME_DOWN_ACTION = "key_sw_volume_down_action"; @@ -135,18 +185,31 @@ public class PreferencesKeys { // Screensaver public static final String KEY_SCREENSAVER_CLOCK_STYLE = "key_screensaver_clock_style"; + public static final String KEY_SCREENSAVER_CLOCK_DIAL = "key_screensaver_clock_dial"; + public static final String KEY_SCREENSAVER_CLOCK_DIAL_MATERIAL = "key_screensaver_clock_dial_material"; + public static final String KEY_SCREENSAVER_DIGITAL_CLOCK_FONT = "key_screensaver_digital_clock_font"; + public static final String KEY_SCREENSAVER_ANALOG_CLOCK_SIZE = "key_screensaver_analog_clock_size"; public static final String KEY_DISPLAY_SCREENSAVER_CLOCK_SECONDS = "key_display_screensaver_clock_seconds"; + public static final String KEY_SCREENSAVER_CLOCK_SECOND_HAND = "key_screensaver_clock_second_hand"; + public static final String KEY_DISPLAY_SCREENSAVER_BATTERY = "key_display_screensaver_battery"; public static final String KEY_SCREENSAVER_CLOCK_DYNAMIC_COLORS = "key_screensaver_clock_dynamic_colors"; public static final String KEY_SCREENSAVER_CLOCK_COLOR_PICKER = "key_screensaver_clock_color_picker"; + public static final String KEY_SCREENSAVER_BATTERY_COLOR_PICKER = "key_screensaver_battery_color_picker"; public static final String KEY_SCREENSAVER_DATE_COLOR_PICKER = "key_screensaver_date_color_picker"; public static final String KEY_SCREENSAVER_NEXT_ALARM_COLOR_PICKER = "key_screensaver_next_alarm_color_picker"; public static final String KEY_SCREENSAVER_BRIGHTNESS = "key_screensaver_brightness"; + public static final String KEY_SCREENSAVER_DIGITAL_CLOCK_FONT_SIZE = "key_screensaver_digital_clock_font_size"; public static final String KEY_SCREENSAVER_DIGITAL_CLOCK_IN_BOLD = "key_screensaver_digital_clock_in_bold"; public static final String KEY_SCREENSAVER_DIGITAL_CLOCK_IN_ITALIC = "key_screensaver_digital_clock_in_italic"; + public static final String KEY_SCREENSAVER_BATTERY_IN_BOLD = "key_screensaver_battery_in_bold"; + public static final String KEY_SCREENSAVER_BATTERY_IN_ITALIC = "key_screensaver_battery_in_italic"; public static final String KEY_SCREENSAVER_DATE_IN_BOLD = "key_screensaver_date_in_bold"; public static final String KEY_SCREENSAVER_DATE_IN_ITALIC = "key_screensaver_date_in_italic"; public static final String KEY_SCREENSAVER_NEXT_ALARM_IN_BOLD = "key_screensaver_next_alarm_in_bold"; public static final String KEY_SCREENSAVER_NEXT_ALARM_IN_ITALIC = "key_screensaver_next_alarm_in_italic"; + public static final String KEY_SCREENSAVER_BACKGROUND_IMAGE = "key_screensaver_background_image"; + public static final String KEY_ENABLE_SCREENSAVER_BLUR_EFFECT = "key_enable_screensaver_blur_effect"; + public static final String KEY_SCREENSAVER_BLUR_INTENSITY = "key_screensaver_blur_intensity"; public static final String KEY_SCREENSAVER_PREVIEW = "key_screensaver_preview"; public static final String KEY_SCREENSAVER_DAYDREAM_SETTINGS = "key_screensaver_daydream_settings"; @@ -155,24 +218,41 @@ public class PreferencesKeys { // ************** // Widget settings + public static final String KEY_ANALOG_WIDGET_CUSTOMIZATION = "key_analog_widget_customization"; public static final String KEY_DIGITAL_WIDGET_CUSTOMIZATION = "key_digital_widget_customization"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_CUSTOMIZATION = "key_vertical_digital_widget_customization"; public static final String KEY_NEXT_ALARM_WIDGET_CUSTOMIZATION = "key_next_alarm_widget_customization"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_CUSTOMIZATION = "key_material_you_analog_widget_customization"; public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOMIZATION = "key_material_you_digital_widget_customization"; public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOMIZATION = "key_material_you_vertical_digital_widget_customization"; public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOMIZATION = "key_material_you_next_alarm_widget_customization"; // Analog Widget + public static final String KEY_ANALOG_WIDGET_CLOCK_DIAL = "key_analog_widget_clock_dial"; public static final String KEY_ANALOG_WIDGET_WITH_SECOND_HAND = "key_analog_widget_with_second_hand"; + public static final String KEY_ANALOG_WIDGET_CLOCK_SECOND_HAND = "key_analog_widget_clock_second_hand"; + public static final String KEY_ANALOG_WIDGET_DEFAULT_DIAL_COLOR = "key_analog_widget_default_dial_color"; + public static final String KEY_ANALOG_WIDGET_CUSTOM_DIAL_COLOR = "key_analog_widget_custom_dial_color"; + public static final String KEY_ANALOG_WIDGET_DEFAULT_HOUR_HAND_COLOR = "key_analog_widget_default_hour_hand_color"; + public static final String KEY_ANALOG_WIDGET_CUSTOM_HOUR_HAND_COLOR = "key_analog_widget_custom_hour_hand_color"; + public static final String KEY_ANALOG_WIDGET_DEFAULT_MINUTE_HAND_COLOR = "key_analog_widget_default_minute_hand_color"; + public static final String KEY_ANALOG_WIDGET_CUSTOM_MINUTE_HAND_COLOR = "key_analog_widget_custom_minute_hand_color"; + public static final String KEY_ANALOG_WIDGET_DEFAULT_SECOND_HAND_COLOR = "key_analog_widget_default_second_hand_color"; + public static final String KEY_ANALOG_WIDGET_CUSTOM_SECOND_HAND_COLOR = "key_analog_widget_custom_second_hand_color"; // Digital Widget + public static final String KEY_DIGITAL_WIDGET_DISPLAY_TEXT_UPPERCASE = "key_digital_widget_display_text_uppercase"; + public static final String KEY_DIGITAL_WIDGET_DISPLAY_TEXT_SHADOW = "key_digital_widget_display_text_shadow"; public static final String KEY_DIGITAL_WIDGET_DISPLAY_SECONDS = "key_digital_widget_display_seconds"; public static final String KEY_DIGITAL_WIDGET_HIDE_AM_PM = "key_digital_widget_hide_am_pm"; + public static final String KEY_DIGITAL_WIDGET_DISPLAY_BACKGROUND = "key_digital_widget_display_background"; + public static final String KEY_DIGITAL_WIDGET_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = "key_digital_widget_customize_background_corner_radius"; + public static final String KEY_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS = "key_digital_widget_background_corner_radius"; public static final String KEY_DIGITAL_WIDGET_DISPLAY_DATE = "key_digital_widget_display_date"; public static final String KEY_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = "key_digital_widget_display_next_alarm"; - public static final String KEY_DIGITAL_WIDGET_DISPLAY_BACKGROUND = "key_digital_widget_display_background"; - public static final String KEY_DIGITAL_WIDGET_BACKGROUND_COLOR = "key_digital_widget_background_color"; public static final String KEY_DIGITAL_WIDGET_WORLD_CITIES_DISPLAYED = "key_digital_widget_world_cities_displayed"; + public static final String KEY_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = "key_digital_widget_apply_horizontal_padding"; + public static final String KEY_DIGITAL_WIDGET_BACKGROUND_COLOR = "key_digital_widget_background_color"; public static final String KEY_DIGITAL_WIDGET_DEFAULT_CLOCK_COLOR = "key_digital_widget_default_clock_color"; public static final String KEY_DIGITAL_WIDGET_CUSTOM_CLOCK_COLOR = "key_digital_widget_custom_clock_color"; public static final String KEY_DIGITAL_WIDGET_DEFAULT_DATE_COLOR = "key_digital_widget_default_date_color"; @@ -184,10 +264,14 @@ public class PreferencesKeys { public static final String KEY_DIGITAL_WIDGET_DEFAULT_CITY_NAME_COLOR = "key_digital_widget_default_city_name_color"; public static final String KEY_DIGITAL_WIDGET_CUSTOM_CITY_NAME_COLOR = "key_digital_widget_custom_city_name_color"; public static final String KEY_DIGITAL_WIDGET_MAXIMUM_CLOCK_FONT_SIZE = "key_digital_widget_maximum_clock_font_size"; - public static final String KEY_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = "key_digital_widget_apply_horizontal_padding"; // Next Alarm Widget + public static final String KEY_NEXT_ALARM_WIDGET_DISPLAY_TEXT_UPPERCASE = "key_next_alarm_widget_display_text_uppercase"; + public static final String KEY_NEXT_ALARM_WIDGET_DISPLAY_TEXT_SHADOW = "key_next_alarm_widget_display_text_shadow"; public static final String KEY_NEXT_ALARM_WIDGET_DISPLAY_BACKGROUND = "key_next_alarm_widget_display_background"; + public static final String KEY_NEXT_ALARM_WIDGET_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = "key_next_alarm_widget_customize_background_corner_radius"; + public static final String KEY_NEXT_ALARM_WIDGET_BACKGROUND_CORNER_RADIUS = "key_next_alarm_widget_background_corner_radius"; + public static final String KEY_NEXT_ALARM_WIDGET_APPLY_HORIZONTAL_PADDING = "key_next_alarm_widget_apply_horizontal_padding"; public static final String KEY_NEXT_ALARM_WIDGET_BACKGROUND_COLOR = "key_next_alarm_widget_background_color"; public static final String KEY_NEXT_ALARM_WIDGET_DEFAULT_TITLE_COLOR = "key_next_alarm_widget_default_title_color"; public static final String KEY_NEXT_ALARM_WIDGET_CUSTOM_TITLE_COLOR = "key_next_alarm_widget_custom_title_color"; @@ -196,105 +280,95 @@ public class PreferencesKeys { public static final String KEY_NEXT_ALARM_WIDGET_DEFAULT_ALARM_COLOR = "key_next_alarm_widget_default_alarm_color"; public static final String KEY_NEXT_ALARM_WIDGET_CUSTOM_ALARM_COLOR = "key_next_alarm_widget_custom_alarm_color"; public static final String KEY_NEXT_ALARM_WIDGET_MAXIMUM_FONT_SIZE = "key_next_alarm_widget_maximum_font_size"; - public static final String KEY_NEXT_ALARM_WIDGET_APPLY_HORIZONTAL_PADDING = "key_next_alarm_widget_apply_horizontal_padding"; // Vertical Digital Widget + public static final String KEY_VERTICAL_DIGITAL_WIDGET_DISPLAY_TEXT_UPPERCASE = "key_vertical_digital_widget_display_text_uppercase"; + public static final String KEY_VERTICAL_DIGITAL_WIDGET_DISPLAY_TEXT_SHADOW = "key_vertical_digital_widget_display_text_shadow"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_DISPLAY_BACKGROUND = "key_vertical_digital_widget_display_background"; + public static final String KEY_VERTICAL_WIDGET_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = "key_vertical_widget_customize_background_corner_radius"; + public static final String KEY_VERTICAL_WIDGET_BACKGROUND_CORNER_RADIUS = "key_vertical_widget_background_corner_radius"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_DISPLAY_DATE = "key_vertical_digital_widget_display_date"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = "key_vertical_digital_widget_display_next_alarm"; + public static final String KEY_VERTICAL_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = "key_vertical_digital_widget_apply_horizontal_padding"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_BACKGROUND_COLOR = "key_vertical_digital_widget_background_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_DEFAULT_HOURS_COLOR = "key_vertical_digital_widget_default_hours_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_CUSTOM_HOURS_COLOR = "key_vertical_digital_widget_custom_hours_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_DEFAULT_MINUTES_COLOR = "key_vertical_digital_widget_default_minutes_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_CUSTOM_MINUTES_COLOR = "key_vertical_digital_widget_custom_minutes_color"; - public static final String KEY_VERTICAL_DIGITAL_WIDGET_DATE_DEFAULT_COLOR = "key_vertical_digital_widget_default_date_color"; + public static final String KEY_VERTICAL_DIGITAL_WIDGET_DEFAULT_DATE_COLOR = "key_vertical_digital_widget_default_date_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_CUSTOM_DATE_COLOR = "key_vertical_digital_widget_custom_date_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_DEFAULT_NEXT_ALARM_COLOR = "key_vertical_digital_widget_default_next_alarm_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_CUSTOM_NEXT_ALARM_COLOR = "key_vertical_digital_widget_custom_next_alarm_color"; public static final String KEY_VERTICAL_DIGITAL_WIDGET_MAXIMUM_CLOCK_FONT_SIZE = "key_vertical_digital_widget_maximum_clock_font_size"; - public static final String KEY_VERTICAL_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = "key_vertical_digital_widget_apply_horizontal_padding"; // Material You Analog Widget + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_CLOCK_DIAL = "key_material_you_analog_widget_clock_dial"; public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_WITH_SECOND_HAND = "key_material_you_analog_widget_with_second_hand"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_DEFAULT_DIAL_COLOR = "key_material_you_analog_widget_default_dial_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_DIAL_COLOR = "key_material_you_analog_widget_custom_dial_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_DEFAULT_HOUR_HAND_COLOR = "key_material_you_analog_widget_default_hour_hand_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_HOUR_HAND_COLOR = "key_material_you_analog_widget_custom_hour_hand_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_DEFAULT_MINUTE_HAND_COLOR = "key_material_you_analog_widget_default_minute_hand_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_MINUTE_HAND_COLOR = "key_material_you_analog_widget_custom_minute_hand_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_DEFAULT_SECOND_HAND_COLOR = "key_material_you_analog_widget_default_second_hand_color"; + public static final String KEY_MATERIAL_YOU_ANALOG_WIDGET_CUSTOM_SECOND_HAND_COLOR = "key_material_you_analog_widget_custom_second_hand_color"; // Material You Digital Widget - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_SECONDS_DISPLAYED = - "key_material_you_digital_widget_seconds_displayed"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_HIDE_AM_PM = - "key_material_you_digital_widget_hide_am_pm"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_DATE = - "key_material_you_digital_widget_display_date"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = - "key_material_you_digital_widget_display_next_alarm"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_WORLD_CITIES_DISPLAYED = - "key_material_you_digital_widget_world_cities_displayed"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_CLOCK_COLOR = - "key_material_you_digital_widget_default_clock_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_CLOCK_COLOR = - "key_material_you_digital_widget_custom_clock_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_DATE_COLOR = - "key_material_you_digital_widget_default_date_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_DATE_COLOR = - "key_material_you_digital_widget_custom_date_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_NEXT_ALARM_COLOR = - "key_material_you_digital_widget_default_next_alarm_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_NEXT_ALARM_COLOR = - "key_material_you_digital_widget_custom_next_alarm_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_CITY_CLOCK_COLOR = - "key_material_you_digital_widget_default_city_clock_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_CITY_CLOCK_COLOR = - "key_material_you_digital_widget_custom_city_clock_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_CITY_NAME_COLOR = - "key_material_you_digital_widget_default_city_name_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_CITY_NAME_COLOR = - "key_material_you_digital_widget_custom_city_name_color"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_MAXIMUM_CLOCK_FONT_SIZE = - "key_material_you_digital_widget_maximum_clock_font_size"; - public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = - "key_material_you_digital_widget_apply_horizontal_padding"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_SECONDS_DISPLAYED = "key_material_you_digital_widget_seconds_displayed"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_HIDE_AM_PM = "key_material_you_digital_widget_hide_am_pm"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_BACKGROUND = "key_material_you_digital_widget_display_background"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = "key_material_you_digital_widget_customize_background_corner_radius"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS = "key_material_you_digital_widget_background_corner_radius"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_DATE = "key_material_you_digital_widget_display_date"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = "key_material_you_digital_widget_display_next_alarm"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_WORLD_CITIES_DISPLAYED = "key_material_you_digital_widget_world_cities_displayed"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = "key_material_you_digital_widget_apply_horizontal_padding"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_BACKGROUND_COLOR = "key_material_you_digital_widget_default_background_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_BACKGROUND_COLOR = "key_material_you_digital_widget_custom_background_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_CLOCK_COLOR = "key_material_you_digital_widget_default_clock_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_CLOCK_COLOR = "key_material_you_digital_widget_custom_clock_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_DATE_COLOR = "key_material_you_digital_widget_default_date_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_DATE_COLOR = "key_material_you_digital_widget_custom_date_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_NEXT_ALARM_COLOR = "key_material_you_digital_widget_default_next_alarm_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_NEXT_ALARM_COLOR = "key_material_you_digital_widget_custom_next_alarm_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_CITY_CLOCK_COLOR = "key_material_you_digital_widget_default_city_clock_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_CITY_CLOCK_COLOR = "key_material_you_digital_widget_custom_city_clock_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_DEFAULT_CITY_NAME_COLOR = "key_material_you_digital_widget_default_city_name_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_CUSTOM_CITY_NAME_COLOR = "key_material_you_digital_widget_custom_city_name_color"; + public static final String KEY_MATERIAL_YOU_DIGITAL_WIDGET_MAXIMUM_CLOCK_FONT_SIZE = "key_material_you_digital_widget_maximum_clock_font_size"; // Material You Vertical Digital Widget - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DISPLAY_DATE = - "key_material_you_vertical_digital_widget_display_date"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = - "key_material_you_vertical_digital_widget_display_next_alarm"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_HOURS_COLOR = - "key_material_you_vertical_digital_widget_default_hours_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_HOURS_COLOR = - "key_material_you_vertical_digital_widget_custom_hours_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_MINUTES_COLOR = - "key_material_you_vertical_digital_widget_default_minutes_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_MINUTES_COLOR = - "key_material_you_vertical_digital_widget_custom_minutes_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_DATE_COLOR = - "key_material_you_vertical_digital_widget_default_date_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_DATE_COLOR = - "key_material_you_vertical_digital_widget_custom_date_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_NEXT_ALARM_COLOR = - "key_material_you_vertical_digital_widget_default_next_alarm_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_NEXT_ALARM_COLOR = - "key_material_you_vertical_digital_widget_custom_next_alarm_color"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_MAXIMUM_CLOCK_FONT_SIZE = - "key_material_you_vertical_digital_widget_maximum_clock_font_size"; - public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = - "key_material_you_vertical_digital_widget_apply_horizontal_padding"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DISPLAY_BACKGROUND = "key_material_you_vertical_digital_widget_display_background"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = "key_material_you_vertical_digital_widget_customize_background_corner_radius"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS = "key_material_you_vertical_digital_widget_background_corner_radius"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DISPLAY_DATE = "key_material_you_vertical_digital_widget_display_date"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DISPLAY_NEXT_ALARM = "key_material_you_vertical_digital_widget_display_next_alarm"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_APPLY_HORIZONTAL_PADDING = "key_material_you_vertical_digital_widget_apply_horizontal_padding"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_BACKGROUND_COLOR = "key_material_you_vertical_digital_widget_default_background_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_BACKGROUND_COLOR = "key_material_you_vertical_digital_widget_custom_background_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_HOURS_COLOR = "key_material_you_vertical_digital_widget_default_hours_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_HOURS_COLOR = "key_material_you_vertical_digital_widget_custom_hours_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_MINUTES_COLOR = "key_material_you_vertical_digital_widget_default_minutes_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_MINUTES_COLOR = "key_material_you_vertical_digital_widget_custom_minutes_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_DATE_COLOR = "key_material_you_vertical_digital_widget_default_date_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_DATE_COLOR = "key_material_you_vertical_digital_widget_custom_date_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_DEFAULT_NEXT_ALARM_COLOR = "key_material_you_vertical_digital_widget_default_next_alarm_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_CUSTOM_NEXT_ALARM_COLOR = "key_material_you_vertical_digital_widget_custom_next_alarm_color"; + public static final String KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_MAXIMUM_CLOCK_FONT_SIZE = "key_material_you_vertical_digital_widget_maximum_clock_font_size"; // Material You Next Alarm Widget - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_TITLE_COLOR = - "key_material_you_next_alarm_widget_default_title_color"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_TITLE_COLOR = - "key_material_you_next_alarm_widget_custom_title_color"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_ALARM_TITLE_COLOR = - "key_material_you_next_alarm_widget_default_alarm_title_color"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_ALARM_TITLE_COLOR = - "key_material_you_next_alarm_widget_custom_alarm_title_color"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_ALARM_COLOR = - "key_material_you_next_alarm_widget_default_alarm_color"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_ALARM_COLOR = - "key_material_you_next_alarm_widget_custom_alarm_color"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_MAXIMUM_FONT_SIZE = - "key_material_you_next_alarm_widget_maximum_font_size"; - public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_APPLY_HORIZONTAL_PADDING = - "key_material_you_next_alarm_widget_apply_horizontal_padding"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DISPLAY_BACKGROUND = "key_material_you_next_alarm_widget_display_background"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOMIZE_BACKGROUND_CORNER_RADIUS = "key_material_you_next_alarm_widget_customize_background_corner_radius"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_BACKGROUND_CORNER_RADIUS = "key_material_you_next_alarm_widget_background_corner_radius"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_APPLY_HORIZONTAL_PADDING = "key_material_you_next_alarm_widget_apply_horizontal_padding"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_BACKGROUND_COLOR = "key_material_you_next_alarm_widget_default_background_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_BACKGROUND_COLOR = "key_material_you_next_alarm_widget_custom_background_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_TITLE_COLOR = "key_material_you_next_alarm_widget_default_title_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_TITLE_COLOR = "key_material_you_next_alarm_widget_custom_title_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_ALARM_TITLE_COLOR = "key_material_you_next_alarm_widget_default_alarm_title_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_ALARM_TITLE_COLOR = "key_material_you_next_alarm_widget_custom_alarm_title_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_DEFAULT_ALARM_COLOR = "key_material_you_next_alarm_widget_default_alarm_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_CUSTOM_ALARM_COLOR = "key_material_you_next_alarm_widget_custom_alarm_color"; + public static final String KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_MAXIMUM_FONT_SIZE = "key_material_you_next_alarm_widget_maximum_font_size"; } diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/AlarmSnoozeDurationPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/AlarmSnoozeDurationPreference.java new file mode 100644 index 000000000..35baf23ac --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/AlarmSnoozeDurationPreference.java @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.settings.PreferencesDefaultValues.ALARM_SNOOZE_DURATION_DISABLED; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_SNOOZE_DURATION; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import com.best.deskclock.R; + +/** + * A custom {@link DialogPreference} that allows users to select the snooze duration for alarms. + *

+ * This preference stores the snooze duration in minutes using Android's shared preferences system. + * When shown in the preferences UI, it opens a custom dialog where the user can input hours and minutes. + *

+ */ +public class AlarmSnoozeDurationPreference extends DialogPreference { + + /** + * Constructs a new AlarmSnoozeDurationPreference instance, used to manage user preferences + * related to alarm snooze duration. + * + * @param context The application context in which this preference is used. + * @param attrs The attribute set from XML that may include custom parameters. + */ + public AlarmSnoozeDurationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(true); + } + + @Override + public CharSequence getSummary() { + int minutes = getSnoozeDuration(); + + if (minutes == ALARM_SNOOZE_DURATION_DISABLED) { + return getContext().getString(R.string.snooze_duration_none); + } + + int h = minutes / 60; + int m = minutes % 60; + + if (h > 0 && m > 0) { + String hoursString = getContext().getResources().getQuantityString(R.plurals.hours, h, h); + String minutesString = getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + return String.format("%s %s", hoursString, minutesString); + } else if (h > 0) { + return getContext().getResources().getQuantityString(R.plurals.hours, h, h); + } else { + return getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + } + } + + /** + * Returns the currently persisted snooze delay duration in minutes. + * + * @return The snooze delay in minutes, or 10 if no value has been previously persisted. + */ + public int getSnoozeDuration() { + return getPersistedInt(DEFAULT_ALARM_SNOOZE_DURATION); + } + + /** + * Persists the snooze delay duration in minutes. + * + * @param minutes The snooze duration to be stored, in minutes. + */ + public void setSnoozeDuration(int minutes) { + persistInt(minutes); + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/AlarmVolumePreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/AlarmVolumePreference.java new file mode 100644 index 000000000..16ee6d028 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/AlarmVolumePreference.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ + +package com.best.deskclock.settings.custompreference; + +import static android.content.Context.AUDIO_SERVICE; +import static android.media.AudioManager.STREAM_ALARM; +import static android.view.View.GONE; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.utils.RingtoneUtils.ALARM_PREVIEW_DURATION_MS; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.best.deskclock.R; +import com.best.deskclock.data.DataModel; +import com.best.deskclock.ringtone.RingtonePreviewKlaxon; +import com.best.deskclock.utils.RingtoneUtils; +import com.best.deskclock.utils.ThemeUtils; +import com.google.android.material.slider.Slider; + +import java.util.Locale; + +public class AlarmVolumePreference extends Preference { + + private Context mContext; + private SharedPreferences mPrefs; + private Slider mSlider; + private ImageView mSliderMinus; + private ImageView mSliderPlus; + private AudioManager mAudioManager; + private int mMinVolume; + private final Handler mRingtoneHandler = new Handler(Looper.getMainLooper()); + private Runnable mRingtoneStopRunnable; + private boolean mIsPreviewPlaying = false; + + public AlarmVolumePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + if (holder.itemView.isInEditMode()) { + // Skip logic during Android Studio preview + return; + } + + mContext = getContext(); + mPrefs = getDefaultSharedPreferences(mContext); + + super.onBindViewHolder(holder); + + mAudioManager = (AudioManager) mContext.getSystemService(AUDIO_SERVICE); + + // Disable click feedback for this preference. + holder.itemView.setClickable(false); + + // Minimum volume for alarm is not 0, calculate it. + mMinVolume = RingtoneUtils.getAlarmMinVolume(mAudioManager); + int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_ALARM) - mMinVolume; + mSlider = (Slider) holder.findViewById(R.id.slider); + mSlider.setValueTo(maxVolume); + mSlider.setValueFrom(0f); + mSlider.setStepSize(1f); + mSlider.setValue((float) mAudioManager.getStreamVolume(STREAM_ALARM) - mMinVolume); + + final TextView sliderSummary = (TextView) holder.findViewById(android.R.id.summary); + updateSliderSummary(sliderSummary); + + mSliderMinus = (ImageView) holder.findViewById(R.id.slider_minus_icon); + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_volume_down)); + + mSliderPlus = (ImageView) holder.findViewById(R.id.slider_plus_icon); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_volume_up)); + + setupVolumeSliderButton(mSliderMinus, -1); + setupVolumeSliderButton(mSliderPlus, +1); + updateSliderButtonStates(); + + final TextView resetSlider = (TextView) holder.findViewById(R.id.reset_slider_value); + resetSlider.setVisibility(GONE); + + final ContentObserver volumeObserver = new ContentObserver(mSlider.getHandler()) { + @Override + public void onChange(boolean selfChange) { + // Volume was changed elsewhere, update our slider. + float currentVolume = (float) (mAudioManager.getStreamVolume(STREAM_ALARM) - mMinVolume); + mSlider.setValue(currentVolume); + updateSliderSummary(sliderSummary); + } + }; + + mSlider.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { + mContext.getContentResolver().registerContentObserver(Settings.System.CONTENT_URI, + true, volumeObserver); + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + mContext.getContentResolver().unregisterContentObserver(volumeObserver); + } + }); + + mSlider.addOnChangeListener((slider, progress, fromUser) -> { + if (fromUser) { + int newVolume = (int) progress + mMinVolume; + mAudioManager.setStreamVolume(STREAM_ALARM, newVolume, 0); + updateSliderSummary(sliderSummary); + updateSliderButtonStates(); + } + }); + + mSlider.addOnSliderTouchListener(new Slider.OnSliderTouchListener() { + @Override + public void onStartTrackingTouch(@NonNull Slider slider) { + startRingtonePreview(); + } + + @Override + public void onStopTrackingTouch(@NonNull Slider slider) { + } + }); + } + + /** + * Updates the summary text view to show the current alarm volume as a percentage. + */ + private void updateSliderSummary(TextView sliderSummary) { + int currentVolume = mAudioManager.getStreamVolume(STREAM_ALARM); + int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_ALARM); + int volumePercentage = (int) (((float) currentVolume / maxVolume) * 100); + + String formattedText = String.format(Locale.getDefault(), "%d%%", volumePercentage); + sliderSummary.post(() -> sliderSummary.setText(formattedText)); + } + + /** + * Configures the minus or plus button to adjust the alarm volume when clicked. + * + * @param button the ImageView button (minus or plus) + * @param delta +1 to increase volume, -1 to decrease + */ + private void setupVolumeSliderButton(@NonNull ImageView button, int delta) { + button.setOnClickListener(v -> { + int current = (int) mSlider.getValue(); + int max = (int) mSlider.getValueTo(); + + int newValue = Math.min(Math.max(current + delta, 0), max); + if (newValue != current) { + mSlider.setValue(newValue); + updateVolume(mAudioManager); + startRingtonePreview(); + updateSliderButtonStates(); + } + }); + } + + /** + * Enables or disables the minus and plus buttons based on the current slider value + * and the enabled state of the preference. + */ + private void updateSliderButtonStates() { + boolean isPrefEnabled = isEnabled(); + int progress = (int) mSlider.getValue(); + int max = (int) mSlider.getValueTo(); + + ThemeUtils.updateSliderButtonEnabledState(mContext, mSliderMinus, isPrefEnabled + && progress > 0 + && !RingtoneUtils.hasExternalAudioDeviceConnected(mContext, mPrefs)); + ThemeUtils.updateSliderButtonEnabledState(mContext, mSliderPlus, isPrefEnabled + && progress < max + && !RingtoneUtils.hasExternalAudioDeviceConnected(mContext, mPrefs)); + } + + private void updateVolume(AudioManager audioManager) { + int newVolume = (int) mSlider.getValue() + mMinVolume; + audioManager.setStreamVolume(STREAM_ALARM, newVolume, 0); + } + + private void startRingtonePreview() { + if (mRingtoneStopRunnable != null) { + mRingtoneHandler.removeCallbacks(mRingtoneStopRunnable); + } + + // If we are not currently playing, start. + Uri ringtoneUri = DataModel.getDataModel().getAlarmRingtoneUriFromSettings(); + if (RingtoneUtils.isRandomRingtone(ringtoneUri)) { + ringtoneUri = RingtoneUtils.getRandomRingtoneUri(); + } else if (RingtoneUtils.isRandomCustomRingtone(ringtoneUri)) { + ringtoneUri = RingtoneUtils.getRandomCustomRingtoneUri(); + } + + RingtonePreviewKlaxon.start(mContext, mPrefs, ringtoneUri); + mIsPreviewPlaying = true; + + mRingtoneStopRunnable = this::stopRingtonePreview; + // Stop the preview after 5 seconds + mRingtoneHandler.postDelayed(mRingtoneStopRunnable, ALARM_PREVIEW_DURATION_MS); + } + + public void stopRingtonePreview() { + if (!mIsPreviewPlaying) { + return; + } + + if (mRingtoneStopRunnable != null) { + mRingtoneHandler.removeCallbacks(mRingtoneStopRunnable); + } + + RingtonePreviewKlaxon.stop(mContext, mPrefs); + RingtonePreviewKlaxon.releaseResources(); + + mIsPreviewPlaying = false; + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/AutoSilenceDurationPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/AutoSilenceDurationPreference.java new file mode 100644 index 000000000..9c6cf7c94 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/AutoSilenceDurationPreference.java @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_AUTO_SILENCE_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_TIMER_AUTO_SILENCE_DURATION; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_END_OF_RINGTONE; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; +import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_AUTO_SILENCE_DURATION; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import com.best.deskclock.R; + +public class AutoSilenceDurationPreference extends DialogPreference { + + /** + * Constructs a new AutoSilenceDurationPreference instance, used to manage user preferences + * related to auto silence duration. + * + * @param context The application context in which this preference is used. + * @param attrs The attribute set from XML that may include custom parameters. + */ + public AutoSilenceDurationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(true); + } + + @Override + public CharSequence getSummary() { + int duration = getAutoSilenceDuration(); + + if (duration == TIMEOUT_END_OF_RINGTONE) { + return getContext().getString(R.string.auto_silence_end_of_ringtone); + } else if (duration == TIMEOUT_NEVER) { + return getContext().getString(R.string.label_never); + } + + int m = duration / 60; + int s = duration % 60; + + if (m > 0 && s > 0) { + String minutesString = getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + String secondsString = getContext().getResources().getQuantityString(R.plurals.seconds, s, s); + return String.format("%s %s", minutesString, secondsString); + } else if (m > 0) { + return getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + } else { + return getContext().getResources().getQuantityString(R.plurals.seconds, s, s); + } + } + + /** + * Returns the currently persisted auto silence duration + * For alarms : in minutes. + * For timers : in seconds. + * + * @return The auto silence duration, or none if no value has been previously persisted. + */ + public int getAutoSilenceDuration() { + return getPersistedInt(isForTimer() + ? DEFAULT_TIMER_AUTO_SILENCE_DURATION + : DEFAULT_AUTO_SILENCE_DURATION); + } + + /** + * @return {@code true} if this preference is for timers; {@code false} if it is for alarms. + */ + public boolean isForTimer() { + return KEY_TIMER_AUTO_SILENCE_DURATION.equals(getKey()); + } + + /** + * Persists the auto silence duration. + * + * @param duration Duration in seconds (for timers) or minutes (for alarms). + */ + public void setAutoSilenceDuration(int duration) { + persistInt(duration); + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/ColorPickerPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/ColorPickerPreference.java new file mode 100644 index 000000000..52d360542 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/ColorPickerPreference.java @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static androidx.core.util.TypedValueCompat.dpToPx; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceViewHolder; + +import com.best.deskclock.R; +import com.best.deskclock.utils.ThemeUtils; + +import com.rarepebble.colorpicker.ColorPreference; + +/** + * This class extends {@link ColorPreference} and overrides the view binding to show a circular thumbnail + * with the currently selected color. + */ +public class ColorPickerPreference extends ColorPreference { + + public ColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + if (holder.itemView.isInEditMode()) { + // Skip logic during Android Studio preview + return; + } + + super.onBindViewHolder(holder); + + View thumbnail = addThumbnail(holder.itemView); + if (thumbnail != null) { + View colorPreview = thumbnail.findViewById(R.id.colorPreview); + if (colorPreview != null) { + int color = getColor(); + GradientDrawable circle = (GradientDrawable) ThemeUtils.circleDrawable(); + circle.setColor(color); + colorPreview.setBackground(circle); + } + + View border = thumbnail.findViewById(R.id.border); + if (border != null) { + GradientDrawable borderCircle = new GradientDrawable(); + borderCircle.setShape(GradientDrawable.OVAL); + borderCircle.setColor(Color.TRANSPARENT); + borderCircle.setStroke( + (int) dpToPx(2, getContext().getResources().getDisplayMetrics()), + ContextCompat.getColor(getContext(), R.color.md_theme_outline) + ); + border.setBackground(borderCircle); + } + } + } + + private View addThumbnail(View view) { + LinearLayout widgetFrameView = view.findViewById(android.R.id.widget_frame); + if (widgetFrameView == null) { + return null; + } + + widgetFrameView.setVisibility(View.VISIBLE); + widgetFrameView.removeAllViews(); + + LayoutInflater.from(getContext()).inflate(R.layout.settings_preference_color_thumbnail, + widgetFrameView); + + return widgetFrameView.findViewById(R.id.thumbnail); + } +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomAboutTitlePreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomAboutTitlePreference.java new file mode 100644 index 000000000..b70ec7314 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomAboutTitlePreference.java @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.ThemeUtils; + +/** + * A {@link Preference} with a custom layout applied to the title in About. + */ +public class CustomAboutTitlePreference extends Preference { + + public CustomAboutTitlePreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + if (holder.itemView.isInEditMode()) { + return; + } + + super.onBindViewHolder(holder); + + Context context = holder.itemView.getContext(); + SharedPreferences prefs = getDefaultSharedPreferences(context); + String fontPath = SettingsDAO.getGeneralFont(prefs); + + TextView slogan = (TextView) holder.findViewById(R.id.slogan); + if (slogan != null) { + slogan.setTypeface(ThemeUtils.loadFont(fontPath)); + } + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomListPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomListPreference.java new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomPreference.java new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomPreferenceCategory.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomPreferenceCategory.java new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSeekbarPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSeekbarPreference.java new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSliderPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSliderPreference.java new file mode 100644 index 000000000..df6b1ec38 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSliderPreference.java @@ -0,0 +1,670 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ANALOG_CLOCK_SIZE; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_DIGITAL_CLOCK_FONT_SIZE; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_SHADOW_OFFSET; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_BLUR_INTENSITY; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_ALARM_TITLE_FONT_SIZE_PREF; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_EXTERNAL_AUDIO_DEVICE_VOLUME; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_MATERIAL_YOU_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_SCREENSAVER_BRIGHTNESS; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_SHAKE_INTENSITY; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_TIMER_SHAKE_INTENSITY; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_WIDGETS_FONT_SIZE; +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_ANALOG_CLOCK_SIZE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_BLUR_INTENSITY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_DIGITAL_CLOCK_FONT_SIZE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_SHADOW_OFFSET; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_TITLE_FONT_SIZE_PREF; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ANALOG_CLOCK_SIZE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_DIGITAL_CLOCK_FONT_SIZE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_EXTERNAL_AUDIO_DEVICE_VOLUME; +import static com.best.deskclock.settings.PreferencesKeys.KEY_MATERIAL_YOU_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_NEXT_ALARM_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SCREENSAVER_ANALOG_CLOCK_SIZE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SCREENSAVER_BLUR_INTENSITY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SCREENSAVER_BRIGHTNESS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SCREENSAVER_DIGITAL_CLOCK_FONT_SIZE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SHAKE_INTENSITY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_BLUR_INTENSITY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_SHADOW_OFFSET; +import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_SHAKE_INTENSITY; +import static com.best.deskclock.settings.PreferencesKeys.KEY_VERTICAL_WIDGET_BACKGROUND_CORNER_RADIUS; +import static com.best.deskclock.utils.RingtoneUtils.ALARM_PREVIEW_DURATION_MS; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.widget.TextViewCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.best.deskclock.R; +import com.best.deskclock.data.DataModel; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.ringtone.RingtonePreviewKlaxon; +import com.best.deskclock.utils.RingtoneUtils; +import com.best.deskclock.utils.ThemeUtils; +import com.best.deskclock.utils.WidgetUtils; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.slider.Slider; + +import java.util.Locale; + +public class CustomSliderPreference extends Preference { + + private static final int MIN_FONT_SIZE_VALUE = 20; + private static final int MIN_CORNER_RADIUS_VALUE = 0; + private static final int MIN_SHAKE_INTENSITY_VALUE = DEFAULT_SHAKE_INTENSITY; + private static final int MIN_TIMER_SHAKE_INTENSITY_VALUE = DEFAULT_TIMER_SHAKE_INTENSITY; + private static final int MIN_BRIGHTNESS_VALUE = 0; + private static final int MIN_EXTERNAL_AUDIO_DEVICE_VOLUME = 10; + private static final int MIN_SHADOW_OFFSET_VALUE = 1; + private static final int MIN_BLUR_INTENSITY_VALUE = 1; + private static final int MIN_ANALOG_CLOCK_SIZE_VALUE = 1; + + // The max values below correspond to the max values defined in the preferences XML files. + private static final int MAX_BRIGHTNESS_VALUE = 100; + private static final int MAX_CORNER_RADIUS_VALUE = 100; + private static final int MAX_SHAKE_INTENSITY_VALUE = 55; + private static final int MAX_TIMER_SHAKE_INTENSITY_VALUE = 55; + private static final int MAX_SHADOW_OFFSET_VALUE = 20; + private static final int MAX_BLUR_INTENSITY_VALUE = 100; + private static final int MAX_EXTERNAL_AUDIO_DEVICE_VOLUME = 100; + private static final int MAX_ANALOG_CLOCK_SIZE_VALUE = 100; + private static final int MAX_FONT_SIZE_VALUE = 200; + + private Context mContext; + private SharedPreferences mPrefs; + private Slider mSlider; + private ImageView mSliderMinus; + private ImageView mSliderPlus; + private TextView mResetSlider; + private final Handler mRingtoneHandler = new Handler(Looper.getMainLooper()); + private Runnable mRingtoneStopRunnable; + private boolean mIsPreviewPlaying = false; + + public CustomSliderPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + /** + * Binds the preference view, initializes the slider and associated UI elements, + * and sets up listeners for user interactions. + */ + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + if (holder.itemView.isInEditMode()) { + // Skip logic during Android Studio preview + return; + } + + super.onBindViewHolder(holder); + + mContext = getContext(); + mPrefs = getDefaultSharedPreferences(mContext); + String fontPath = SettingsDAO.getGeneralFont(mPrefs); + + holder.itemView.setClickable(false); + + mSlider = (Slider) holder.findViewById(R.id.slider); + configureSliderBounds(); + mSlider.setStepSize(1f); + + final TextView sliderSummary = (TextView) holder.findViewById(android.R.id.summary); + setSliderProgress(sliderSummary); + + mSliderMinus = (ImageView) holder.findViewById(R.id.slider_minus_icon); + mSliderPlus = (ImageView) holder.findViewById(R.id.slider_plus_icon); + mResetSlider = (TextView) holder.findViewById(R.id.reset_slider_value); + + configureSliderButtonDrawables(); + setupSliderButton(mSliderMinus, isExternalAudioDeviceVolumePreference() ? -10 : -5, sliderSummary); + setupSliderButton(mSliderPlus, isExternalAudioDeviceVolumePreference() ? 10 : 5, sliderSummary); + updateSliderButtonStates(); + updateResetButtonStates(); + + mResetSlider.setTypeface(ThemeUtils.boldTypeface(fontPath)); + mResetSlider.setOnClickListener(v -> { + resetPreference(); + setSliderProgress(sliderSummary); + startRingtonePreviewForExternalAudioDevices(); + updateDigitalWidgets(); + updateSliderButtonStates(); + updateResetButtonStates(); + }); + + mSlider.addOnChangeListener((slider, progress, fromUser) -> { + if (fromUser) { + updateSliderSummary(sliderSummary, (int) progress); + updateSliderButtonStates(); + updateResetButtonStates(); + } + }); + + mSlider.addOnSliderTouchListener(new Slider.OnSliderTouchListener() { + @Override + public void onStartTrackingTouch(@NonNull Slider slider) { + startRingtonePreviewForExternalAudioDevices(); + } + + @Override + public void onStopTrackingTouch(@NonNull Slider slider) { + int finalProgress = (int) slider.getValue(); + saveSliderValue(finalProgress); + updateDigitalWidgets(); + } + }); + } + + /** + * Sets the minimum and maximum value of the slider based on the preference type. + */ + private void configureSliderBounds() { + if (isScreensaverBrightnessPreference()) { + mSlider.setValueTo(MAX_BRIGHTNESS_VALUE); + mSlider.setValueFrom(MIN_BRIGHTNESS_VALUE); + } else if (isDigitalWidgetBackgroundCornerRadius() + || isNextAlarmWidgetBackgroundCornerRadius() + || isVerticalWidgetBackgroundCornerRadius() + || isMaterialYouDigitalWidgetBackgroundCornerRadius() + || isMaterialYouNextAlarmWidgetBackgroundCornerRadius() + || isMaterialYouVerticalWidgetBackgroundCornerRadius()) { + mSlider.setValueTo(MAX_CORNER_RADIUS_VALUE); + mSlider.setValueFrom(MIN_CORNER_RADIUS_VALUE); + } else if (isShakeIntensityPreference()) { + mSlider.setValueTo(MAX_SHAKE_INTENSITY_VALUE); + mSlider.setValueFrom(MIN_SHAKE_INTENSITY_VALUE); + } else if (isTimerShakeIntensityPreference()) { + mSlider.setValueTo(MAX_TIMER_SHAKE_INTENSITY_VALUE); + mSlider.setValueFrom(MIN_TIMER_SHAKE_INTENSITY_VALUE); + } else if (isTimerShadowOffsetPreference() || isAlarmShadowOffsetPreference()) { + mSlider.setValueTo(MAX_SHADOW_OFFSET_VALUE); + mSlider.setValueFrom(MIN_SHADOW_OFFSET_VALUE); + } else if (isScreensaverBlurIntensityPreference() + || isTimerBlurIntensityPreference() + || isAlarmBlurIntensityPreference()) { + mSlider.setValueTo(MAX_BLUR_INTENSITY_VALUE); + mSlider.setValueFrom(MIN_BLUR_INTENSITY_VALUE); + } else if (isExternalAudioDeviceVolumePreference()) { + mSlider.setValueTo(MAX_EXTERNAL_AUDIO_DEVICE_VOLUME); + mSlider.setValueFrom(MIN_EXTERNAL_AUDIO_DEVICE_VOLUME); + } else if (isAnalogClockSizePreference() + || isScreensaverAnalogClockSizePreference() + || isAlarmAnalogClockSizePreference()) { + mSlider.setValueTo(MAX_ANALOG_CLOCK_SIZE_VALUE); + mSlider.setValueFrom(MIN_ANALOG_CLOCK_SIZE_VALUE); + } else { + mSlider.setValueTo(MAX_FONT_SIZE_VALUE); + mSlider.setValueFrom(MIN_FONT_SIZE_VALUE); + } + } + + /** + * Sets the slider value base on the current preference (screensaver brightness, widget font size, etc.) + * and updates the slider summary to reflect this value. + */ + private void setSliderProgress(TextView sliderSummary) { + int currentProgress = mPrefs.getInt(getKey(), getDefaultSliderValue()); + + updateSliderSummary(sliderSummary, currentProgress); + mSlider.setValue(currentProgress); + } + + /** + * Updates the slider summary. + */ + private void updateSliderSummary(TextView sliderSummary, int progress) { + if (progress == getDefaultSliderValue()) { + sliderSummary.setText(R.string.label_default); + } else if (isScreensaverBrightnessPreference() + || isScreensaverAnalogClockSizePreference() + || isExternalAudioDeviceVolumePreference() + || isAnalogClockSizePreference() + || isAlarmAnalogClockSizePreference()) { + String formattedText = String.format(Locale.getDefault(), "%d%%", progress); + sliderSummary.setText(formattedText); + } else { + sliderSummary.setText(String.valueOf(progress)); + } + } + + /** + * Sets the icons for the minus and plus buttons of the slider based on the current preference type. + */ + private void configureSliderButtonDrawables() { + if (isScreensaverBrightnessPreference()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_brightness_decrease)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_brightness_increase)); + } else if (isDigitalWidgetBackgroundCornerRadius() + || isNextAlarmWidgetBackgroundCornerRadius() + || isVerticalWidgetBackgroundCornerRadius() + || isMaterialYouDigitalWidgetBackgroundCornerRadius() + || isMaterialYouNextAlarmWidgetBackgroundCornerRadius() + || isMaterialYouVerticalWidgetBackgroundCornerRadius()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_rounded_corner_decrease)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_rounded_corner_increase)); + } else if (isShakeIntensityPreference() || isTimerShakeIntensityPreference()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_sensor_low)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_sensor_high)); + } else if (isTimerShadowOffsetPreference() || isAlarmShadowOffsetPreference()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_shadow_decrease)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_shadow_increase)); + } else if (isScreensaverBlurIntensityPreference() + || isTimerBlurIntensityPreference() + || isAlarmBlurIntensityPreference()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_blur_decrease)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_blur_increase)); + } else if (isExternalAudioDeviceVolumePreference()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_volume_down)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_volume_up)); + } else if (isAnalogClockSizePreference() + || isScreensaverAnalogClockSizePreference() + || isAlarmAnalogClockSizePreference()) { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_zoom_in)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_zoom_out)); + } else { + mSliderMinus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_text_decrease)); + mSliderPlus.setImageDrawable(AppCompatResources.getDrawable(mContext, R.drawable.ic_text_increase)); + } + } + + /** + * Configures the buttons to increase or decrease the value of the slider. + * The interface is updated with the new value and saved in the preferences. + * Sends a broadcast if it concerns buttons related to widget settings (widget font size). + */ + private void setupSliderButton(ImageView button, final int delta, final TextView sliderSummary) { + button.setOnClickListener(v -> { + int newSliderValue = getNewSliderValue(delta); + mSlider.setValue(newSliderValue); + updateSliderSummary(sliderSummary, newSliderValue); + saveSliderValue(newSliderValue); + startRingtonePreviewForExternalAudioDevices(); + updateDigitalWidgets(); + updateSliderButtonStates(); + updateResetButtonStates(); + }); + } + + /** + * Updates the enabled state of the minus and plus buttons based on the current slider value + * and the enabled state of the preference. + * + *

Disables the minus button if the current value is at the minimum allowed, + * and disables the plus button if the value is at the maximum allowed.

+ */ + private void updateSliderButtonStates() { + boolean isPrefEnabled = isEnabled(); + int current = (int) mSlider.getValue(); + int min = (int) mSlider.getValueFrom(); + int max = (int) mSlider.getValueTo(); + + ThemeUtils.updateSliderButtonEnabledState(mContext, mSliderMinus, isPrefEnabled && current > min); + ThemeUtils.updateSliderButtonEnabledState(mContext, mSliderPlus, isPrefEnabled && current < max); + } + + /** + * Updates the enabled state of the reset button based on the current slider value + * and the enabled state of the preference. + * + *

The reset button is enabled only when the slider value differs from the default + * value for the current preference type.

+ */ + private void updateResetButtonStates() { + boolean isEnabled = isEnabled() && !isSliderAtDefault((int) mSlider.getValue()); + mResetSlider.setEnabled(isEnabled); + + if (isEnabled) { + int enabledColor = MaterialColors.getColor(mContext, androidx.appcompat.R.attr.colorPrimary, Color.BLACK); + + mResetSlider.setTextColor(enabledColor); + TextViewCompat.setCompoundDrawableTintList(mResetSlider, ColorStateList.valueOf(enabledColor)); + } else { + int disabledColor = mContext.getColor(R.color.colorDisabled); + mResetSlider.setTextColor(disabledColor); + TextViewCompat.setCompoundDrawableTintList(mResetSlider, ColorStateList.valueOf(disabledColor)); + } + } + + /** + * @return true if the slider is currently set to its default value, depending on the preference type. + */ + private boolean isSliderAtDefault(int currentValue) { + return currentValue == getDefaultSliderValue(); + } + + /** + * @return a new value for the slider by applying a delta to the current value while respecting + * the minimum and maximum value of the slider. + */ + private int getNewSliderValue(int delta) { + int currentSliderValue = (int) mSlider.getValue(); + + return (int) Math.min(Math.max(currentSliderValue + delta, mSlider.getValueFrom()), mSlider.getValueTo()); + } + + /** + * @return the default value for the slider depending on the preference type. + */ + private int getDefaultSliderValue() { + if (isScreensaverBrightnessPreference()) { + return DEFAULT_SCREENSAVER_BRIGHTNESS; + } else if (isDigitalWidgetBackgroundCornerRadius() + || isNextAlarmWidgetBackgroundCornerRadius() + || isVerticalWidgetBackgroundCornerRadius()) { + return DEFAULT_WIDGET_BACKGROUND_CORNER_RADIUS; + } else if (isMaterialYouDigitalWidgetBackgroundCornerRadius() + || isMaterialYouNextAlarmWidgetBackgroundCornerRadius() + || isMaterialYouVerticalWidgetBackgroundCornerRadius()) { + return DEFAULT_MATERIAL_YOU_WIDGET_BACKGROUND_CORNER_RADIUS; + } else if (isShakeIntensityPreference()) { + return DEFAULT_SHAKE_INTENSITY; + } else if (isTimerShakeIntensityPreference()) { + return DEFAULT_TIMER_SHAKE_INTENSITY; + } else if (isScreensaverDigitalClockFontSizePreference() + || isDigitalClockFontSizePreference() + || isAlarmDigitalClockFontSizePreference()) { + return DEFAULT_DIGITAL_CLOCK_FONT_SIZE; + } else if (isAlarmTitleFontSizePreference()) { + return DEFAULT_ALARM_TITLE_FONT_SIZE_PREF; + } else if (isTimerShadowOffsetPreference() || isAlarmShadowOffsetPreference()) { + return DEFAULT_SHADOW_OFFSET; + } else if (isScreensaverBlurIntensityPreference() + || isTimerBlurIntensityPreference() + || isAlarmBlurIntensityPreference()) { + return DEFAULT_BLUR_INTENSITY; + } else if (isExternalAudioDeviceVolumePreference()) { + return DEFAULT_EXTERNAL_AUDIO_DEVICE_VOLUME; + } else if (isAnalogClockSizePreference() + || isScreensaverAnalogClockSizePreference() + || isAlarmAnalogClockSizePreference()) { + return DEFAULT_ANALOG_CLOCK_SIZE; + } else { + return DEFAULT_WIDGETS_FONT_SIZE; + } + } + + /** + * Saves the current value of the slider in SharedPreferences using the appropriate preference key. + */ + private void saveSliderValue(int value) { + mPrefs.edit().putInt(getKey(), value).apply(); + } + + /** + * Resets the slider value. + */ + private void resetPreference() { + mPrefs.edit().remove(getKey()).apply(); + } + + /** + * Update digital widgets if the Preference is linked to the widgets one. + */ + private void updateDigitalWidgets() { + if (!isScreensaverBrightnessPreference() + && !isScreensaverDigitalClockFontSizePreference() + && !isScreensaverAnalogClockSizePreference() + && !isScreensaverBlurIntensityPreference() + && !isShakeIntensityPreference() + && !isTimerShakeIntensityPreference() + && !isTimerShadowOffsetPreference() + && !isTimerBlurIntensityPreference() + && !isAlarmDigitalClockFontSizePreference() + && !isAlarmTitleFontSizePreference() + && !isAlarmShadowOffsetPreference() + && !isAlarmBlurIntensityPreference() + && !isExternalAudioDeviceVolumePreference() + && !isAnalogClockSizePreference() + && !isAlarmAnalogClockSizePreference() + && !isDigitalClockFontSizePreference()) { + + WidgetUtils.updateAllDigitalWidgets(mContext); + } + } + + /** + * @return {@code true} if the current preference is related to screensaver brightness. + * {@code false} otherwise. + */ + private boolean isScreensaverBrightnessPreference() { + return getKey().equals(KEY_SCREENSAVER_BRIGHTNESS); + } + + /** + * @return {@code true} if the current preference is related to the screensaver analog clock size. + * {@code false} otherwise. + */ + private boolean isScreensaverAnalogClockSizePreference() { + return getKey().equals(KEY_SCREENSAVER_ANALOG_CLOCK_SIZE); + } + + /** + * @return {@code true} if the current preference is related to the font size of the screensaver + * clock. {@code false} otherwise. + */ + private boolean isScreensaverDigitalClockFontSizePreference() { + return getKey().equals(KEY_SCREENSAVER_DIGITAL_CLOCK_FONT_SIZE); + } + + /** + * @return {@code true} if the current preference is related to blur intensity for screensaver. + * {@code false} otherwise. + */ + private boolean isScreensaverBlurIntensityPreference() { + return getKey().equals(KEY_SCREENSAVER_BLUR_INTENSITY); + } + + /** + * @return {@code true} if the current preference is related to corner radius of + * the digital widget background. {@code false} otherwise. + */ + private boolean isDigitalWidgetBackgroundCornerRadius() { + return getKey().equals(KEY_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS); + } + + /** + * @return {@code true} if the current preference is related to corner radius of + * the Next alarm widget background. {@code false} otherwise. + */ + private boolean isNextAlarmWidgetBackgroundCornerRadius() { + return getKey().equals(KEY_NEXT_ALARM_WIDGET_BACKGROUND_CORNER_RADIUS); + } + + /** + * @return {@code true} if the current preference is related to corner radius of + * the vertical widget background. {@code false} otherwise. + */ + private boolean isVerticalWidgetBackgroundCornerRadius() { + return getKey().equals(KEY_VERTICAL_WIDGET_BACKGROUND_CORNER_RADIUS); + } + + /** + * @return {@code true} if the current preference is related to corner radius of + * the Material You digital widget background. {@code false} otherwise. + */ + private boolean isMaterialYouDigitalWidgetBackgroundCornerRadius() { + return getKey().equals(KEY_MATERIAL_YOU_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS); + } + + /** + * @return {@code true} if the current preference is related to corner radius of + * the Material You Next alarm widget background. {@code false} otherwise. + */ + private boolean isMaterialYouNextAlarmWidgetBackgroundCornerRadius() { + return getKey().equals(KEY_MATERIAL_YOU_NEXT_ALARM_WIDGET_BACKGROUND_CORNER_RADIUS); + } + + /** + * @return {@code true} if the current preference is related to corner radius of + * the Material You vertical digital widget background. {@code false} otherwise. + */ + private boolean isMaterialYouVerticalWidgetBackgroundCornerRadius() { + return getKey().equals(KEY_MATERIAL_YOU_VERTICAL_DIGITAL_WIDGET_BACKGROUND_CORNER_RADIUS); + } + + /** + * @return {@code true} if the current preference is related to shake intensity. + * {@code false} otherwise. + */ + private boolean isShakeIntensityPreference() { + return getKey().equals(KEY_SHAKE_INTENSITY); + } + + /** + * @return {@code true} if the current preference is related to timer shake intensity. + * {@code false} otherwise. + */ + private boolean isTimerShakeIntensityPreference() { + return getKey().equals(KEY_TIMER_SHAKE_INTENSITY); + } + + /** + * @return {@code true} if the current preference is related to shadow offset for timers. + * {@code false} otherwise. + */ + private boolean isTimerShadowOffsetPreference() { + return getKey().equals(KEY_TIMER_SHADOW_OFFSET); + } + + /** + * @return {@code true} if the current preference is related to blur intensity for timers. + * {@code false} otherwise. + */ + private boolean isTimerBlurIntensityPreference() { + return getKey().equals(KEY_TIMER_BLUR_INTENSITY); + } + + /** + * @return {@code true} if the current preference is related to the analog clock size. + * {@code false} otherwise. + */ + private boolean isAnalogClockSizePreference() { + return getKey().equals(KEY_ANALOG_CLOCK_SIZE); + } + + /** + * @return {@code true} if the current preference is related to the font size of the clock. + * {@code false} otherwise. + */ + private boolean isDigitalClockFontSizePreference() { + return getKey().equals(KEY_DIGITAL_CLOCK_FONT_SIZE); + } + + /** + * @return {@code true} if the current preference is related to the alarm analog clock size. + * {@code false} otherwise. + */ + private boolean isAlarmAnalogClockSizePreference() { + return getKey().equals(KEY_ALARM_ANALOG_CLOCK_SIZE); + } + + /** + * @return {@code true} if the current preference is related to the font size of the alarm clock. + * {@code false} otherwise. + */ + private boolean isAlarmDigitalClockFontSizePreference() { + return getKey().equals(KEY_ALARM_DIGITAL_CLOCK_FONT_SIZE); + } + + /** + * @return {@code true} if the current preference is related to the alarm title font size. + * {@code false} otherwise. + */ + private boolean isAlarmTitleFontSizePreference() { + return getKey().equals(KEY_ALARM_TITLE_FONT_SIZE_PREF); + } + + /** + * @return {@code true} if the current preference is related to shadow offset for alarms. + * {@code false} otherwise. + */ + private boolean isAlarmShadowOffsetPreference() { + return getKey().equals(KEY_ALARM_SHADOW_OFFSET); + } + + /** + * @return {@code true} if the current preference is related to blur intensity for alarms. + * {@code false} otherwise. + */ + private boolean isAlarmBlurIntensityPreference() { + return getKey().equals(KEY_ALARM_BLUR_INTENSITY); + } + + /** + * @return {@code true} if the current preference is related to volume when + * an external audio device is connected. {@code false} otherwise. + */ + private boolean isExternalAudioDeviceVolumePreference() { + return getKey().equals(KEY_EXTERNAL_AUDIO_DEVICE_VOLUME); + } + + /** + * Plays ringtone preview if preference is "External audio device volume" or if there is an + * external audio device connected. + */ + private void startRingtonePreviewForExternalAudioDevices() { + if (!isExternalAudioDeviceVolumePreference() + || !RingtoneUtils.hasExternalAudioDeviceConnected(mContext, mPrefs)) { + return; + } + + if (mRingtoneStopRunnable != null) { + mRingtoneHandler.removeCallbacks(mRingtoneStopRunnable); + } + + // If we are not currently playing, start. + Uri ringtoneUri = DataModel.getDataModel().getAlarmRingtoneUriFromSettings(); + if (RingtoneUtils.isRandomRingtone(ringtoneUri)) { + ringtoneUri = RingtoneUtils.getRandomRingtoneUri(); + } else if (RingtoneUtils.isRandomCustomRingtone(ringtoneUri)) { + ringtoneUri = RingtoneUtils.getRandomCustomRingtoneUri(); + } + + RingtonePreviewKlaxon.start(mContext, mPrefs, ringtoneUri); + mIsPreviewPlaying = true; + + mRingtoneStopRunnable = this::stopRingtonePreviewForExternalAudioDevices; + + // Stop the preview after 5 seconds + mRingtoneHandler.postDelayed(mRingtoneStopRunnable, ALARM_PREVIEW_DURATION_MS); + } + + /** + * Stops playing the ringtone preview if it is currently playing. + */ + public void stopRingtonePreviewForExternalAudioDevices() { + if (!mIsPreviewPlaying) { + return; + } + + if (mRingtoneStopRunnable != null) { + mRingtoneHandler.removeCallbacks(mRingtoneStopRunnable); + } + + RingtonePreviewKlaxon.stop(mContext, mPrefs); + RingtonePreviewKlaxon.stopListeningToPreferences(); + + mIsPreviewPlaying = false; + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSwitchPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSwitchPreference.java new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/PermissionsManagementPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/PermissionsManagementPreference.java new file mode 100644 index 000000000..5e9309a1c --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/PermissionsManagementPreference.java @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.settings.PreferencesKeys.KEY_FULL_SCREEN_NOTIFICATION_PERMISSION; +import static com.best.deskclock.settings.PreferencesKeys.KEY_IGNORE_BATTERY_OPTIMIZATIONS; +import static com.best.deskclock.settings.PreferencesKeys.KEY_NOTIFICATION_PERMISSION; +import static com.best.deskclock.settings.PreferencesKeys.KEY_SHOW_LOCKSCREEN_PERMISSION; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.uicomponents.CustomDialog; +import com.best.deskclock.utils.PermissionUtils; +import com.best.deskclock.utils.ThemeUtils; + +public class PermissionsManagementPreference extends Preference { + + private Context mContext; + + private TextView mStatusState; + + public PermissionsManagementPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + if (holder.itemView.isInEditMode()) { + // Skip logic during Android Studio preview + return; + } + + mContext = getContext(); + SharedPreferences prefs = getDefaultSharedPreferences(mContext); + String fontPath = SettingsDAO.getGeneralFont(prefs); + Typeface regularTypeFace = ThemeUtils.loadFont(fontPath); + + super.onBindViewHolder(holder); + + final TextView requirementTitle = (TextView) holder.findViewById(R.id.requirement_title); + final TextView requirementAdvice = (TextView) holder.findViewById(R.id.requirement_advice); + final TextView statusTitle = (TextView) holder.findViewById(R.id.status_title); + mStatusState = (TextView) holder.findViewById(R.id.status_state); + + requirementTitle.setTypeface(regularTypeFace); + requirementAdvice.setTypeface(regularTypeFace); + statusTitle.setTypeface(regularTypeFace); + mStatusState.setTypeface(regularTypeFace); + + if (isShowLockScreenPermissionPreference()) { + statusTitle.setText(mContext.getString(R.string.permission_info_title)); + mStatusState.setVisibility(GONE); + } else { + statusTitle.setText(mContext.getString(R.string.permission_status_title)); + + if (isIgnoreBatteryOtimizationsPreference()) { + setStatusText(PermissionUtils.isIgnoringBatteryOptimizations(mContext)); + } else if (isNotificationPermissionPreference()) { + setStatusText(PermissionUtils.areNotificationsEnabled(mContext)); + } else if (isFullScreenNotificationPermissionPreference()) { + setStatusText(PermissionUtils.areFullScreenNotificationsEnabled(mContext)); + } + + mStatusState.setVisibility(VISIBLE); + } + + ImageButton detailsButton = (ImageButton) holder.findViewById(R.id.details_button); + detailsButton.setOnClickListener(v -> displayPermissionDetailsDialog()); + + } + + /** + * Sets the permission status text. + */ + public void setStatusText(boolean isGranted) { + if (isGranted) { + mStatusState.setText(mContext.getString(R.string.permission_granted)); + mStatusState.setTextColor(mContext.getColor(R.color.colorGranted)); + } else { + mStatusState.setText(mContext.getString(R.string.permission_denied)); + mStatusState.setTextColor(mContext.getColor(R.color.colorAlert)); + } + } + + /** + * @return {@code true} if the current preference is related to the battery optimizations; + * {@code false} otherwise. + */ + private boolean isIgnoreBatteryOtimizationsPreference() { + return getKey().equals(KEY_IGNORE_BATTERY_OPTIMIZATIONS); + } + + /** + * @return {@code true} if the current preference is related to the notification permission; + * {@code false} otherwise. + */ + private boolean isNotificationPermissionPreference() { + return getKey().equals(KEY_NOTIFICATION_PERMISSION); + } + + /** + * @return {@code true} if the current preference is related to the full screen notification + * permission; {@code false} otherwise. + */ + private boolean isFullScreenNotificationPermissionPreference() { + return getKey().equals(KEY_FULL_SCREEN_NOTIFICATION_PERMISSION); + } + + /** + * @return {@code true} if the current preference is related to the "Show lockscreen" permission + * for MIUI devices; {@code false} otherwise. + */ + private boolean isShowLockScreenPermissionPreference() { + return getKey().equals(KEY_SHOW_LOCKSCREEN_PERMISSION); + } + + /** + * Display dialog when user wants to read the permission details. + */ + private void displayPermissionDetailsDialog() { + int iconId; + int titleId; + int messageId; + + if (isIgnoreBatteryOtimizationsPreference()) { + iconId = R.drawable.ic_battery_settings; + titleId = R.string.ignore_battery_optimizations_dialog_title; + messageId = R.string.ignore_battery_optimizations_dialog_text; + } else if (isNotificationPermissionPreference()) { + iconId = R.drawable.ic_notifications; + titleId = R.string.notifications_dialog_title; + messageId = R.string.notifications_dialog_text; + } else if (isFullScreenNotificationPermissionPreference()) { + iconId = R.drawable.ic_fullscreen; + titleId = R.string.FSN_dialog_title; + messageId = R.string.FSN_dialog_text; + } else { + iconId = R.drawable.ic_screen_lock; + titleId = R.string.show_lockscreen_dialog_title; + messageId = R.string.show_lockscreen_dialog_text; + } + + CustomDialog.create( + mContext, + null, + AppCompatResources.getDrawable(mContext, iconId), + mContext.getString(titleId), + mContext.getString(messageId), + null, + mContext.getString(R.string.dialog_close), + null, + null, + null, + null, + null, + null, + CustomDialog.SoftInputMode.NONE + ).show(); + } + + public void refreshState() { + notifyChanged(); + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/PreferenceStyler.java b/app/src/main/java/com/best/deskclock/settings/custompreference/PreferenceStyler.java new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/TimerAddTimeButtonValuePreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/TimerAddTimeButtonValuePreference.java new file mode 100644 index 000000000..e81e499f7 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/TimerAddTimeButtonValuePreference.java @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_TIMER_ADD_TIME_BUTTON_VALUE; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import com.best.deskclock.R; + +public class TimerAddTimeButtonValuePreference extends DialogPreference { + + public TimerAddTimeButtonValuePreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(true); + } + + @Override + public CharSequence getSummary() { + int value = getAddTimeButtonValue(); + + int m = value / 60; + int s = value % 60; + + if (m > 0 && s > 0) { + String hoursString = getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + String secondString = getContext().getResources().getQuantityString(R.plurals.seconds, s, s); + return String.format("%s %s", hoursString, secondString); + } else if (m == 60) { + return getContext().getResources().getQuantityString(R.plurals.hours, 1, 1); + } else if (m > 0) { + return getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + } else { + return getContext().getResources().getQuantityString(R.plurals.seconds, s, s); + } + } + + public int getAddTimeButtonValue() { + return getPersistedInt(DEFAULT_TIMER_ADD_TIME_BUTTON_VALUE); + } + + public void setAddTimeButtonValue(int minutes) { + persistInt(minutes); + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/VibrationPatternPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/VibrationPatternPreference.java new file mode 100644 index 000000000..e54a3b50a --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/VibrationPatternPreference.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VIBRATION_PATTERN; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import com.best.deskclock.R; + +/** + * A custom {@link DialogPreference} that allows users to select a vibration pattern. + *

+ * This preference persists the selected pattern as a {@link String}, which can + * later be retrieved and used to generate a {@link android.os.VibrationEffect}. + */ +public class VibrationPatternPreference extends DialogPreference { + + /** + * Creates a new {@link VibrationPatternPreference} instance. + * + * @param context The application context. + * @param attrs The attribute set containing custom XML attributes. + */ + public VibrationPatternPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(true); + } + + @Override + public CharSequence getSummary() { + String patternKey = getPattern(); + + String[] entries = getContext().getResources().getStringArray(R.array.vibration_pattern_entries); + String[] values = getContext().getResources().getStringArray(R.array.vibration_pattern_values); + + for (int i = 0; i < values.length; i++) { + if (values[i].equals(patternKey)) { + return entries[i]; + } + } + + return super.getSummary(); + } + + /** + * Retrieves the currently persisted vibration pattern key. + * + * @return The key corresponding to the selected vibration pattern, + * or a default value if none has been set. + */ + public String getPattern() { + return getPersistedString(DEFAULT_VIBRATION_PATTERN); + } + + /** + * Persists the given vibration pattern key. + * + * @param patternKey The key of the vibration pattern to store. + */ + public void setPattern(String patternKey) { + persistString(patternKey); + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/VibrationStartDelayPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/VibrationStartDelayPreference.java new file mode 100644 index 000000000..2082baac8 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/VibrationStartDelayPreference.java @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VIBRATION_START_DELAY; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import com.best.deskclock.R; + +/** + * A custom {@link DialogPreference} that allows users to select a vibration start delay. + */ +public class VibrationStartDelayPreference extends DialogPreference { + + /** + * Constructs a new {@link VibrationStartDelayPreference} instance, used to manage user preferences + * related to vibration start delay. + * + * @param context The application context in which this preference is used. + * @param attrs The attribute set from XML that may include custom parameters. + */ + public VibrationStartDelayPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(true); + } + + /** + * Returns the currently persisted vibration start delay in seconds. + * + * @return The vibration start delay in seconds, or none if no value has been previously persisted. + */ + public int getVibrationStartDelay() { + return getPersistedInt(DEFAULT_VIBRATION_START_DELAY); + } + + /** + * Persists the vibration start delay in seconds. + * + * @param seconds The vibration start delay to be stored, in seconds. + */ + public void setVibrationStartDelay(int seconds) { + persistInt(seconds); + } + + @Override + public CharSequence getSummary() { + int seconds = getVibrationStartDelay(); + + if (seconds == DEFAULT_VIBRATION_START_DELAY) { + return getContext().getString(R.string.vibration_start_delay_none); + } else { + int minutes = seconds / 60; + return getContext().getResources().getQuantityString(R.plurals.minutes, minutes, minutes); + } + } + +} diff --git a/app/src/main/java/com/best/deskclock/settings/custompreference/VolumeCrescendoDurationPreference.java b/app/src/main/java/com/best/deskclock/settings/custompreference/VolumeCrescendoDurationPreference.java new file mode 100644 index 000000000..3a3d524d6 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/VolumeCrescendoDurationPreference.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.settings.custompreference; + +import static com.best.deskclock.settings.PreferencesDefaultValues.DEFAULT_VOLUME_CRESCENDO_DURATION; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import com.best.deskclock.R; + +public class VolumeCrescendoDurationPreference extends DialogPreference { + + /** + * Constructs a new VolumeCrescendoDurationPreference instance, used to manage user preferences + * related to volume crescendo duration. + * + * @param context The application context in which this preference is used. + * @param attrs The attribute set from XML that may include custom parameters. + */ + public VolumeCrescendoDurationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(true); + } + + @Override + public CharSequence getSummary() { + int seconds = getVolumeCrescendoDuration(); + + if (seconds == DEFAULT_VOLUME_CRESCENDO_DURATION) { + return getContext().getString(R.string.label_off); + } + + int m = seconds / 60; + int s = seconds % 60; + + if (m > 0 && s > 0) { + String minutesString = getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + String secondsString = getContext().getResources().getQuantityString(R.plurals.seconds, s, s); + return String.format("%s %s", minutesString, secondsString); + } else if (m > 0) { + return getContext().getResources().getQuantityString(R.plurals.minutes, m, m); + } else { + return getContext().getResources().getQuantityString(R.plurals.seconds, s, s); + } + } + + /** + * Returns the currently persisted volume crescendo duration in seconds. + * + * @return The crescendo duration in seconds, or none if no value has been previously persisted. + */ + public int getVolumeCrescendoDuration() { + return getPersistedInt(DEFAULT_VOLUME_CRESCENDO_DURATION); + } + + /** + * Persists the volume crescendo duration in seconds. + * + * @param seconds The volume crescendo duration to be stored, in seconds. + */ + public void setVolumeCrescendoDuration(int seconds) { + persistInt(seconds); + } + +} diff --git a/app/src/main/java/com/best/deskclock/uicomponents/CollapsingToolbarBaseActivity.java b/app/src/main/java/com/best/deskclock/uicomponents/CollapsingToolbarBaseActivity.java new file mode 100644 index 000000000..20ab7b3bb --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/CollapsingToolbarBaseActivity.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ + +package com.best.deskclock.uicomponents; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.settings.PreferencesDefaultValues.AMOLED_DARK_MODE; + +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.graphics.Insets; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.best.deskclock.BaseActivity; +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; + +import com.best.deskclock.settings.SettingsActivity; +import com.best.deskclock.utils.InsetsUtils; +import com.best.deskclock.utils.SdkUtils; +import com.best.deskclock.utils.ThemeUtils; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.CollapsingToolbarLayout; + +/** + * A base Activity that has a collapsing toolbar layout is used for the activities intending to + * enable the collapsing toolbar function. + */ +public abstract class CollapsingToolbarBaseActivity extends BaseActivity { + + @Nullable + private CollapsingToolbarLayout mCollapsingToolbarLayout; + + @Nullable + protected AppBarLayout mAppBarLayout; + + protected CoordinatorLayout mCoordinatorLayout; + + /** + * This method should be implemented by subclasses of CollapsingToolbarBaseActivity + * to provide the title for the activity's collapsing toolbar. + *

+ * The title returned by this method will be displayed in the collapsing toolbar layout + * and will be correctly translated when changing the language in settings. + * + * @return The title of the activity to be displayed in the collapsing toolbar. + */ + protected abstract String getActivityTitle(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + final SharedPreferences prefs = getDefaultSharedPreferences(this); + boolean isFadeTransitionEnabled = SettingsDAO.isFadeTransitionsEnabled(prefs); + + if (isFadeTransitionEnabled) { + if (SdkUtils.isAtLeastAndroid14()) { + overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, R.anim.fade_in, R.anim.fade_out); + } else { + overridePendingTransition(R.anim.fade_in, R.anim.fade_out); + } + } else { + if (SdkUtils.isAtLeastAndroid14()) { + overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, + R.anim.activity_slide_from_right, R.anim.activity_slide_to_left); + } else { + overridePendingTransition(R.anim.activity_slide_from_right, R.anim.activity_slide_to_left); + } + } + + super.onCreate(savedInstanceState); + + // To manually manage insets + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + + ThemeUtils.allowDisplayCutout(getWindow()); + + super.setContentView(R.layout.collapsing_toolbar_base_layout); + + mCoordinatorLayout = findViewById(R.id.coordinator_layout); + + final String getDarkMode = SettingsDAO.getDarkMode(prefs); + mCollapsingToolbarLayout = findViewById(R.id.collapsing_toolbar); + if (mCollapsingToolbarLayout == null) { + return; + } + + final Typeface typeface = ThemeUtils.loadFont(SettingsDAO.getGeneralFont(prefs)); + mCollapsingToolbarLayout.setExpandedTitleTypeface(typeface); + mCollapsingToolbarLayout.setCollapsedTitleTypeface(typeface); + + if (ThemeUtils.isNight(getResources()) && getDarkMode.equals(AMOLED_DARK_MODE)) { + mCollapsingToolbarLayout.setBackgroundColor(getColor(android.R.color.black)); + mCollapsingToolbarLayout.setContentScrimColor(getColor(android.R.color.black)); + } + + mAppBarLayout = findViewById(R.id.app_bar); + disableCollapsingToolbarLayoutScrollingBehavior(); + + final Toolbar toolbar = findViewById(R.id.action_bar); + setSupportActionBar(toolbar); + + applyWindowInsets(); + + // Exclude SettingsActivity as this is handled in SettingsFragment. + if (!(this instanceof SettingsActivity)) { + getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + finish(); + if (isFadeTransitionEnabled) { + if (SdkUtils.isAtLeastAndroid14()) { + overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, + R.anim.fade_in, R.anim.fade_out); + } else { + overridePendingTransition(R.anim.fade_in, R.anim.fade_out); + } + } else { + if (SdkUtils.isAtLeastAndroid14()) { + overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, + R.anim.activity_slide_from_left, R.anim.activity_slide_to_right); + } else { + overridePendingTransition( + R.anim.activity_slide_from_left, R.anim.activity_slide_to_right); + } + } + } + }); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (mCollapsingToolbarLayout != null) { + mCollapsingToolbarLayout.setTitle(getActivityTitle()); + } + } + + @Override + public void setContentView(int layoutResID) { + final ViewGroup parent = findViewById(R.id.content_frame); + if (parent != null) { + parent.removeAllViews(); + } + LayoutInflater.from(this).inflate(layoutResID, parent); + } + + @Override + public void setContentView(View view) { + final ViewGroup parent = findViewById(R.id.content_frame); + if (parent != null) { + parent.addView(view); + } + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + final ViewGroup parent = findViewById(R.id.content_frame); + if (parent != null) { + parent.addView(view, params); + } + } + + @Override + public void setTitle(CharSequence title) { + if (mCollapsingToolbarLayout != null) { + mCollapsingToolbarLayout.setTitle(title); + } else { + super.setTitle(title); + } + } + + @Override + public void setTitle(int titleId) { + if (mCollapsingToolbarLayout != null) { + mCollapsingToolbarLayout.setTitle(getText(titleId)); + } else { + super.setTitle(titleId); + } + } + + @Override + public boolean onNavigateUp() { + if (!super.onNavigateUp()) { + finishAfterTransition(); + } + return true; + } + + private void disableCollapsingToolbarLayoutScrollingBehavior() { + if (mAppBarLayout == null) { + return; + } + final CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams(); + final AppBarLayout.Behavior behavior = new AppBarLayout.Behavior(); + behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() { + @Override + public boolean canDrag(@NonNull AppBarLayout appBarLayout) { + return false; + } + }); + params.setBehavior(behavior); + } + + + /** + * This method adjusts the spacing of the Toolbar and content to take into account system insets, + * so that they are not obscured by system elements (status bar, navigation bar or cutout). + */ + private void applyWindowInsets() { + InsetsUtils.doOnApplyWindowInsets(mAppBarLayout, (v, insets) -> { + // Get the system bar and notch insets + Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars() | + WindowInsetsCompat.Type.displayCutout()); + + v.setPadding(bars.left, bars.top, bars.right, 0); + }); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/best/deskclock/uicomponents/CustomDialog.java b/app/src/main/java/com/best/deskclock/uicomponents/CustomDialog.java new file mode 100644 index 000000000..526edfa41 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/CustomDialog.java @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.uicomponents; + +import static androidx.core.util.TypedValueCompat.dpToPx; +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.widget.NestedScrollView; + +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.ThemeUtils; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public class CustomDialog { + + public static AlertDialog create( + @NonNull Context context, + @Nullable @StyleRes Integer styleRes, + @Nullable Drawable icon, + @Nullable CharSequence title, + @Nullable CharSequence message, + @Nullable View customView, + @Nullable CharSequence positiveText, + @Nullable DialogInterface.OnClickListener positiveListener, + @Nullable CharSequence negativeText, + @Nullable DialogInterface.OnClickListener negativeListener, + @Nullable CharSequence neutralText, + @Nullable DialogInterface.OnClickListener neutralListener, + @Nullable OnDialogReady onDialogReady, + @NonNull SoftInputMode softInputMode + ) { + + SharedPreferences prefs = getDefaultSharedPreferences(context); + Typeface typeface = ThemeUtils.loadFont(SettingsDAO.getGeneralFont(prefs)); + + LayoutInflater inflater = LayoutInflater.from(context); + + // Title + @SuppressLint("InflateParams") + View titleView = inflater.inflate(R.layout.dialog_title_custom, null); + TextView titleText = titleView.findViewById(R.id.dialog_title); + + if (icon != null) { + titleText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); + titleText.setCompoundDrawablePadding((int) dpToPx(18, context.getResources().getDisplayMetrics())); + } + + if (title != null) { + titleText.setText(title); + titleText.setTypeface(typeface); + } + + // Dialog view + View dialogContent; + + if (message != null) { + // Message + @SuppressLint("InflateParams") + View messageView = inflater.inflate(R.layout.dialog_message_custom, null); + TextView messageText = messageView.findViewById(R.id.dialog_message); + messageText.setText(message); + messageText.setTypeface(typeface); + + dialogContent = messageView; + } else { + dialogContent = customView; + } + + // Builder + MaterialAlertDialogBuilder builder = (styleRes != null) + ? new MaterialAlertDialogBuilder(context, styleRes) + : new MaterialAlertDialogBuilder(context); + + builder + .setCustomTitle(titleView) + .setView(dialogContent); + + if (positiveText != null) { + builder.setPositiveButton(positiveText, positiveListener); + } + if (negativeText != null) { + builder.setNegativeButton(negativeText, negativeListener); + } + if (neutralText != null) { + builder.setNeutralButton(neutralText, neutralListener); + } + + AlertDialog dialog = builder.create(); + + // Apply typeface to buttons + dialog.setOnShowListener(d -> { + if (dialogContent != null && dialogContent.findViewById(R.id.scrollView) != null) { + configureScrollView(dialogContent); + } + + Button positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + Button negative = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); + Button neutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + + if (positive != null) { + positive.setTypeface(typeface); + } + if (negative != null) { + negative.setTypeface(typeface); + } + if (neutral != null) { + neutral.setTypeface(typeface); + } + + if (onDialogReady != null) { + onDialogReady.onReady(dialog); + } + }); + + // Soft input mode + if (softInputMode == SoftInputMode.SHOW_KEYBOARD) { + Window window = dialog.getWindow(); + if (window != null) { + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN | + WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE + ); + } + } + + return dialog; + } + + public static AlertDialog createSimpleDialog( + Context context, + @DrawableRes int iconRes, + @StringRes int titleRes, + CharSequence message, + @StringRes int positiveTextRes, + DialogInterface.OnClickListener positiveListener + ) { + return create( + context, + null, + AppCompatResources.getDrawable(context, iconRes), + context.getString(titleRes), + message, + null, + context.getString(positiveTextRes), + positiveListener, + context.getString(android.R.string.cancel), + null, + null, + null, + null, + SoftInputMode.NONE + ); + } + + private static void configureScrollView(View dialogView) { + NestedScrollView scrollView = dialogView.findViewById(R.id.scrollView); + + boolean scrollable = scrollView.canScrollVertically(1) + || scrollView.canScrollVertically(-1); + + if (scrollable) { + scrollView.setScrollIndicators(View.SCROLL_INDICATOR_BOTTOM); + } + + scrollView.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) ( + v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + + scrollView.setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); + + boolean atTop = !scrollView.canScrollVertically(-1); + boolean atBottom = !scrollView.canScrollVertically(1); + + if (atTop) { + scrollView.setScrollIndicators(View.SCROLL_INDICATOR_BOTTOM); + } + if (atBottom) { + scrollView.setScrollIndicators(View.SCROLL_INDICATOR_TOP); + } + }); + } + + public interface OnDialogReady { + void onReady(AlertDialog dialog); + } + + public enum SoftInputMode { + NONE, + SHOW_KEYBOARD + } + +} diff --git a/app/src/main/java/com/best/deskclock/uicomponents/CustomTooltip.java b/app/src/main/java/com/best/deskclock/uicomponents/CustomTooltip.java new file mode 100644 index 000000000..832593fd6 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/CustomTooltip.java @@ -0,0 +1,120 @@ +package com.best.deskclock.uicomponents; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.ThemeUtils; + +/** + * Utility class for displaying custom tooltips anchored to views. + * + *

This class replaces the default system tooltips with fully customizable + * popup tooltips that can appear above or below a target view. The tooltip + * layout, colors, and typeface can be styled freely, allowing consistent + * visual integration with the application's theme.

+ * + *

Tooltips are shown for a short duration and automatically dismissed.

+ */ +public class CustomTooltip { + + private static final int TOOLTIP_DURATION = 2000; + + /** + * Displays a custom tooltip above the given anchor view. + * + *

This is a convenience method that delegates to the internal + * {@link #show(View, String, Position)} method using the ABOVE position.

+ * + * @param anchor the view above which the tooltip should appear + * @param text the text to display inside the tooltip + */ + public static void showAbove(View anchor, String text) { + show(anchor, text, Position.ABOVE); + } + + /** + * Displays a custom tooltip below the given anchor view. + * + *

This is a convenience method that delegates to the internal + * {@link #show(View, String, Position)} method using the BELOW position.

+ * + * @param anchor the view under which the tooltip should appear + * @param text the text to display inside the tooltip + */ + public static void showBelow(View anchor, String text) { + show(anchor, text, Position.BELOW); + } + + /** + * Internal method that creates and displays a custom tooltip anchored to a view. + * + *

The tooltip is horizontally centered relative to the anchor and positioned + * either above or below it depending on the specified {@link Position}.

+ * + * @param anchor the view used as the reference point for positioning + * @param text the text to display inside the tooltip + * @param position whether the tooltip should appear above or below the anchor + */ + private static void show(View anchor, String text, Position position) { + Context context = anchor.getContext(); + SharedPreferences prefs = getDefaultSharedPreferences(context); + Typeface typeface = ThemeUtils.loadFont(SettingsDAO.getGeneralFont(prefs)); + + // Inflate layout + @SuppressLint("InflateParams") + View tooltipView = LayoutInflater.from(context).inflate(R.layout.custom_tooltip, null); + TextView tooltipText = tooltipView.findViewById(R.id.tooltip_text); + tooltipText.setText(text); + tooltipText.setTypeface(typeface); + + // Create a popup window + PopupWindow popup = new PopupWindow( + tooltipView, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + false + ); + + popup.setBackgroundDrawable(ThemeUtils.pillBackground( + context, com.google.android.material.R.attr.colorSecondary) + ); + popup.setOutsideTouchable(true); + + // Position + int[] location = new int[2]; + anchor.getLocationOnScreen(location); + + tooltipView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + int tooltipWidth = tooltipView.getMeasuredWidth(); + int tooltipHeight = tooltipView.getMeasuredHeight(); + int anchorWidth = anchor.getWidth(); + int anchorHeight = anchor.getHeight(); + + // Horizontal centering + int x = location[0] + (anchorWidth / 2) - (tooltipWidth / 2); + + // Vertical position + int y = position == Position.BELOW + ? location[1] + anchorHeight + : location[1] - tooltipHeight; + + popup.showAtLocation(anchor, Gravity.NO_GRAVITY, x, y); + + tooltipView.postDelayed(popup::dismiss, TOOLTIP_DURATION); + } + + private enum Position {ABOVE, BELOW} + +} diff --git a/app/src/main/java/com/best/deskclock/uicomponents/TextTime.java b/app/src/main/java/com/best/deskclock/uicomponents/TextTime.java new file mode 100644 index 000000000..ae0ec7fe5 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/TextTime.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ + +package com.best.deskclock.uicomponents; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static java.util.Calendar.HOUR_OF_DAY; +import static java.util.Calendar.MINUTE; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.util.AttributeSet; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.widget.AppCompatTextView; + +import com.best.deskclock.data.DataModel; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.ClockUtils; +import com.best.deskclock.utils.ThemeUtils; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Based on {@link android.widget.TextClock}, This widget displays a constant time of day using + * format specifiers. {@link android.widget.TextClock} doesn't support a non-ticking clock. + */ +public class TextTime extends AppCompatTextView { + + @VisibleForTesting() + static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a"; + @VisibleForTesting() + static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm"; + /** + * UTC does not have DST rules and will not alter the {@link #mHour} and {@link #mMinute}. + */ + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + private CharSequence mFormat12; + private CharSequence mFormat24; + private CharSequence mFormat; + private Context mContext; + private SharedPreferences mPrefs; + + private boolean mAttached; + + private int mHour; + private int mMinute; + + private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + + @Override + public void onChange(boolean selfChange) { + chooseFormat(); + updateTime(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + chooseFormat(); + updateTime(); + } + }; + + public TextTime(Context context) { + this(context, null); + } + + public TextTime(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TextTime(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + if (isInEditMode()) { + return; + } + + mContext = context; + mPrefs = getDefaultSharedPreferences(context); + setTimeFormat(0.45f, false); + chooseFormat(); + } + + public void setFormat12Hour(CharSequence format) { + mFormat12 = format; + + chooseFormat(); + updateTime(); + } + + public void setFormat24Hour(CharSequence format) { + mFormat24 = format; + + chooseFormat(); + updateTime(); + } + + private void chooseFormat() { + final boolean format24Requested = DataModel.getDataModel().is24HourFormat(); + if (format24Requested) { + mFormat = mFormat24 == null ? DEFAULT_FORMAT_24_HOUR : mFormat24; + } else { + mFormat = mFormat12 == null ? DEFAULT_FORMAT_12_HOUR : mFormat12; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (isInEditMode()) { + return; + } + + if (!mAttached) { + mAttached = true; + registerObserver(); + updateTime(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mAttached) { + unregisterObserver(); + mAttached = false; + } + } + + private void registerObserver() { + final ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver); + } + + private void unregisterObserver() { + final ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(mFormatChangeObserver); + } + + public void setTime(int hour, int minute) { + if (isInEditMode()) { + return; + } + + mHour = hour; + mMinute = minute; + updateTime(); + } + + public void setTypeface(boolean isAlarmEnabled) { + String fontPath = SettingsDAO.getAlarmFont(mPrefs); + Typeface typeface = isAlarmEnabled + ? ThemeUtils.boldTypeface(fontPath) + : ThemeUtils.loadFont(fontPath); + + setTypeface(typeface); + } + + public void setTimeFormat(float amPmRatio, boolean includeSeconds) { + CharSequence format12 = ClockUtils.get12ModeFormat(mContext, amPmRatio, includeSeconds, + true, false, false); + setFormat12Hour(format12); + + CharSequence format24 = ClockUtils.get24ModeFormat(includeSeconds, false); + setFormat24Hour(format24); + } + + private void updateTime() { + if (isInEditMode()) { + return; + } + + // Format the time relative to UTC to ensure hour and minute are not adjusted for DST. + final Calendar calendar = DataModel.getDataModel().getCalendar(); + calendar.setTimeZone(UTC); + calendar.set(HOUR_OF_DAY, mHour); + calendar.set(MINUTE, mMinute); + final CharSequence text = DateFormat.format(mFormat, calendar); + setText(text); + // Strip away the spans from text so talkback is not confused + setContentDescription(text.toString()); + } +} diff --git a/app/src/main/java/com/best/deskclock/uicomponents/toast/CustomToast.java b/app/src/main/java/com/best/deskclock/uicomponents/toast/CustomToast.java new file mode 100644 index 000000000..45c20a873 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/toast/CustomToast.java @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.uicomponents.toast; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.StringRes; + +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.ThemeUtils; + +/** + * Utility class for displaying custom styled toasts using the application's + * selected accent color and font preferences. + * + *

This class automatically applies the correct themed context so that the toast + * respects the user's accent color, night mode settings, and custom font.

+ */ +public class CustomToast { + + /** + * Inflates and configures the custom toast layout. + * + *

This method applies the themed context, sets the message text, + * applies the appropriate background and font, and returns the fully + * prepared view ready to be used inside a Toast.

+ * + * @param context the base context used to resolve theme and resources + * @param message the text to display inside the toast + * @return a fully configured toast layout view + */ + private static View createLayout(Context context, String message) { + SharedPreferences prefs = getDefaultSharedPreferences(context); + Context themedContext = ThemeUtils.getThemedContext(context, prefs); + + @SuppressLint("InflateParams") + View layout = LayoutInflater.from(themedContext).inflate(R.layout.custom_toast, null); + + TextView text = layout.findViewById(R.id.toast_text); + text.setBackground(ThemeUtils.pillBackground(themedContext, com.google.android.material.R.attr.colorSecondary)); + text.setText(message); + + Typeface typeface = ThemeUtils.loadFont(SettingsDAO.getGeneralFont(prefs)); + if (typeface != null) { + text.setTypeface(typeface); + } + + return layout; + } + + /** + * Creates a Toast instance using the given layout and duration. + * + *

The toast is built using the themed context to ensure proper + * styling across light and dark modes. The returned Toast is not + * shown automatically and must be displayed by the caller.

+ * + *

Note: even if {@link Toast#setView(View)} is obsolete, its use is not problematic; + * indeed, apps targeting API level 30 or higher that are in the background will + * not have custom toast views displayed.

+ * + * @param context the base context used to apply theming + * @param layout the custom layout to display inside the toast + * @param duration the toast duration (e.g., Toast.LENGTH_SHORT or Toast.LENGTH_LONG) + * @return a configured Toast instance ready to be shown + */ + private static Toast buildToast(Context context, View layout, int duration) { + SharedPreferences prefs = getDefaultSharedPreferences(context); + Context themedContext = ThemeUtils.getThemedContext(context, prefs); + + Toast toast = new Toast(themedContext); + toast.setDuration(duration); + toast.setView(layout); + + return toast; + } + + /** + * Displays a short custom toast using a string resource. + */ + public static void show(Context context, @StringRes int messageRes) { + show(context, context.getString(messageRes)); + } + + /** + * Displays a short custom toast with the given text. + */ + public static void show(Context context, String message) { + View layout = createLayout(context, message); + buildToast(context, layout, Toast.LENGTH_SHORT).show(); + } + + /** + * Displays a long-duration custom toast with the given text. + */ + public static void showLong(Context context, String message) { + View layout = createLayout(context, message); + buildToast(context, layout, Toast.LENGTH_LONG).show(); + } + + /** + * Displays a long-duration custom toast while ensuring that any previously + * shown toast is cancelled before displaying the new one. + * + *

This is useful in situations where multiple toasts may be triggered in + * quick succession, such as alarm snooze actions.

+ */ + public static void showLongWithManager(Context context, String message) { + View layout = createLayout(context, message); + Toast toast = buildToast(context, layout, Toast.LENGTH_LONG); + + ToastManager.setToast(toast); + toast.show(); + } + +} diff --git a/app/src/main/java/com/best/deskclock/uicomponents/toast/SnackbarManager.java b/app/src/main/java/com/best/deskclock/uicomponents/toast/SnackbarManager.java new file mode 100644 index 000000000..e1b7a1805 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/toast/SnackbarManager.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ + +package com.best.deskclock.uicomponents.toast; + +import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; + +import android.content.SharedPreferences; +import android.graphics.Typeface; + +import com.best.deskclock.R; +import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.utils.ThemeUtils; + +import com.google.android.material.snackbar.Snackbar; + +import java.lang.ref.WeakReference; + +/** + * Manages visibility of Snackbar and allow preemptive dismiss of current displayed Snackbar. + */ +public final class SnackbarManager { + + private static WeakReference sSnackbar = null; + + public static void show(Snackbar snackbar) { + sSnackbar = new WeakReference<>(snackbar); + if (ThemeUtils.isTablet() || (!ThemeUtils.isTablet() && ThemeUtils.isPortrait())) { + snackbar.setAnchorView(R.id.button_layout); + } + + SharedPreferences prefs = getDefaultSharedPreferences(snackbar.getContext()); + Typeface typeface = ThemeUtils.loadFont(SettingsDAO.getGeneralFont(prefs)); + ThemeUtils.applyTypeface(snackbar.getView(), typeface); + + snackbar.show(); + } + + public static void dismiss() { + final Snackbar snackbar = sSnackbar == null ? null : sSnackbar.get(); + if (snackbar != null) { + snackbar.dismiss(); + sSnackbar = null; + } + } + +} diff --git a/app/src/main/java/com/best/deskclock/uicomponents/toast/ToastManager.java b/app/src/main/java/com/best/deskclock/uicomponents/toast/ToastManager.java new file mode 100644 index 000000000..d506bf895 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/uicomponents/toast/ToastManager.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ + +package com.best.deskclock.uicomponents.toast; + +import android.widget.Toast; + +public final class ToastManager { + + private static Toast sToast = null; + + private ToastManager() { + } + + public static void setToast(Toast toast) { + if (sToast != null) + sToast.cancel(); + sToast = toast; + } + + public static void cancelToast() { + if (sToast != null) + sToast.cancel(); + sToast = null; + } + +} diff --git a/app/src/main/java/com/best/deskclock/utils/BackupAndRestoreUtils.java b/app/src/main/java/com/best/deskclock/utils/BackupAndRestoreUtils.java index 0b628097b..235300a0e 100644 --- a/app/src/main/java/com/best/deskclock/utils/BackupAndRestoreUtils.java +++ b/app/src/main/java/com/best/deskclock/utils/BackupAndRestoreUtils.java @@ -365,7 +365,7 @@ private static void restoreAlarm(Context context, ContentResolver contentResolve Alarm.addAlarm(contentResolver, restoredAlarm); if (restoredAlarm.enabled) { - AlarmInstance alarmInstance = restoredAlarm.createInstanceAfter(Calendar.getInstance()); + AlarmInstance alarmInstance = restoredAlarm.createInstanceAfter(context, Calendar.getInstance()); AlarmInstance.addInstance(contentResolver, alarmInstance); AlarmStateManager.registerInstance(context, alarmInstance, true); LogUtils.i("BackupAndRestoreUtils scheduled alarm instance: %s", alarmInstance); diff --git a/app/src/main/res/drawable/ic_earthquake.xml b/app/src/main/res/drawable/ic_earthquake.xml new file mode 100644 index 000000000..0bf51f969 --- /dev/null +++ b/app/src/main/res/drawable/ic_earthquake.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_repeat.xml b/app/src/main/res/drawable/ic_repeat.xml new file mode 100644 index 000000000..dc1f1f0ca --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout/alarm_time_expanded.xml b/app/src/main/res/layout/alarm_time_expanded.xml index 9423cd939..3cc77931f 100644 --- a/app/src/main/res/layout/alarm_time_expanded.xml +++ b/app/src/main/res/layout/alarm_time_expanded.xml @@ -15,7 +15,7 @@ android:layout_marginVertical="4dp" android:elevation="0dp" android:importantForAccessibility="no" - tools:background="@drawable/card_background_for_preview"> + tools:background="@drawable/bg_card_for_preview"> + app:layout_constraintEnd_toStartOf="@+id/arrow" + tools:ignore="TextContrastCheck" /> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="TouchTargetSizeCheck" /> - - + tools:text="Mon, Tue, Wed" /> + app:layout_constraintBottom_toBottomOf="@id/days_of_week" + tools:ignore="TouchTargetSizeCheck" /> - - + app:layout_constraintTop_toBottomOf="@id/repeat_days" /> @@ -164,9 +158,10 @@ android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@null" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/empty_view" + app:layout_constraintTop_toBottomOf="@id/repeat_days" app:layout_constraintBottom_toBottomOf="@+id/schedule_alarm" - tools:visibility="visible" /> + tools:visibility="visible" + tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck"/> + + + + - + + + + + + app:layout_constraintTop_toBottomOf="@id/missed_alarm_repeat_limit_title" /> + tools:text="@string/alarm_alert_dismiss_text" + tools:textStyle="bold" /> + app:layout_constraintTop_toBottomOf="@id/preemptive_dismiss_button" + tools:textStyle="bold" /> + app:layout_constraintTop_toTopOf="@id/delete" + tools:textStyle="bold" /> \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f7356ed20..3079f1dde 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -26,11 +26,6 @@ "错过的闹钟" %1$s - %2$s "已暂停" - 工作日设置 - 法定工作日 - 大小周大周 - 大小周小周 - 单休 %d 分钟 @@ -88,8 +83,7 @@ "暂停时长" 逐步增大音量 之后静音 - 从不 - 1 分钟 + 从不 关闭 一周的第一天 星期六 @@ -261,6 +255,8 @@ "斐济" "汤加" "雅加达" + "利雅得" + "雷克雅未克" "新闹钟" 创建新闹钟 @@ -283,6 +279,7 @@ %d 个定时器 "展开闹钟" 收起闹钟 + 撤销 "闹钟已删除" / %s %1$s @@ -322,7 +319,6 @@ 秒表 定时器 铃声结束 - 主时钟自定义颜色 必要:  强烈推荐 电池优化 @@ -349,7 +345,6 @@ 指针时钟 Material You 一个或多个基本权限被拒绝。不授予它们可能会导致故障。 显示某些应用程序元素(闹钟、城市、定时器按钮等)的背景 - • 将闹钟设置为特定日期;\n\n• 翻转和摇晃操作可关闭/推迟闹钟;\n\n• 使用电源按钮或音量按钮关闭/推迟闹钟;\n\n• 仅适用于某些骁龙处理器设备,设备关机时会触发闹钟;\n\t\t遗憾的是,尽管存在“com.qualcomm.qti.poweroffalarm”系统应用,但此功能可能无法在某些设备上运行。\n\n• 滑动删除闹钟;\n\n• 重复闹钟;\n\n• 自定义闹钟标题;\n\n• 可自定义铃声;\n\n• 浅色、深色或系统主题;\n\n• 深色主题的 AMOLED 模式;\n\n• 数字或指针时钟样式;\n\n• 旅行时显示家中时间;\n\n• 显示世界各地许多城市的时间;\n\n• 包括定时器和秒表;\n\n• 可与联系人分享秒表;\n\n• 可自定义的屏幕保护程序;\n\n• 现代微件;\n\n• 可自定义的微件;\n\n• 支持快捷设置中的图块(适用于 Android 7+);\n\n• 备份和还原应用程序数据(自定义铃声除外);\n\n• Material 设计;\n\n• 适用于 Android 12+ 的动态配色; ► 浅色、深色或系统主题;<br> <br> ► 可自定义的屏幕保护程序;<br> <br> ► 可自定义的微件;<br> <br> ► 支持快捷设置中的图块(适用于 Android 7+);<br> <br> ► 备份和还原应用程序数据(自定义铃声除外);<br> <br> ► Material 设计和动态配色;<br> <br> ► 将闹钟设置为特定日期;<br> <br> ► 滑动删除闹钟;<br> <br> ► 重复闹钟;<br> <br> ► 翻转和摇晃操作可关闭/推迟闹钟;<br> <br> ► 使用电源按钮或音量按钮关闭/推迟闹钟;<br> <br> ► %s 通知提醒 在 GitHub 查看 @@ -362,9 +357,9 @@ …还有更多! 日期以斜体显示 背景颜色 - 在微件上显示城市 - 城市时钟自定义颜色 - 城市名称自定义颜色 + 在微件上显示城市 + 城市时钟自定义颜色 + 城市名称自定义颜色 显示应用程序正常工作所需的权限 滑动以关闭或暂停闹钟 电源按钮 @@ -375,8 +370,6 @@ 主要特点 开放源代码许可 GNU General Public License v3.0 - 贡献者 - 致谢 关闭 向左或向右滑动以关闭 设置默认铃声、音量和定时器排序 @@ -386,14 +379,14 @@ 打开 Android 屏幕保护程序主设置 动态时钟颜色 时钟颜色 - 日期颜色 + 日期颜色 亮度 数字时钟以粗体显示 数字时钟以斜体显示 日期以粗体显示 预览 立即启动屏幕保护程序 - 下次闹钟颜色 + 下次闹钟颜色 下次闹钟以粗体显示 下次闹钟以斜体显示 版本 %s @@ -401,21 +394,18 @@ 退出 是否确定要离开应用程序? 显示背景 - 使用主时钟的默认颜色 - 使用日期的默认颜色 - 日期自定义颜色 - 使用城市时钟的默认颜色 - 使用城市名称的默认颜色 - 主时钟的最大字体大小 + 使用主时钟的默认颜色 + 使用日期的默认颜色 + 使用城市时钟的默认颜色 + 使用城市名称的默认颜色 + 主时钟的最大字体大小 必须禁用“在微件上显示城市”才能访问此设置 权限管理 • <b>全屏通知;</b> <br> <br> - 下次闹钟使用默认颜色 - 下次闹钟自定义颜色 + 下次闹钟使用默认颜色 闹钟标签 点击此处显示权限管理页面。 样式 - 样式和颜色 字体 杂项 铃声 @@ -432,46 +422,40 @@ 显示某些应用程序元素(闹钟、城市、定时器按钮等)的背景边框 忽略电池优化 已拒绝 - 关闭 是否确定要撤销此权限? 状态:  已授予 - 自定义闹钟显示 “暂停”按钮的颜色 “关闭”按钮的颜色 闹钟按钮颜色 定时器结束时的透明背景 闹钟标题颜色 - 秒针颜色 - 闹钟字体大小 + 秒针颜色 + 时钟字体大小 闹钟标题字体大小 了解更新内容: \n \n%s 访问项目 GitHub 页面: \n \n%s 阅读许可:\n\n%s - 访问 %1$s 的 GitHub 页面: -\n -\n%2$s 立式数字时钟 Material You 微件 立式数字时钟 - 自定义小时颜色 - 使用分钟默认颜色 - 使用小时默认颜色 - 自定义分钟颜色 + 自定义小时颜色 + 使用分钟默认颜色 + 使用小时默认颜色 + 分钟颜色 标准 Material You 设置微件的样式、颜色和字体大小 在底部导航栏菜单中显示标签页指示器 下次闹钟 Material You - “下次闹钟”和“没有预定的闹钟”标题自定义颜色 - 使用默认颜色作为闹钟标题 - 闹钟标题自定义颜色 - 微件最大字体大小 + “下次闹钟”和“没有预定的闹钟”标题自定义颜色 + 使用默认颜色作为闹钟标题 + 微件最大字体大小 下次闹钟 没有预定的闹钟 - 为“下次闹钟”和“没有预定的闹钟”标题使用默认颜色 + 为“下次闹钟”和“没有预定的闹钟”标题使用默认颜色 切换屏幕时启用淡化过渡 下次闹钟 排序定时器 @@ -498,24 +482,8 @@ 12 小时前 按时长(升序) 按时长(降序) - 按名称 - 1 分钟 - 2 分钟 - 3 分钟 - 4 分钟 - 5 分钟 - 6 分钟 - 7 分钟 - 9 分钟 - 10 分钟 - 30 分钟 - 1 小时 + 按名称 - 8 分钟 - 10 秒 - 20 秒 - 30 秒 - 5 秒 关闭并删除 向左滑动以暂停,向右滑动以关闭并删除闹钟 向左或向右滑动以关闭并删除闹钟 @@ -524,7 +492,6 @@ 预定的临时闹钟 默认启用自动删除临时闹钟 可以在扩展闹钟视图中为每个闹钟更改此设置 - 已删除 %s 的临时闹钟 备份和还原 备份或还原应用程序数据,自定义铃声除外 是否要备份或还原应用程序数据? @@ -536,7 +503,7 @@ 摇晃设备以停止闹钟 将强调色应用于夜间模式 夜间模式的强调色 - 手动(通过拖动) + 手动(通过拖动) 使用音量按钮重置已结束的定时器 仅适用于锁屏 使用电源按钮重置已结束的定时器 @@ -582,18 +549,13 @@ 指针 (Material) 荷兰语 俄语 - 无秒针 - 有秒针 - 此微件仅可从 Android 12 配置 - 选择样式 - 上次使用的标签页‌ + 上次使用的标签页 输入的值不正确 - 打开应用时显示的标签页‌ + 打开应用时显示的标签页 请输入 0 到 60 分钟之间的值 土耳其语 重置设置 - 是否确定要重置所有应用设置?\n\n注意:不会重置自定义铃声。 - 查看资料 + 是否确定要重置所有应用设置? 蓝色 重置已完成 韩语 @@ -602,7 +564,6 @@ 在工具栏中显示标题 显示标签页标题 始终 - 从不 选择时 捷克语 注意:无论此设置的状态如何,如果定时器或秒表正在运行,并且显示了相应的标签页,则屏幕将始终开启 @@ -617,7 +578,7 @@ 中文(繁体) 已关闭并删除 %s 的闹钟 设置闹钟 - 已设置于 %s + 已设置 %s 日历 文本 日期选择器样式 @@ -646,16 +607,124 @@ 请断开蓝牙设备的连接以访问此设置 仅在连接蓝牙设备时有效 将边距应用于微件两侧 - - 节假日闹钟 - - 更新节假日数据 - - 手动更新节假日和工作日数据 - - 节假日数据 URL - - 设置节假日和工作日数据的 URL - - 输入节假日和工作日数据的 URL + 为每个闹钟使用自定义音量 + 延迟闹钟… + 颜色 + 秒针 + 复古 + 表盘 + 太阳 + 花朵 + 表盘颜色 + 时针颜色 + 使用默认时针颜色 + 分针颜色 + 使用默认分针颜色 + 使用默认秒针颜色 + 使用默认背景颜色 + 带数字 + 无数字 + 使用默认表盘颜色 + 排序闹钟 + 按闹钟时间 + 按下次闹钟时间 + 优先显示启用的闹钟 + 排序城市 + 按时区(升序) + 按时区(降序) + 向城市添加个人备注 + 点击城市即可添加或编辑其个人备注 + %s 的备注 + 搜索城市 + 按创建顺序(从旧到新) + 按创建顺序(从新到旧) + 启用长按操作按钮创建延迟响铃的闹钟 + 如果禁用,将显示两个按钮用于创建闹钟 + 振动模式 + 柔和 + 强烈 + 心跳 + 逐渐增强 + 钟摆振动 + 振动开始前的延迟时间 + + 请输入 0 到 10 分钟之间的值 + \n\n是否要继续? + 为每个闹钟使用自定义暂停时长 + 自定义闹钟暂停时长将被重置。%s + 自定义闹钟音量将被重置。%s + 闹钟启用后立即显示关闭按钮 + 如果禁用,该按钮将与通知提醒同时出现 + 为每个闹钟使用自定义错过的闹钟重复次数 + 自定义错过的闹钟重复次数将被重置。%s + 重复错过的闹钟 + 1 次 + 3 次 + 5 次 + 10 次 + 为每个闹钟使用自定义音量增大时长 + 自定义闹钟音量增大时长将被重置。%s + 为每个闹钟使用自定义闹钟停止时长 + 自定义闹钟停止时长将被重置。%s + 访问主要功能页面:\n\n%s + 闹钟已关闭。已重新设置 %s。 + 模糊强度 + 选择背景图片 + 图片已成功选择 + 对背景图片应用模糊效果 + 显示文本阴影 + 阴影颜色 + 阴影偏移 + 显示定时器状态指示器 + 正在运行的定时器指示器颜色 + 已暂停的定时器指示器颜色 + 已结束的定时器指示器颜色 + 自定义背景圆角半径 + 如果您的主屏幕不支持,此设置允许您将微件边角变圆 + 背景圆角半径 + 正在添加铃声… (%1$d/%2$d) + 以大写字母显示文本 + 新图片 + 选择或删除背景图片 + 背景图片 + 图片已成功删除 + 新字体 + 选择自定义字体 + 选择或删除自定义字体 + 字体 + 字体已成功选择 + 字体已成功删除 + 振动 + 显示设置 + 为每个闹钟使用自定义振动模式 + 自定义振动模式将被重置。%s + 指针时钟大小 + 以紧凑模式显示活动定时器 + 显示电池电量 + 电池电量颜色 + 电池电量粗体显示 + 电池电量斜体显示 + 总计 + 圈数 + 分段 + 保加利亚语 + 泰米尔语 + + 工作日设置 + 法定工作日 + 大小周大周 + 大小周小周 + 单休 + + 节假日闹钟 + + 更新节假日数据 + + 手动更新节假日和工作日数据 + + 节假日数据 URL + + 设置节假日和工作日数据的 URL + + 输入节假日和工作日数据的 URL diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 87711be2c..f94687967 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -1,27 +1,21 @@ - - - + - + Clock debug - - h:mm - - kk:mm - -  / EEE - + + h:mm + + kk:mm + +  / EEE + EEEMMMd - + EEEEMMMMd - + A - + Alarm Clock Timer @@ -30,7 +24,7 @@ Analog Widget Digital Widget - + Dismiss Snooze Create @@ -62,14 +56,14 @@ Set Crescendo Duration Set Alarm Volume - + DeskClock Notification Intent HardwareButton Shortcut - + Clock debug Debug Debug settings displayed @@ -81,24 +75,24 @@ Enable local logging This setting may cause slowdowns; disable it to remove Debug mode - - - - + + + + @string/system_theme @string/light_theme @string/dark_theme - + 0 1 2 - + @string/label_default @string/blue_gray_accent_color @@ -113,7 +107,7 @@ @string/yellow_accent_color @string/blue_accent_color - + 0 1 @@ -129,31 +123,31 @@ 11 - + @string/label_default @string/amoled_dark_theme_mode - + 0 1 - + @string/tab_title_visibility_always @string/tab_title_visibility_never @string/tab_title_visibility_when_selected - + 0 1 2 - + @string/alarm_confirmation_toast_1 @string/alarm_confirmation_toast_2 @@ -165,7 +159,7 @@ @string/alarm_confirmation_toast_8 - + @string/alarm_notification_reminder_30_minutes @string/alarm_notification_reminder_1_hour @@ -176,7 +170,7 @@ @string/alarm_notification_reminder_10_hours @string/alarm_notification_reminder_12_hours - + 30 60 @@ -188,7 +182,7 @@ 720 - + @string/auto_silence_5_seconds @string/auto_silence_10_seconds @@ -198,7 +192,7 @@ @string/auto_silence_end_of_ringtone @string/auto_silence_never - + 5 10 @@ -209,30 +203,30 @@ -1 - + @string/week_start_saturday @string/week_start_sunday @string/week_start_monday - + - + 7 - + 1 - + 2 - + @string/button_action_snooze @string/button_action_dismiss @string/button_action_control_volume @string/button_action_nothing - + 1 2 @@ -240,79 +234,77 @@ -1 - + @string/button_action_snooze @string/button_action_dismiss @string/button_action_nothing - + 1 2 0 - + @string/clock_style_analog @string/clock_style_analog_material @string/clock_style_digital - + analog analog_material digital - + @string/clock_style_analog @string/clock_style_digital @string/clock_style_spinner - + analog digital spinner - + @string/date_picker_style_calendar @string/date_picker_style_text @string/clock_style_spinner - + calendar text spinner - + @string/timer_creation_view_style_keypad @string/clock_style_spinner - + keypad spinner - + @string/sort_timer_manually @string/sort_timer_ascending_duration @string/sort_timer_descending_duration @string/sort_timer_name - + 0 1 @@ -320,7 +312,7 @@ 3 - + @string/default_time_to_add_to_timer_1_minute @string/default_time_to_add_to_timer_2_minutes @@ -335,8 +327,7 @@ @string/default_time_to_add_to_timer_30_minutes @string/default_time_to_add_to_timer_1_hour - + 1 2 @@ -352,7 +343,7 @@ 60 - + @string/jocular_content_message_1 @string/jocular_content_message_2 @@ -366,7 +357,7 @@ @string/jocular_content_message_10 - + @string/button_action_nothing @string/sw_start_pause_action @@ -374,7 +365,7 @@ @string/sw_lap_button @string/sw_share_button - + 0 1 @@ -383,7 +374,7 @@ 4 - + @string/last_tab_used_title @string/menu_alarm @@ -391,8 +382,7 @@ @string/menu_timer @string/menu_stopwatch - + -1 0 @@ -401,7 +391,7 @@ 3 - + @string/settings_language_system @string/settings_language_english_us @@ -422,7 +412,7 @@ @string/settings_language_czech @string/settings_language_chinese_traditional_tw - + system_language_code en_US @@ -443,4 +433,121 @@ cs zh_TW - +Set Delay + + @string/label_default + @string/vibration_pattern_soft + @string/vibration_pattern_strong + @string/vibration_pattern_heartbeat + @string/vibration_pattern_escalating + @string/vibration_pattern_tick_tock + + + + default + soft + strong + heartbeat + escalating + tick_tock + + + + + @string/clock_dial_with_numbers + @string/clock_dial_without_numbers + + + + dial_with_numbers + dial_without_numbers + + + + + @string/clock_dial_sun + @string/clock_dial_flower + + + + dial_sun + dial_flower + + + + + @string/label_default + @string/clock_dial_with_numbers + @string/clock_dial_without_numbers + + + + default + dial_with_numbers + dial_without_numbers + + + + + @string/label_default + @string/clock_second_hand_vintage + + + + default + second_hand_vintage + + + + + @string/sort_alarm_time + @string/sort_next_alarm_time + @string/sort_name + @string/sort_descending_creation_order + @string/sort_ascending_creation_order + + + + 0 + 1 + 2 + 3 + 4 + + + + + @string/sort_cities_ascending_time_zone + @string/sort_cities_descending_time_zone + @string/sort_name + @string/sort_manually + + + + 0 + 1 + 2 + 3 + +Clock nightly + + Set Vibration Pattern + Set Missed Alarm Repeat Limit + + @string/label_never + @string/missed_alarm_repeat_limit_1_time + @string/missed_alarm_repeat_limit_3_times + @string/missed_alarm_repeat_limit_5_times + @string/missed_alarm_repeat_limit_10_times + + + + -1 + 1 + 3 + 5 + 10 + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 748cc42c3..5eae5e21d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,741 +1,691 @@ - - - - + + + + Clock - + Alarm - + Schedule alarm - + Holiday Skip holiday 大小周大周 大小周小周 單休 - + Vibrate - + Flash - + Delete alarm once dismissed - + Select time - + Select Date - + Alarm label - + Default - + Off - + Delete - + Duplicate - + Warning - + Hours - + Minutes - + Seconds - + Default alarm sound - + Alarm sound - + Timer sound - + Add new - + Long click to select a folder - + Remove - + Selected - + More options - + Alarms and timers using this sound will play the default sound instead. - + Your sounds - + Device sounds - + The sound content cannot be accessed. - + Timer Expired - + Tomorrow - + Today - + Scheduled for %s - + Dismiss - + Dismiss & Delete - + Missed alarm - %1$s - %2$s - + %1$s - %2$s + Snoozed - + - + 1 min - - %d min + + %d min - + Alarm off - + Alarm off and deleted - + Snooze - + - + Snoozing for 1 minute. - - Snoozing for %d minutes. + + Snoozing for %d minutes. - - Snoozing until %s - + + Snoozing until %s + Upcoming alarm - + Upcoming occasional alarm - + Your missed alarm has been deleted - - - + + + Less than a minute remaining - %1$s %3$s - %2$s %3$s - %1$s %2$s remaining - %4$s %3$s - %2$s %4$s %3$s - %1$s %4$s %3$s - %1$s %2$s %4$s %3$s - - %1$s minute added to timer, %2$s - + %1$s %3$s + %2$s %3$s + %1$s %2$s remaining + %4$s %3$s + %2$s %4$s %3$s + %1$s %4$s %3$s + %1$s %2$s %4$s %3$s + + %1$s minute added to timer, %2$s + remaining - + remaining - + Alarm set for less than 1 minute from now. - - Alarm set for %1$s from now. - - Alarm set for %2$s from now. - - Alarm set for %1$s and %2$s from now. - - Alarm set for %3$s from now. - - Alarm set for %1$s and %3$s from now. - - Alarm set for %2$s and %3$s from now. - - Alarm set for %1$s, %2$s, and %3$s from now. - + + Alarm set for %1$s from now. + + Alarm set for %2$s from now. + + Alarm set for %1$s and %2$s from now. + + Alarm set for %3$s from now. + + Alarm set for %1$s and %3$s from now. + + Alarm set for %2$s and %3$s from now. + + Alarm set for %1$s, %2$s, and %3$s from now. + - + 1 day - - %s days + + %s days - + - + 1 hour - - %s hours + + %s hours - + 1 hr - - %s hr + + %s hr - + - + 1 minute - - %s minutes + + %s minutes - + 1 min - - %s min + + %s min - + - + 1 second - - %s seconds + + %s seconds - + Every day - + ", " - + Loading\u2026 - + Analog clock - + Choose your style - + Without the second hand - + With the second hand - + This widget is only configurable from Android 12 - + Analog clock Material You - + Digital clock - + Vertical digital clock - + Next alarm - + Digital clock Material You - + Vertical digital clock Material You - + Next alarm Material You - + Settings - + One or more essential permissions are denied. Not granting them can lead to malfunctions. - + Click here to display the Permissions management page. - + Style - + Style and colors - + Font - + Miscellaneous - + Ringtone - + Actions - + Standard - + Material You - + Interface - + Set style and color applied to the application - + Theme - + System - + Light - + Dark - + Accent color - + Apply accent color to night mode - + Accent color for day mode - + Accent color for night mode - + Black - + Blue - + Blue Gray - + Brown - + Green - + Indigo - + Orange - + Pink - + Purple - + Red - + Yellow - + Dark theme mode - + AMOLED - + Display card backgrounds - + Displays a background for some application elements (alarms, cities, timer button, etc.) - + Display background borders - + Displays a background border for some application elements (alarms, cities, timer button, etc.) - + Language - + System language - + English (US) - + French - + German - + Chinese (simplified, PRC) - + Estonian - + Italian - + Polish - + Portuguese - + Serbian - + Spanish - + Ukrainian - + Dutch - + Russian - + Turkish - + Korean - + Czech - + Chinese (traditional, Taiwan) - + Tab to display when opening the application - + Last tab used - + Enable vibrations - + Only some interface buttons are affected (e.g.: the timer play/pause button or switches) - + Display the title in the toolbar - + Display the tab titles - + Always - + Never - + When selected - + Display the tab indicator in the bottom navigation menu - + Enable fade transitions when switching screens - + Keep the screen on - + Note: The screen will always be on if a timer or stopwatch is running and the corresponding tab is displayed, regardless of the status of this setting - + Snooze length - + Use a custom volume for each alarm - + Allows setting a different volume for each alarm in the expanded alarm view - + Notification reminder - + 30 minutes before - + 1 hour before - + 2 hours before - + 4 hours before - + 6 hours before - + 8 hours before - + 10 hours before - + 12 hours before - + Use advanced audio playback - + Improves Bluetooth compatibility and prevents sound interruptions during repetition \n\nNote: Some ringtones may have slight audio glitches - + Enable automatic routing of ringtones to Bluetooth devices - + Use system media volume - + Only works if a Bluetooth device is connected - + Alarm volume for Bluetooth devices - + Connect a Bluetooth device to access this setting - + Disconnect the Bluetooth device to access this setting - + Enable vibrations when creating alarms - + Enable vibrations when alarm is snoozed or dismissed - + Double vibration when alarm is snoozed and single vibration when alarm is dismissed - + Turn on back flash when alarm is triggered - + Enable automatic deletion of occasional alarms by default - + Time picker style - + Date picker style - + Calendar - + Text - + This setting can be changed for each alarm in the expanded alarm view - + Customize alarm display - + Alarm title color - + Slide zone color - + Color of the \"Snooze\" title - + Color of the \"Snooze\" button - + Color of the \"Dismiss\" title - + Color of the \"Dismiss\" button - + Alarm button color - + Alarm clock font size - + Alarm title font size - + Display ringtone title - + Ringtone title color - + Gradually increase volume - + Silence after - + Never - + The end of the ringtone - + 5 seconds - + 10 seconds - + 20 seconds - + 30 seconds - + 1 minute - + None - + Start week on - + Saturday - + Sunday - + Monday - + Alarm volume - + Silent - + Random ringtone - + Unknown - + Alarm volume muted - + Unmute - + Default alarm ringtone is silent - + Change - + Device is set to total silence - + Swipe to dismiss or snooze alarms - + Volume buttons - + Snooze - + Dismiss - + Control volume - + Nothing - + Power button - - - + + + Label Ringtone Add label - - + + Alarm - + Timer - + Clock - + Stopwatch - - + + Add alarm - + Cities - - + + Sort by time - + Sort by name - + Selected Cities - - + + Resume - + Reset - - - Set actions for volume buttons - + + + Set actions for volume buttons + Volume up - + Volume up (after long press) - + Volume down - + Volume down (after long press) - + Start / Pause - + Start - + Pause - + Lap - + Share - + h - + m - + s - - %1$s, %2$s, %3$s - - # %d - - # %02d - - - My time is %s - + + %1$s, %2$s, %3$s + + # %d + + # %02d + + + My time is %s + Lap times: - - Lap %d - - + + Lap %d + + Timer label - + New timer duration - + Enter a value between 0 and 24 hours - + Enter a value between 0 and 59 minutes - + Enter a value between 0 and 59 seconds - + Minutes to add to the timer - + Enter a value between 0 and 60 minutes - + Enter a value between 0 and 59 seconds - + Incorrect value entered - + Add Timer - + Start - - Delete %s - + + Delete %s + Add %s Minute - + Add %1$s Minute %2$s Seconds - + Add %s min - + Add %1$s min %2$s s - + + %s - + Stop - + Stop all timers - + Timer canceled - + Time\'s up - - %d timers expired - - %d timers missed - + + %d timers expired + + %d timers missed + Timer - - - Missed timer: %s - + + + Missed timer: %s + Pause - + Reset all timers - %1$d:%2$02d:%3$02d - %1$d:%2$02d - %1$02d - + %1$d:%2$02d:%3$02d + %1$d:%2$02d + %1$02d + You\'re quite the speed demon. - + Enjoy the fruits of your labor. - + Androids are known to be fast, but not as fast as you! - + Phew. - + L33t times. - + Such prodigious velocity. - + Let\'s do the time warp again. - + Just a jump to the left. - + You have a palette for haste. - + Photonic velocity. - + Home - + Cities - + Clock Set style, time zone and change device date and time - + Style - + Display time with seconds - + Hide AM/PM text - + Change date \u0026 time - + Analog - + Analog (Material) - + Digital - + Spinner - + Automatic home clock - + While traveling in an area where the time is different, add a clock for home - + Home time zone - - - - %s checked - - %s unchecked - - + + + + %s checked + + %s unchecked + + "Marshall Islands" "Midway Island" "Hawaii" @@ -822,8 +772,8 @@ "Tonga" "Jakarta" - - + + Pacific/Majuro Pacific/Midway Pacific/Honolulu @@ -910,43 +860,43 @@ Pacific/Tongatapu Asia/Jakarta - + About - + 100% FOSS Clock, based on AOSP - + Version - - Version %1$s - + + Version %1$s + What\'s new - + Discover what\'s new:\n\n%s - + Main features - + View on GitHub - + Visit the project GitHub page:\n\n%s - + Translate on Codeberg - + Visit the Codeberg page:\n\n%s - + Open-source license - + Read the licence:\n\n%s - + GNU General Public License v3.0 - + Contributors - + View profile - + Visit %1$s\'s GitHub page:\n\n%2$s - + Credits - + • Set the alarms to a specific date; \n\n• Flip and shake action to dismiss/postpone alarm; @@ -972,329 +922,252 @@ \n\n• Material design; \n\n• Dynamic colors for Android 12+; - + Reset settings - + Are you sure you want to reset all app settings? \n\nNote: custom ringtones will not be reset. - + Reset completed - + Close - + New alarm - + Create new alarm - + New timer - + Create new timer - + Start - + Start stopwatch - + Pause - + Pause stopwatch - + Screensaver - + Start screensaver - + Alarms - + Set style, default ringtone, volume, actions to dismiss or snooze alarms, first day of the week and reminder notification time - + Processes actions from timer notifications. - + Processes actions from stopwatch notifications. - + Paused - + Swipe left to snooze or right to dismiss - + Swipe left to snooze or right to dismiss and delete alarm - + Swipe left or right to dismiss - + Swipe left or right to dismiss and delete alarm - + Click to snooze alarm - + Click to dismiss alarm - + Click to dismiss and delete alarm - + Timers - + Set default ringtone, volume and timer sorting - + Timer creation view style - + Keypad - + Timer vibrate - + Reset expired timers with volume buttons - + Reset expired timers with power button - + Only works on lock screen - + Flip the device to stop the alarm - + Shake the device to stop the alarm - + Sort timers - + Manually (by dragging) - + By duration (ascending order) - + By duration (descending order) - + By name - + Default time to add to timer - + 1 minute - + 2 minutes - + 3 minutes - + 4 minutes - + 5 minutes - + 6 minutes - + 7 minutes - + 8 minutes - + 9 minutes - + 10 minutes - + 30 minutes - + 1 hour - + Transparent background when timers expire - + Display a warning before deleting a timer - + Delete timer - + Are you sure you want to delete the \"%s\" timer? - + Timer paused - - %d timers - + + %d timers + Screensaver - + Set style, color and font of the screensaver - + Select/enable screensaver - + Opens the main Android screensaver settings - + Dynamic clock colors - + Clock color - + Seconds hand color - + Date color - + Next alarm color - + Brightness - + Digital clock in bold - + Digital clock in italics - + Date in bold - + Date in italics - + Next alarm in bold - + Next alarm in italics - + Preview - + Starts the screensaver immediately - + Expand alarm - + Collapse alarm - + Alarm deleted - - Occasional %s alarm deleted - + + Occasional %s alarm deleted + Alarm created - - / %s - - %1$s ahead - - %1$s behind - - %1$s %2$s ahead - - %1$s %2$s behind - - Tomorrow, %1$s - - Yesterday, %1$s - - - - - Next alarm: %s - + + / %s + + %1$s ahead + + %1$s behind + + %1$s %2$s ahead + + %1$s %2$s behind + + Tomorrow, %1$s + + Yesterday, %1$s + + + + + Next alarm: %s + No Alarms - - Invalid time %1$d:%2$d %3$s - - No alarm at %1$d:%2$d - + + Invalid time %1$d:%2$d %3$s + + No alarm at %1$d:%2$d + No scheduled alarms - + No label specified - + No alarms contain the label - + No alarm scheduled for this time - - %s alarm dismissed - - %s alarm dismissed and deleted - - Alarm is set for %s - + + %s alarm dismissed + + %s alarm dismissed and deleted + + Alarm is set for %s + Timer created - + Timer dismissed - %d timers dismissed + %d timers dismissed - - + + Invalid timer length - + Invalid timer selected - + No expired timers - - %s alarm can\'t be dismissed yet, still more than 24 hours away - + + %s alarm can\'t be dismissed yet, still more than 24 hours away + Dismiss alarm - + Pick which alarm to dismiss - + No firing alarms - - %s alarm snoozed for 10 minutes - + + %s alarm snoozed for 10 minutes + Device flip action - + Device shake action - + Shake intensity - + Firing alarms & timers Missed alarms Snoozed alarms Upcoming alarms Stopwatch Timer - + Version %s MAIN FEATURES Now @@ -1312,7 +1185,7 @@ ► Flip and shake action to dismiss/postpone alarm; <br> <br> ► Turn off/postpone the alarm with the power button or volume buttons; <br> <br> ► %s
- + … and much more! IMPORTANT INFORMATION @@ -1325,87 +1198,83 @@ • <b>FULL SCREEN NOTIFICATIONS;</b> <br> <br> Quit Are you sure you want to leave the application? - + Holiday alarm - + Update holiday data - + Manually update holiday and work schedule data - + Holiday data URL - + Set the URL for holiday and work schedule data - + Enter the URL for the holiday and work schedule data - - + + Widgets - + Set style, colors and font size of the widgets - + Display background - + Background color - + Display cities on the widget - + Use default color for main clock - + Main clock custom color - + Use default color for hours - + Custom hours color - + Use default color for minutes - + Custom minutes color - + Display date on the widget - + Use default color for date - + Date custom color - + Display next alarm on the widget - + Use default color for next alarm - + Next alarm custom color - + Use default color for city clock - + City clock custom color - + Use default color for city name - + City name custom color - + Maximum font size of main clock - + \"Display cities on the widget\" must be disabled to access this setting - + Apply padding to widget sides - + Use default color for \"Next alarm\" and \"No upcoming alarm\" titles - + Custom color for \"Next alarm\" and \"No upcoming alarm\" titles - + Use default color for the alarm title - + Alarm title custom color - + Maximum widget font size - + Next alarm - + No upcoming alarm - + Permissions management Show the permissions necessary for the application to work properly Requirement:  @@ -1414,45 +1283,295 @@ For Xiaomi devices only Granted Denied - + Permission denied Revoke access Close Are you sure you want to revoke this permission? - + IGNORE BATTERY OPTIMIZATIONS Battery Optimizations By default, the Android system configures all user-installed apps to be optimized to save battery life. \n\nWhile this is acceptable for many apps, this app is an alarm app and often causes issues if configured to optimize battery. Once enabled, the Android system can kill the app at any time and/or cancel alarms it has scheduled, which would adversely affect the app\'s operation. \n\nWe recommend whitelisting this app from battery optimizations. - + ENABLE NOTIFICATIONS Notifications In addition to setting alarms, this app also has a timer and a stopwatch mode. \n\nIn order for notifications to appear correctly for all modules and especially for the alarm to be triggered, we recommend activating notifications for this application. - + ENABLE FULL SCREEN NOTIFICATIONS Full Screen Notifications Since Android 14, in order for the alarm to be triggered when the screen is off, it\'s imperative that \"Full Screen Notifications\" permission is enabled. - + SHOW ON LOCKSCREEN Show on lockscreen On Xiaomi devices, you should manually enable the \"Show on Lock screen\" permission from the device settings in order for the application to work properly. - - + + Backup & Restore - + Backup or restore application data except custom ringtones - + Do you want to backup or restore application data? - + Backup - + Restore - + Backup completed - + Restore completed - \ No newline at end of file +Alarm in… + + \n\nDo you want to continue? + + Colors + + Use a custom snooze duration for each alarm + + Custom alarm snooze durations will be reset.%s + + Custom alarm volumes will be reset.%s + + Display Dismiss button as soon as alarm is enabled + + If disabled, the button will appear at the same time as the notification reminder + + Vibration pattern + + Soft + + Strong + + Heartbeat + + Escalating + + Tick-Tock + + Delay before vibration starts + + None + + Enter a value from 0 to 10 minutes + + Sort alarms + + By alarm time + + By next alarm time + + By name + + By creation order (oldest last) + + By creation order (oldest first) + + Display enabled alarms first + + Enable long press on the action button to create alarms with delay + + If disabled, two buttons will be displayed to create alarms + + Use a custom volume increase duration for each alarm + + Custom alarm volume increase durations will be reset.%s + + Use a custom alarm stop duration for each alarm + + Custom alarm stop durations will be reset.%s + + Search for a city + + With numbers + + Without numbers + + Second hand + + Vintage + + Dial + + Sun + + Flower + + Sort cities + + Add a personal note to cities + + Tap on a city to add or edit its personal note + + Note for %s + + Manually (by dragging) + + By time zone (ascending order) + + By time zone (descending order) + + Use default color for main clock + + Hour color + + Use default color for hours + + Minute color + + Use default color for minutes + + Dial color + + Use default color for dial + + Hour hand color + + Use default color for hour hand + + Minute hand color + + Use default color for minute hand + + Second hand color + + Use default color for second hand + + Date color + + Use default color for date + + Next alarm color + + Use default color for next alarm + + City clock color + + Use default color for city clock + + City name color + + Use default color for city name + + Use default color for the alarm title + + Alarm dismissed. Reschedule for %s. + + Use default color for background + + Display cities on the widget + + Maximum font size of the main clock + + Use default color for \"Next alarm\" and \"No upcoming alarm\" titles + + Color for \"Next alarm\" and \"No upcoming alarm\" titles + + Maximum widget font size + + Never + + New image + + New font + + Adding ringtones… (%1$d/%2$d) + + Select a custom font + + Select or delete custom font + + Font + + Font successfully selected + + Font successfully deleted + + Display settings + + Use a custom missed alarm repeat count for each alarm + + Custom missed alarm repeat count will be reset.%s + + Repeat missed alarms + + 1 time + + 3 times + + 5 times + + 10 times + + Use a custom vibration pattern for each alarm + + Custom vibration pattern will be reset.%s + + Clock font size + + Display text shadow + + Shadow color + + Shadow offset + + Blur intensity + + Select a background image + + Select or delete background image + + Background image + + Image successfully selected + + Image successfully deleted + + Apply a blur effect to the background image + + Laps + + Split + + Total + + Analog clock size + + Visit the main features page:\n\n%s + + Display active timers in compact mode + + Display timer state indicator + + Running timer indicator color + + Paused timer indicator color + + Expired timer indicator color + + Display battery level + + Battery level color + + Battery level in bold + + Battery level in italics + + undo + + Display text in uppercase + + Customize the background corner radius + + Allows you to round widget corners if your home screen doesn\'t support it + + Background corner radius + + With number + + Without number + + Vibration + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings_alarm.xml b/app/src/main/res/xml/settings_alarm.xml index 0c187a557..fb1745397 100644 --- a/app/src/main/res/xml/settings_alarm.xml +++ b/app/src/main/res/xml/settings_alarm.xml @@ -8,20 +8,27 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - - - + + - - + - - - + + - + + - + + + + - - + + - - - - - + - + + + + + + + + + + + + + + - - - - - - + - - - - - + + - + + - - + + - diff --git a/fix_alarm_settings_final.py b/fix_alarm_settings_final.py new file mode 100644 index 000000000..972310900 --- /dev/null +++ b/fix_alarm_settings_final.py @@ -0,0 +1,13 @@ +import sys + +path = 'app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java' +with open(path, 'r') as f: + content = f.read() + +# Add to onPreferenceChange +if 'case KEY_HOLIDAY_DATA_URL -> {' not in content: + content = content.replace('switch (preference.getKey()) {', + 'switch (preference.getKey()) {\\n case KEY_HOLIDAY_DATA_URL -> {\\n String url = (String) newValue;\\n SettingsDAO.setHolidayDataUrl(mPrefs, url);\\n mHolidayDataUrlPref.setSummary(url);\\n return true;\\n }') + +with open(path, 'w') as f: + f.write(content) diff --git a/fix_alarm_settings_v2.py b/fix_alarm_settings_v2.py new file mode 100644 index 000000000..5f8520c82 --- /dev/null +++ b/fix_alarm_settings_v2.py @@ -0,0 +1,32 @@ +import sys + +path = 'app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java' +with open(path, 'r') as f: + content = f.read() + +# Add missing imports +required_imports = [ + 'import androidx.preference.Preference;', + 'import androidx.preference.SwitchPreferenceCompat;', + 'import androidx.preference.ListPreference;', + 'import androidx.preference.EditTextPreference;', + 'import com.best.deskclock.settings.custompreference.AlarmSnoozeDurationPreference;', + 'import com.best.deskclock.settings.custompreference.AlarmVolumePreference;', + 'import com.best.deskclock.settings.custompreference.AutoSilenceDurationPreference;', + 'import com.best.deskclock.settings.custompreference.CustomListPreference;', + 'import com.best.deskclock.settings.custompreference.CustomPreference;', + 'import com.best.deskclock.settings.custompreference.CustomPreferenceCategory;', + 'import com.best.deskclock.settings.custompreference.CustomSeekbarPreference;', + 'import com.best.deskclock.settings.custompreference.CustomSwitchPreference;', + 'import com.best.deskclock.settings.custompreference.VibrationPatternPreference;', + 'import com.best.deskclock.settings.custompreference.VibrationStartDelayPreference;', + 'import com.best.deskclock.settings.custompreference.VolumeCrescendoDurationPreference;' +] + +import_insertion_point = 'package com.best.deskclock.settings;' +for imp in required_imports: + if imp not in content: + content = content.replace(import_insertion_point, import_insertion_point + '\\n' + imp) + +with open(path, 'w') as f: + f.write(content) diff --git a/fix_alarm_settings_v3.py b/fix_alarm_settings_v3.py new file mode 100644 index 000000000..d8b22ca77 --- /dev/null +++ b/fix_alarm_settings_v3.py @@ -0,0 +1,43 @@ +import sys + +path = 'app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java' +with open(path, 'r') as f: + lines = f.readlines() + +new_lines = [] +for line in lines: + if line.startswith('package com.best.deskclock.settings;'): + new_lines.append(line) + new_lines.append('import androidx.preference.Preference;\n') + new_lines.append('import androidx.preference.SwitchPreferenceCompat;\n') + new_lines.append('import androidx.preference.ListPreference;\n') + new_lines.append('import androidx.preference.EditTextPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.AlarmSnoozeDurationPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.AlarmVolumePreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.AutoSilenceDurationPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.CustomListPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.CustomPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.CustomPreferenceCategory;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.CustomSeekbarPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.CustomSwitchPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.VibrationPatternPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.VibrationStartDelayPreference;\n') + new_lines.append('import com.best.deskclock.settings.custompreference.VolumeCrescendoDurationPreference;\n') + elif not line.strip().startswith('import androidx.preference.') and \ + not line.strip().startswith('import com.best.deskclock.settings.custompreference.') and \ + 'illegal character' not in line: + new_lines.append(line) + +# Remove duplicates if any +final_lines = [] +seen = set() +for line in new_lines: + if line.startswith('import ') or line.startswith('package '): + if line not in seen: + final_lines.append(line) + seen.add(line) + else: + final_lines.append(line) + +with open(path, 'w') as f: + f.writelines(final_lines) diff --git a/fix_alarm_settings_v4.py b/fix_alarm_settings_v4.py new file mode 100644 index 000000000..4980a8d1d --- /dev/null +++ b/fix_alarm_settings_v4.py @@ -0,0 +1,47 @@ +import sys + +path = 'app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java' +with open(path, 'r') as f: + content = f.read() + +# Fix the broken package line +content = content.replace('package com.best.deskclock.settings;\\nimport androidx.preference.EditTextPreference;\\nimport androidx.preference.ListPreference;\\nimport androidx.preference.SwitchPreferenceCompat;', + 'package com.best.deskclock.settings;') + +# Clean up imports +import_list = [ + 'package com.best.deskclock.settings;', + 'import androidx.preference.Preference;', + 'import androidx.preference.SwitchPreferenceCompat;', + 'import androidx.preference.ListPreference;', + 'import androidx.preference.EditTextPreference;', + 'import com.best.deskclock.settings.custompreference.AlarmSnoozeDurationPreference;', + 'import com.best.deskclock.settings.custompreference.AlarmVolumePreference;', + 'import com.best.deskclock.settings.custompreference.AutoSilenceDurationPreference;', + 'import com.best.deskclock.settings.custompreference.CustomListPreference;', + 'import com.best.deskclock.settings.custompreference.CustomPreference;', + 'import com.best.deskclock.settings.custompreference.CustomPreferenceCategory;', + 'import com.best.deskclock.settings.custompreference.CustomSeekbarPreference;', + 'import com.best.deskclock.settings.custompreference.CustomSwitchPreference;', + 'import com.best.deskclock.settings.custompreference.VibrationPatternPreference;', + 'import com.best.deskclock.settings.custompreference.VibrationStartDelayPreference;', + 'import com.best.deskclock.settings.custompreference.VolumeCrescendoDurationPreference;' +] + +lines = content.splitlines() +new_lines = [] +skip_imports = False +for line in lines: + if line.startswith('package '): + new_lines.extend(import_list) + skip_imports = True + continue + if skip_imports: + if line.startswith('import ') or not line.strip(): + continue + else: + skip_imports = False + new_lines.append(line) + +with open(path, 'w') as f: + f.write('\\n'.join(new_lines)) diff --git a/fix_keys_and_defaults.py b/fix_keys_and_defaults.py new file mode 100644 index 000000000..ef24f9bb8 --- /dev/null +++ b/fix_keys_and_defaults.py @@ -0,0 +1,41 @@ +import sys + +keys_path = 'app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java' +with open(keys_path, 'r') as f: + keys_content = f.read() + +missing_keys = [ + ('KEY_ALARM_DIGITAL_CLOCK_FONT_SIZE', '"key_alarm_digital_clock_font_size"'), + ('KEY_DISPLAY_ALARM_SECOND_HAND', '"key_display_alarm_second_hand"'), + ('KEY_TIMER_AUTO_SILENCE_DURATION', '"key_timer_auto_silence_duration"'), + ('KEY_TIMER_ADD_TIME_BUTTON_VALUE', '"key_timer_add_time_button_value"') +] + +for key, val in missing_keys: + if key not in keys_content: + keys_content = keys_content.replace('}', f' public static final String {key} = {val};\n}}') + +with open(keys_path, 'w') as f: + f.write(keys_content) + +defaults_path = 'app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java' +with open(defaults_path, 'r') as f: + defaults_content = f.read() + +missing_defaults = [ + ('DEFAULT_ALARM_DIGITAL_CLOCK_FONT_SIZE', '70', 'int'), + ('DEFAULT_DISPLAY_ALARM_SECOND_HAND', 'true', 'boolean'), + ('DEFAULT_TIMER_AUTO_SILENCE_DURATION', '30', 'int'), + ('DEFAULT_TIMER_ADD_TIME_BUTTON_VALUE', '60', 'int'), + ('DEFAULT_TIMER_AUTO_SILENCE', '"30"', 'String'), + ('DEFAULT_TIME_TO_ADD_TO_TIMER', '"1"', 'String'), + ('DEFAULT_ALARM_VOLUME_CRESCENDO_DURATION', '0', 'int'), + ('DEFAULT_TIMER_VOLUME_CRESCENDO_DURATION', '0', 'int') +] + +for key, val, type_name in missing_defaults: + if key not in defaults_content: + defaults_content = defaults_content.replace('}', f' public static final {type_name} {key} = {val};\n}}') + +with open(defaults_path, 'w') as f: + f.write(defaults_content) diff --git a/fix_viewholder_v2.py b/fix_viewholder_v2.py new file mode 100644 index 000000000..36b9e3703 --- /dev/null +++ b/fix_viewholder_v2.py @@ -0,0 +1,47 @@ +import sys + +path = 'app/src/main/java/com/best/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java' +with open(path, 'r') as f: + content = f.read() + +# Add missing initialization in constructor +if 'holidayOption = itemView.findViewById(R.id.holiday_option);' not in content: + content = content.replace('duplicate = itemView.findViewById(R.id.duplicate);', + 'duplicate = itemView.findViewById(R.id.duplicate);\n holidayOption = itemView.findViewById(R.id.holiday_option);') + +# Fix bindHolidayOption +if 'private void bindHolidayOption' not in content: + bind_holiday_method = ''' + private void bindHolidayOption(Context context, Alarm alarm) { + holidayOption.setVisibility(VISIBLE); + holidayOption.setTypeface(mGeneralTypeface); + switch (alarm.holidayOption) { + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_SKIP_HOLIDAY -> + holidayOption.setText(context.getString(R.string.holiday_option_skip_holiday)); + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_BIG_SMALL_DA -> + holidayOption.setText(context.getString(R.string.holiday_option_big_small_da)); + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_BIG_SMALL_XIAO -> + holidayOption.setText(context.getString(R.string.holiday_option_big_small_xiao)); + case com.best.deskclock.holiday.HolidayUtils.HOLIDAY_OPTION_SINGLE_DAY_OFF -> + holidayOption.setText(context.getString(R.string.holiday_option_single_day_off)); + default -> holidayOption.setText(context.getString(R.string.holiday_option_none)); + } + } +''' + # Insert before onAnimateChange + content = content.replace(' @Override\n public Animator onAnimateChange', bind_holiday_method + '\n @Override\n public Animator onAnimateChange') + +# Call bindHolidayOption in onBindItemView +if 'bindHolidayOption(context, alarm);' not in content: + content = content.replace('bindDeleteAndDuplicateButtons();', 'bindDeleteAndDuplicateButtons();\n bindHolidayOption(context, alarm);') + +# Add click listener for holiday option in constructor +if 'holidayOption.setOnClickListener' not in content: + content = content.replace('duplicate.setOnClickListener(v -> {', + '''holidayOption.setOnClickListener(v -> + getAlarmTimeClickHandler().onHolidayOptionClicked(getItemHolder().item)); + + duplicate.setOnClickListener(v -> {''') + +with open(path, 'w') as f: + f.write(content) diff --git a/update_alarm_settings.py b/update_alarm_settings.py new file mode 100644 index 000000000..5c45d1e9c --- /dev/null +++ b/update_alarm_settings.py @@ -0,0 +1,49 @@ +import sys + +path = 'app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java' +with open(path, 'r') as f: + lines = f.readlines() + +new_lines = [] +for line in lines: + if line.strip() == 'import com.best.deskclock.holiday.HolidayRepository;': + continue + if line.startswith('package com.best.deskclock.settings;'): + new_lines.append(line) + new_lines.append('import com.best.deskclock.holiday.HolidayRepository;\n') + else: + new_lines.append(line) + +# Add holiday preference logic +end_of_on_create = -1 +for i, line in enumerate(new_lines): + if 'mDeleteOccasionalAlarmByDefaultPref.setOnPreferenceChangeListener(this);' in line: + end_of_on_create = i + 1 + break + +if end_of_on_create != -1: + holiday_logic = ''' + Preference updateHolidayDataPref = findPreference(KEY_UPDATE_HOLIDAY_DATA); + if (updateHolidayDataPref != null) { + updateHolidayDataPref.setOnPreferenceClickListener(preference -> { + HolidayRepository.getInstance(requireContext()).updateWorkdayData(); + return true; + }); + } + + mHolidayDataUrlPref = findPreference(KEY_HOLIDAY_DATA_URL); + if (mHolidayDataUrlPref != null) { + mHolidayDataUrlPref.setSummary(SettingsDAO.getHolidayDataUrl(mPrefs)); + mHolidayDataUrlPref.setOnPreferenceChangeListener(this); + } +''' + new_lines.insert(end_of_on_create, holiday_logic) + +# Add field declaration +for i, line in enumerate(new_lines): + if 'private CustomSwitchPreference mDeleteOccasionalAlarmByDefaultPref;' in line: + new_lines.insert(i + 1, ' private androidx.preference.EditTextPreference mHolidayDataUrlPref;\n') + break + +with open(path, 'w') as f: + f.writelines(new_lines)