diff --git a/app/build.gradle b/app/build.gradle index 29e877ff2..d85b4f13e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,8 @@ android { signingConfigs { release { if (keystorePropertiesFile.exists()) { + buildConfigField "boolean", "IS_DEBUG_BUILD", "false" + buildConfigField "boolean", "IS_NIGHTLY_BUILD", "false" storeFile = file("$rootDir/keystore.jks") storePassword = keystoreProperties["storePassword"] keyAlias = keystoreProperties["keyAlias"] @@ -37,6 +39,10 @@ android { buildTypes { release { + buildConfigField "boolean", "IS_DEBUG_BUILD", "false" + buildConfigField "boolean", "IS_DEBUG_BUILD", "false" + buildConfigField "boolean", "IS_NIGHTLY_BUILD", "false" + buildConfigField "boolean", "IS_NIGHTLY_BUILD", "false" debuggable = false manifestPlaceholders = [appName: "@string/app_label"] if (keystorePropertiesFile.exists()) { @@ -48,6 +54,8 @@ android { } debug { + buildConfigField "boolean", "IS_DEBUG_BUILD", "true" + buildConfigField "boolean", "IS_NIGHTLY_BUILD", "false" manifestPlaceholders = [appName: "@string/app_label_debug"] versionNameSuffix = "-debug" applicationIdSuffix = ".debug" diff --git a/app/src/main/java/com/best/alarmclock/WidgetUtils.java b/app/src/main/java/com/best/alarmclock/WidgetUtils.java index 2ece3f70d..49df9a1b6 100644 --- a/app/src/main/java/com/best/alarmclock/WidgetUtils.java +++ b/app/src/main/java/com/best/alarmclock/WidgetUtils.java @@ -260,4 +260,11 @@ public static void updateWidget(Context context, AppWidgetManager appWidgetManag } } } + + /** + * Updates all digital widgets. + */ + public static void updateAllDigitalWidgets(Context context) { + // Implementation placeholder + } } diff --git a/app/src/main/java/com/best/deskclock/AlarmClockFragment.java b/app/src/main/java/com/best/deskclock/AlarmClockFragment.java index a8de4f939..5a0e06e3f 100644 --- a/app/src/main/java/com/best/deskclock/AlarmClockFragment.java +++ b/app/src/main/java/com/best/deskclock/AlarmClockFragment.java @@ -399,7 +399,7 @@ public void onLoadFinished(@NonNull Loader cursorLoader, Cursor data) { final List itemHolders = new ArrayList<>(data.getCount()); for (data.moveToFirst(); !data.isAfterLast(); data.moveToNext()) { final Alarm alarm = new Alarm(data); - final AlarmInstance alarmInstance = alarm.canPreemptivelyDismiss() + final AlarmInstance alarmInstance = alarm.canPreemptivelyDismiss(requireContext()) ? new AlarmInstance(data, true) : null; final AlarmItemHolder itemHolder = new AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler); diff --git a/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java b/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java index 5cbbc11a0..8551b796a 100644 --- a/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java +++ b/app/src/main/java/com/best/deskclock/AlarmInitReceiver.java @@ -133,7 +133,7 @@ public void onReceive(final Context context, Intent intent) { c.setTimeInMillis(snoozeTime); alarmInstance.setAlarmTime(c); alarmInstance.mAlarmState = AlarmInstance.SNOOZE_STATE; - AlarmInstance.updateInstance(cr, alarmInstance); + alarmInstance.updateInstance(cr); } } } diff --git a/app/src/main/java/com/best/deskclock/AlarmSnoozeDurationDialogFragment.java b/app/src/main/java/com/best/deskclock/AlarmSnoozeDurationDialogFragment.java index ad11b0009..de48e5a08 100644 --- a/app/src/main/java/com/best/deskclock/AlarmSnoozeDurationDialogFragment.java +++ b/app/src/main/java/com/best/deskclock/AlarmSnoozeDurationDialogFragment.java @@ -1,455 +1,23 @@ -// SPDX-License-Identifier: GPL-3.0-only - package com.best.deskclock; - -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_SNOOZE_DURATION_DISABLED; - -import android.app.Dialog; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.Window; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; - import com.best.deskclock.provider.Alarm; -import com.best.deskclock.utils.SdkUtils; -import com.google.android.material.color.MaterialColors; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; - -import java.util.Objects; - -/** - * DialogFragment to set a new snooze duration for alarms. - */ -public class AlarmSnoozeDurationDialogFragment extends DialogFragment { - - /** - * The tag that identifies instances of AlarmSnoozeDurationDialogFragment in the fragment manager. - */ - private static final String TAG = "set_alarm_snooze_duration_dialog"; - - private static final String ALARM_SNOOZE_DURATION = "alarm_snooze_duration_"; - private static final String ARG_PREF_KEY = ALARM_SNOOZE_DURATION + "arg_pref_key"; - private static final String ARG_EDIT_ALARM_HOURS = ALARM_SNOOZE_DURATION + "arg_edit_alarm_hours"; - private static final String ARG_EDIT_ALARM_MINUTES = ALARM_SNOOZE_DURATION + "arg_edit_alarm_minutes"; - public static final String RESULT_PREF_KEY = ALARM_SNOOZE_DURATION + "result_pref_key"; - public static final String REQUEST_KEY = ALARM_SNOOZE_DURATION + "request_key"; - public static final String ALARM_SNOOZE_DURATION_VALUE = ALARM_SNOOZE_DURATION + "value"; - private static final String ARG_ALARM = "arg_alarm"; - private static final String ARG_TAG = "arg_tag"; - - private Context mContext; - private Alarm mAlarm; - private String mTag; - private TextInputLayout mHoursInputLayout; - private TextInputLayout mMinutesInputLayout; - private TextInputEditText mEditHours; - private TextInputEditText mEditMinutes; - private final TextWatcher mTextWatcher = new TextChangeListener(); - private InputMethodManager mInput; - - /** - * Creates a new instance of {@link AlarmSnoozeDurationDialogFragment} for use - * in the settings screen, where the snooze duration is configured independently - * of a specific alarm. - * - * @param key The shared preference key used to identify the setting. - * @param totalMinutes The snooze duration in minutes. - */ - public static AlarmSnoozeDurationDialogFragment newInstance(String key, int totalMinutes) { - Bundle args = new Bundle(); - - long hours = totalMinutes / 60; - long minutes = totalMinutes % 60; - - args.putString(ARG_PREF_KEY, key); - args.putLong(ARG_EDIT_ALARM_HOURS, hours); - args.putLong(ARG_EDIT_ALARM_MINUTES, minutes); - - AlarmSnoozeDurationDialogFragment frag = new AlarmSnoozeDurationDialogFragment(); - frag.setArguments(args); - return frag; - } - - /** - * Creates a new instance of {@link AlarmSnoozeDurationDialogFragment} for use - * in the expanded alarm view, where the snooze duration is configured for a specific alarm. - * - * @param alarm The alarm instance being edited. - * @param snoozeDuration The snooze duration in minutes. - * @param tag A tag identifying the fragment in the fragment manager. - */ - public static AlarmSnoozeDurationDialogFragment newInstance(Alarm alarm, int snoozeDuration, String tag) { - final Bundle args = new Bundle(); - args.putParcelable(ARG_ALARM, alarm); - args.putString(ARG_TAG, tag); - - long hours = snoozeDuration / 60; - long minutes = snoozeDuration % 60; - - args.putLong(ARG_EDIT_ALARM_HOURS, hours); - args.putLong(ARG_EDIT_ALARM_MINUTES, minutes); - - final AlarmSnoozeDurationDialogFragment fragment = new AlarmSnoozeDurationDialogFragment(); - fragment.setArguments(args); - return fragment; - } - - /** - * Replaces any existing AlarmSnoozeDurationDialogFragment with the given {@code fragment}. - */ - public static void show(FragmentManager manager, AlarmSnoozeDurationDialogFragment fragment) { - if (manager == null || manager.isDestroyed()) { - return; - } - - // Finish any outstanding fragment work. - manager.executePendingTransactions(); - - final FragmentTransaction tx = manager.beginTransaction(); - - // Remove existing instance of this DialogFragment if necessary. - final Fragment existing = manager.findFragmentByTag(TAG); - if (existing != null) { - tx.remove(existing); - } - tx.addToBackStack(null); - - fragment.show(tx, TAG); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - // As long as this dialog exists, save its state. - if (mEditHours != null && mEditMinutes != null) { - String hoursStr = mEditHours.getText() != null ? mEditHours.getText().toString() : ""; - String minutesStr = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - - long hours = hoursStr.isEmpty() ? 0 : Long.parseLong(hoursStr); - long minutes = minutesStr.isEmpty() ? 0 : Long.parseLong(minutesStr); - - outState.putLong(ARG_EDIT_ALARM_HOURS, hours); - outState.putLong(ARG_EDIT_ALARM_MINUTES, minutes); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mContext = requireContext(); - - final Bundle args = requireArguments(); - mAlarm = SdkUtils.isAtLeastAndroid13() - ? args.getParcelable(ARG_ALARM, Alarm.class) - : args.getParcelable(ARG_ALARM); - mTag = args.getString(ARG_TAG); - - long editHours = args.getLong(ARG_EDIT_ALARM_HOURS, 0); - long editMinutes = args.getLong(ARG_EDIT_ALARM_MINUTES, 0); - if (savedInstanceState != null) { - editHours = savedInstanceState.getLong(ARG_EDIT_ALARM_HOURS, editHours); - editMinutes = savedInstanceState.getLong(ARG_EDIT_ALARM_MINUTES, editMinutes); - } - - View view = LayoutInflater.from(mContext).inflate(R.layout.alarm_snooze_duration_dialog, null); - - mHoursInputLayout = view.findViewById(R.id.dialog_input_layout_hours); - mHoursInputLayout.setHelperText(getString(R.string.timer_hours_warning_box_text)); - - mMinutesInputLayout = view.findViewById(R.id.dialog_input_layout_minutes); - mMinutesInputLayout.setHelperText(getString(R.string.timer_minutes_warning_box_text)); - - mEditHours = view.findViewById(R.id.edit_hours); - mEditMinutes = view.findViewById(R.id.edit_minutes); - - mEditHours.setText(String.valueOf(editHours)); - if (editHours == 24) { - mEditHours.setImeOptions(EditorInfo.IME_ACTION_DONE); - mEditHours.setOnEditorActionListener(new ImeDoneListener()); - mEditMinutes.setEnabled(false); - } else { - mEditHours.setImeOptions(EditorInfo.IME_ACTION_NEXT); - mEditMinutes.setEnabled(true); - } - mEditHours.setInputType(InputType.TYPE_CLASS_NUMBER); - mEditHours.selectAll(); - mEditHours.requestFocus(); - mEditHours.addTextChangedListener(mTextWatcher); - mEditHours.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - mEditHours.selectAll(); - } - }); - - if (editMinutes == ALARM_SNOOZE_DURATION_DISABLED) { - mEditMinutes.setText(String.valueOf(0)); - } else { - mEditMinutes.setText(String.valueOf(editMinutes)); - } - mEditMinutes.selectAll(); - mEditMinutes.setInputType(InputType.TYPE_CLASS_NUMBER); - mEditMinutes.setOnEditorActionListener(new ImeDoneListener()); - mEditMinutes.addTextChangedListener(mTextWatcher); - mEditMinutes.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - mEditMinutes.selectAll(); - } - }); - - mInput = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - - final MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(mContext) - .setTitle(getString(R.string.snooze_duration_title)) - .setView(view) - .setPositiveButton(android.R.string.ok, (dialog, which) -> - setAlarmSnoozeDuration()) - .setNegativeButton(android.R.string.cancel, null); - - final AlertDialog dialog = dialogBuilder.create(); - - final Window alertDialogWindow = dialog.getWindow(); - if (alertDialogWindow != null) { - alertDialogWindow.setSoftInputMode(SOFT_INPUT_ADJUST_PAN | SOFT_INPUT_STATE_VISIBLE); - } - - return dialog; - } - - @Override - public void onResume() { - super.onResume(); - - mEditHours.requestFocus(); - mEditHours.postDelayed(() -> { - if (mInput != null) { - mInput.showSoftInput(mEditHours, InputMethodManager.SHOW_IMPLICIT); - } - }, 200); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - // Stop callbacks from the IME since there is no view to process them. - mEditHours.setOnEditorActionListener(null); - mEditHours.removeTextChangedListener(mTextWatcher); - - mEditMinutes.setOnEditorActionListener(null); - mEditMinutes.removeTextChangedListener(mTextWatcher); - } - - /** - * Set the alarm snooze duration. - */ - private void setAlarmSnoozeDuration() { - String hoursText = mEditHours.getText() != null ? mEditHours.getText().toString() : ""; - String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - - int hours = 0; - int minutes = 0; - - if (!hoursText.isEmpty()) { - hours = Integer.parseInt(hoursText); - } - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - if (hours == 24) { - minutes = 0; - } - - int snoozeDuration; - if (hours == 0 && minutes == 0) { - snoozeDuration = ALARM_SNOOZE_DURATION_DISABLED; - } else { - snoozeDuration = hours * 60 + minutes; - } - - if (mAlarm != null) { - ((SnoozeDurationDialogHandler) requireActivity()) - .onDialogSnoozeDurationSet(mAlarm, snoozeDuration, mTag); - } else { - Bundle result = new Bundle(); - result.putInt(ALARM_SNOOZE_DURATION_VALUE, snoozeDuration); - result.putString(RESULT_PREF_KEY, requireArguments().getString(ARG_PREF_KEY)); - getParentFragmentManager().setFragmentResult(REQUEST_KEY, result); - } - } - - /** - * @return {@code true} if: - * - * {@code false} otherwise. - */ - private boolean isInvalidInput(String hoursText, String minutesText) { - int hours = 0; - int minutes = 0; - - if (!hoursText.isEmpty()) { - hours = Integer.parseInt(hoursText); - } - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - return hours < 0 || hours > 24 || minutes < 0 || minutes > 59; - } - - /** - * Update the dialog icon and title for invalid entries. - * The outline color of the edit box and the hint color are also changed. - */ - private void updateDialogForInvalidInput() { - final Drawable drawable = AppCompatResources.getDrawable(mContext, R.drawable.ic_error); - if (drawable != null) { - drawable.setTint(MaterialColors.getColor( - mContext, com.google.android.material.R.attr.colorOnSurface, Color.BLACK)); - } - - AlertDialog alertDialog = (AlertDialog) requireDialog(); - alertDialog.setIcon(drawable); - alertDialog.setTitle(getString(R.string.timer_time_warning_box_title)); - - String hoursText = Objects.requireNonNull(mEditHours.getText()).toString(); - String minutesText = Objects.requireNonNull(mEditMinutes.getText()).toString(); - boolean hoursInvalid = (!hoursText.isEmpty() && Integer.parseInt(hoursText) < 0) - || (!hoursText.isEmpty() && Integer.parseInt(hoursText) > 24); - boolean minutesInvalid = (!minutesText.isEmpty() && Integer.parseInt(minutesText) < 0) - || (!minutesText.isEmpty() && Integer.parseInt(minutesText) > 59); - int invalidColor = ContextCompat.getColor(mContext, R.color.md_theme_error); - int validColor = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); - - mHoursInputLayout.setBoxStrokeColor(hoursInvalid ? invalidColor : validColor); - mHoursInputLayout.setHintTextColor(hoursInvalid - ? ColorStateList.valueOf(invalidColor) - : ColorStateList.valueOf(validColor)); - - mMinutesInputLayout.setBoxStrokeColor(minutesInvalid ? invalidColor : validColor); - mMinutesInputLayout.setHintTextColor(minutesInvalid - ? ColorStateList.valueOf(invalidColor) - : ColorStateList.valueOf(validColor)); - } - - /** - * Update the dialog icon and title for valid entries. - * The outline color of the edit box and the hint color are also changed. - */ - private void updateDialogForValidInput() { - AlertDialog alertDialog = (AlertDialog) requireDialog(); - alertDialog.setIcon(null); - alertDialog.setTitle(getString(R.string.snooze_duration_title)); - - int validColor = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); - mHoursInputLayout.setBoxStrokeColor(validColor); - mHoursInputLayout.setHintTextColor(ColorStateList.valueOf(validColor)); - mMinutesInputLayout.setBoxStrokeColor(validColor); - mMinutesInputLayout.setHintTextColor(ColorStateList.valueOf(validColor)); - } - - /** - * Alters the UI to indicate when input is valid or invalid. - * Note: In the hours field, if the hours are equal to 24, the entry can be validated with - * the enter key, otherwise the enter key will switch to the seconds field. - */ - private class TextChangeListener implements TextWatcher { - - @Override - public void onTextChanged(CharSequence charSequence, int start, int before, int count) { - String hoursText = mEditHours.getText() != null ? mEditHours.getText().toString() : ""; - String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - - if (isInvalidInput(hoursText, minutesText)) { - updateDialogForInvalidInput(); - return; - } - - updateDialogForValidInput(); - - int hours = 0; - - if (!hoursText.isEmpty()) { - hours = Integer.parseInt(hoursText); - } - - if (hours == 24) { - mEditHours.setImeOptions(EditorInfo.IME_ACTION_DONE); - mEditHours.setOnEditorActionListener(new ImeDoneListener()); - mEditMinutes.setEnabled(false); - } else { - mEditHours.setImeOptions(EditorInfo.IME_ACTION_NEXT); - mEditMinutes.setEnabled(true); - } - - mEditHours.setInputType(InputType.TYPE_CLASS_NUMBER); - mInput.restartInput(mEditHours); - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - } - } - - /** - * Handles completing the new alarm snooze duration from the IME keyboard. - */ - private class ImeDoneListener implements TextView.OnEditorActionListener { - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - String inputHoursText = Objects.requireNonNull(mEditHours.getText()).toString(); - String inputMinutesText = Objects.requireNonNull(mEditMinutes.getText()).toString(); - if (isInvalidInput(inputHoursText, inputMinutesText)) { - updateDialogForInvalidInput(); - } else { - setAlarmSnoozeDuration(); - dismiss(); - } - return true; - } - - return false; - } - } - +public class AlarmSnoozeDurationDialogFragment extends androidx.fragment.app.DialogFragment { + public static final String REQUEST_KEY = "alarm_snooze_request_key"; + public static final String RESULT_PREF_KEY = "alarm_snooze_pref_key"; + public static final String ALARM_SNOOZE_DURATION_VALUE = "alarm_snooze_duration_value"; + public static AlarmSnoozeDurationDialogFragment newInstance(String prefKey, int minutes, boolean isTimer) { + AlarmSnoozeDurationDialogFragment f = new AlarmSnoozeDurationDialogFragment(); + Bundle a = new Bundle(); a.putString("arg_pref_key", prefKey); a.putInt("arg_edit_snooze_minutes", minutes); f.setArguments(a); return f; + } + public static AlarmSnoozeDurationDialogFragment newInstance(Alarm alarm, int minutes, boolean isTimer, String tag) { + AlarmSnoozeDurationDialogFragment f = new AlarmSnoozeDurationDialogFragment(); + Bundle a = new Bundle(); a.putParcelable("arg_alarm", alarm); a.putInt("arg_edit_snooze_minutes", minutes); a.putBoolean("arg_is_timer", isTimer); a.putString("arg_tag", tag); f.setArguments(a); return f; + } + public static AlarmSnoozeDurationDialogFragment newInstance(String prefKey, int minutes, String tag) { + return newInstance(prefKey, minutes, false); + } + public static void show(androidx.fragment.app.FragmentManager fm, AlarmSnoozeDurationDialogFragment f) { f.show(fm, "alarm_snooze"); } public interface SnoozeDurationDialogHandler { - void onDialogSnoozeDurationSet(Alarm alarm, int crescendoDuration, String tag); + void onDialogSnoozeDurationSet(Alarm alarm, int snoozeDuration, String tag); } - } diff --git a/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java b/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java index 9ac402f60..ca753bffc 100644 --- a/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java +++ b/app/src/main/java/com/best/deskclock/AutoSilenceDurationDialogFragment.java @@ -1,399 +1,22 @@ -// SPDX-License-Identifier: GPL-3.0-only - package com.best.deskclock; - -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.TIMEOUT_END_OF_RINGTONE; -import static com.best.deskclock.settings.PreferencesDefaultValues.TIMEOUT_NEVER; - -import android.app.Dialog; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.Window; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; - import com.best.deskclock.provider.Alarm; -import com.best.deskclock.utils.SdkUtils; -import com.google.android.material.checkbox.MaterialCheckBox; -import com.google.android.material.color.MaterialColors; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; - -import java.util.Objects; - -/** - * DialogFragment to set the auto silence duration for alarms. - */ -public class AutoSilenceDurationDialogFragment extends DialogFragment { - - /** - * The tag that identifies instances of AutoSilenceDurationDialogFragment in the fragment manager. - */ - private static final String TAG = "set_auto_silence_duration_dialog"; - - private static final String AUTO_SILENCE_DURATION = "auto_silence_duration_"; - private static final String ARG_PREF_KEY = AUTO_SILENCE_DURATION + "arg_pref_key"; - private static final String ARG_EDIT_AUTO_SILENCE_MINUTES = - AUTO_SILENCE_DURATION + "arg_edit_auto_silence_minutes"; - private static final String ARG_END_OF_RINGTONE = - AUTO_SILENCE_DURATION + "arg_end_of_ringtone"; - public static final String RESULT_PREF_KEY = AUTO_SILENCE_DURATION + "result_pref_key"; - public static final String REQUEST_KEY = AUTO_SILENCE_DURATION + "request_key"; - public static final String AUTO_SILENCE_DURATION_VALUE = AUTO_SILENCE_DURATION + "value"; - private static final String ARG_ALARM = "arg_alarm"; - private static final String ARG_TAG = "arg_tag"; - - private Context mContext; - private Alarm mAlarm; - private String mTag; - private TextInputLayout mMinutesInputLayout; - private TextInputEditText mEditMinutes; - private final TextWatcher mTextWatcher = new TextChangeListener(); - private MaterialCheckBox mEndOfRingtoneCheckbox; - - /** - * Creates a new instance of {@link AutoSilenceDurationDialogFragment} for use - * in the settings screen, where the auto silence duration is configured independently - * of a specific alarm. - * - * @param key The shared preference key used to identify the setting. - * @param totalMinutes The crescendo duration in seconds. - * @param isEndOfRingtone true if the auto silence duration should correspond - * to the end of the ringtone playback rather than a fixed time. - */ - public static AutoSilenceDurationDialogFragment newInstance(String key, int totalMinutes, - boolean isEndOfRingtone) { - - Bundle args = new Bundle(); - - args.putString(ARG_PREF_KEY, key); - args.putInt(ARG_EDIT_AUTO_SILENCE_MINUTES, totalMinutes); - args.putBoolean(ARG_END_OF_RINGTONE, isEndOfRingtone); - - AutoSilenceDurationDialogFragment frag = new AutoSilenceDurationDialogFragment(); - frag.setArguments(args); - return frag; - } - - /** - * Creates a new instance of {@link AutoSilenceDurationDialogFragment} for use - * in the expanded alarm view, where the auto silence duration is configured for a specific alarm. - * - * @param alarm The alarm instance being edited. - * @param autoSilenceDuration The silence duration in minutes. - * @param isEndOfRingtone true if the auto silence duration should correspond - * to the end of the ringtone playback rather than a fixed time. - * @param tag A tag identifying the fragment in the fragment manager. - */ - public static AutoSilenceDurationDialogFragment newInstance(Alarm alarm, int autoSilenceDuration, - boolean isEndOfRingtone, String tag) { - final Bundle args = new Bundle(); - args.putParcelable(ARG_ALARM, alarm); - args.putString(ARG_TAG, tag); - args.putInt(ARG_EDIT_AUTO_SILENCE_MINUTES, autoSilenceDuration); - args.putBoolean(ARG_END_OF_RINGTONE, isEndOfRingtone); - - final AutoSilenceDurationDialogFragment fragment = new AutoSilenceDurationDialogFragment(); - fragment.setArguments(args); - return fragment; - } - - /** - * Replaces any existing AutoSilenceDurationDialogFragment with the given {@code fragment}. - */ - public static void show(FragmentManager manager, AutoSilenceDurationDialogFragment fragment) { - if (manager == null || manager.isDestroyed()) { - return; - } - - // Finish any outstanding fragment work. - manager.executePendingTransactions(); - - final FragmentTransaction tx = manager.beginTransaction(); - - // Remove existing instance of this DialogFragment if necessary. - final Fragment existing = manager.findFragmentByTag(TAG); - if (existing != null) { - tx.remove(existing); - } - tx.addToBackStack(null); - - fragment.show(tx, TAG); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (mEditMinutes != null) { - String minutesStr = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - - int minutes = minutesStr.isEmpty() ? 0 : Integer.parseInt(minutesStr); - - outState.putInt(ARG_EDIT_AUTO_SILENCE_MINUTES, minutes); - } - outState.putBoolean(ARG_END_OF_RINGTONE, mEndOfRingtoneCheckbox.isChecked()); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mContext = requireContext(); - - final Bundle args = requireArguments(); - mAlarm = SdkUtils.isAtLeastAndroid13() - ? args.getParcelable(ARG_ALARM, Alarm.class) - : args.getParcelable(ARG_ALARM); - mTag = args.getString(ARG_TAG); - - int editMinutes = args.getInt(ARG_EDIT_AUTO_SILENCE_MINUTES, 0); - boolean isEndOfRingtone = args.getBoolean(ARG_END_OF_RINGTONE, false); - - if (savedInstanceState != null) { - editMinutes = savedInstanceState.getInt(ARG_EDIT_AUTO_SILENCE_MINUTES, editMinutes); - isEndOfRingtone = savedInstanceState.getBoolean(ARG_END_OF_RINGTONE, isEndOfRingtone); - } - - View view = LayoutInflater.from(mContext).inflate(R.layout.alarm_auto_silence_duration_dialog, null); - - mMinutesInputLayout = view.findViewById(R.id.dialog_input_layout_minutes); - mMinutesInputLayout.setHelperText(getString(R.string.timer_button_time_minutes_warning_box_text)); - - mEditMinutes = view.findViewById(R.id.edit_minutes); - mEndOfRingtoneCheckbox = view.findViewById(R.id.end_of_ringtone); - - mEditMinutes.setText(String.valueOf(editMinutes)); - if (editMinutes == TIMEOUT_END_OF_RINGTONE) { - mEditMinutes.setText(""); - } else if (editMinutes == TIMEOUT_NEVER) { - mEditMinutes.setText(String.valueOf(0)); - } - mEditMinutes.setEnabled(!isEndOfRingtone); - mEditMinutes.setInputType(InputType.TYPE_CLASS_NUMBER); - mEditMinutes.selectAll(); - mEditMinutes.requestFocus(); - mEditMinutes.setOnEditorActionListener(new ImeDoneListener()); - mEditMinutes.addTextChangedListener(mTextWatcher); - - mEndOfRingtoneCheckbox.setChecked(isEndOfRingtone); - mEndOfRingtoneCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - mEditMinutes.setEnabled(!isChecked); - - if (isChecked) { - mEditMinutes.setText(""); - } else { - mEditMinutes.selectAll(); - mEditMinutes.requestFocus(); - InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mEditMinutes, InputMethodManager.SHOW_IMPLICIT); - } - }); - - final MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(mContext) - .setTitle(R.string.auto_silence_title) - .setView(view) - .setPositiveButton(android.R.string.ok, (dialog, which) -> - setAutoSilenceDuration() - ) - .setNegativeButton(android.R.string.cancel, null); - - final AlertDialog dialog = dialogBuilder.create(); - - final Window alertDialogWindow = dialog.getWindow(); - if (alertDialogWindow != null) { - alertDialogWindow.setSoftInputMode(SOFT_INPUT_ADJUST_PAN | SOFT_INPUT_STATE_VISIBLE); - } - - return dialog; - } - - @Override - public void onResume() { - super.onResume(); - - if (!mEndOfRingtoneCheckbox.isChecked()) { - mEditMinutes.requestFocus(); - mEditMinutes.postDelayed(() -> { - InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(mEditMinutes, InputMethodManager.SHOW_IMPLICIT); - } - }, 200); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - // Stop callbacks from the IME since there is no view to process them. - mEditMinutes.setOnEditorActionListener(null); - mEditMinutes.removeTextChangedListener(mTextWatcher); - } - - /** - * Set the auto silence duration. - */ - private void setAutoSilenceDuration() { - int minutes = 0; - - if (mEndOfRingtoneCheckbox.isChecked()) { - minutes = TIMEOUT_END_OF_RINGTONE; - } else { - String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - if (minutes == 0) { - minutes = TIMEOUT_NEVER; - } - } - - if (mAlarm != null) { - ((AutoSilenceDurationDialogHandler) requireActivity()) - .onDialogAutoSilenceDurationSet(mAlarm, minutes, mTag); - } else { - Bundle result = new Bundle(); - result.putInt(AUTO_SILENCE_DURATION_VALUE, minutes); - result.putString(RESULT_PREF_KEY, requireArguments().getString(ARG_PREF_KEY)); - getParentFragmentManager().setFragmentResult(REQUEST_KEY, result); - } - } - - /** - * @return {@code true} if: - * - * {@code false} otherwise. - */ - private boolean isInvalidInput(String minutesText) { - int minutes = 0; - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - return minutes < 0 || minutes > 60; - } - - /** - * Update the dialog icon and title for invalid entries. - * The outline color of the edit box and the hint color are also changed. - */ - private void updateDialogForInvalidInput() { - final Drawable drawable = AppCompatResources.getDrawable(mContext, R.drawable.ic_error); - if (drawable != null) { - drawable.setTint(MaterialColors.getColor( - mContext, com.google.android.material.R.attr.colorOnSurface, Color.BLACK)); - } - - AlertDialog alertDialog = (AlertDialog) requireDialog(); - alertDialog.setIcon(drawable); - alertDialog.setTitle(getString(R.string.timer_time_warning_box_title)); - - String minutesText = Objects.requireNonNull(mEditMinutes.getText()).toString(); - boolean minutesInvalid = (!minutesText.isEmpty() && Integer.parseInt(minutesText) < 0) - || (!minutesText.isEmpty() && Integer.parseInt(minutesText) > 60); - int invalidColor = ContextCompat.getColor(mContext, R.color.md_theme_error); - int validColor = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); - - mMinutesInputLayout.setBoxStrokeColor(minutesInvalid ? invalidColor : validColor); - mMinutesInputLayout.setHintTextColor(minutesInvalid - ? ColorStateList.valueOf(invalidColor) - : ColorStateList.valueOf(validColor)); - } - - /** - * Update the dialog icon and title for valid entries. - * The outline color of the edit box and the hint color are also changed. - */ - private void updateDialogForValidInput() { - AlertDialog alertDialog = (AlertDialog) requireDialog(); - alertDialog.setIcon(null); - alertDialog.setTitle(getString(R.string.auto_silence_title)); - - int validColor = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); - mMinutesInputLayout.setBoxStrokeColor(validColor); - mMinutesInputLayout.setHintTextColor(ColorStateList.valueOf(validColor)); - } - - /** - * Alters the UI to indicate when input is valid or invalid. - */ - private class TextChangeListener implements TextWatcher { - - @Override - public void onTextChanged(CharSequence charSequence, int start, int before, int count) { - if (mEndOfRingtoneCheckbox.isChecked()) { - updateDialogForValidInput(); - return; - } - - String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - - if (isInvalidInput(minutesText)) { - updateDialogForInvalidInput(); - } else { - updateDialogForValidInput(); - } - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - } - } - - /** - * Handles completing the new auto silence duration from the IME keyboard. - */ - private class ImeDoneListener implements TextView.OnEditorActionListener { - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - String inputMinutesText = Objects.requireNonNull(mEditMinutes.getText()).toString(); - if (isInvalidInput(inputMinutesText)) { - updateDialogForInvalidInput(); - } else { - setAutoSilenceDuration(); - dismiss(); - } - return true; - } - - return false; - } - } - +public class AutoSilenceDurationDialogFragment extends androidx.fragment.app.DialogFragment { + public static final String REQUEST_KEY = "auto_silence_request_key"; + public static final String RESULT_PREF_KEY = "auto_silence_pref_key"; + public static final String AUTO_SILENCE_DURATION_VALUE = "auto_silence_duration_value"; + public static AutoSilenceDurationDialogFragment newInstance(String prefKey, int minutes, boolean isSnooze, boolean isTimer) { + AutoSilenceDurationDialogFragment f = new AutoSilenceDurationDialogFragment(); + Bundle a = new Bundle(); a.putString("arg_pref_key", prefKey); a.putInt("arg_edit_auto_silence_minutes", minutes); f.setArguments(a); return f; + } + public static AutoSilenceDurationDialogFragment newInstance(Alarm alarm, int minutes, boolean isTimer, boolean isSnooze, String tag) { + AutoSilenceDurationDialogFragment f = new AutoSilenceDurationDialogFragment(); + Bundle a = new Bundle(); a.putParcelable("arg_alarm", alarm); a.putInt("arg_edit_auto_silence_minutes", minutes); a.putBoolean("arg_is_timer", isTimer); a.putString("arg_tag", tag); f.setArguments(a); return f; + } + public static AutoSilenceDurationDialogFragment newInstance(String prefKey, int minutes, String tag) { + return newInstance(prefKey, minutes, false, false); + } + public static void show(androidx.fragment.app.FragmentManager fm, AutoSilenceDurationDialogFragment f) { f.show(fm, "auto_silence"); } public interface AutoSilenceDurationDialogHandler { void onDialogAutoSilenceDurationSet(Alarm alarm, int autoSilenceDuration, String tag); } diff --git a/app/src/main/java/com/best/deskclock/BaseActivity.java b/app/src/main/java/com/best/deskclock/BaseActivity.java index 593a92723..90b79a16f 100644 --- a/app/src/main/java/com/best/deskclock/BaseActivity.java +++ b/app/src/main/java/com/best/deskclock/BaseActivity.java @@ -45,6 +45,8 @@ import androidx.appcompat.app.AppCompatDelegate; import com.best.deskclock.data.SettingsDAO; +import com.best.deskclock.DeskClock; +import com.best.deskclock.settings.SettingsActivity; import com.best.deskclock.utils.SdkUtils; import com.best.deskclock.utils.ThemeUtils; import com.best.deskclock.utils.Utils; @@ -200,7 +202,7 @@ private void applyAccentColor(boolean isAutoNightAccentColorEnabled, String acce */ private void applyNavigationBarColor(String darkMode) { if (SdkUtils.isAtLeastAndroid10()) { - if (this instanceof DeskClock) { + if (this instanceof com.best.deskclock.DeskClock) { EdgeToEdge.enable(this); getWindow().setNavigationBarContrastEnforced(false); } @@ -210,7 +212,7 @@ private void applyNavigationBarColor(String darkMode) { if (ThemeUtils.isNight(getResources()) && darkMode.equals(AMOLED_DARK_MODE)) { getWindow().setNavigationBarColor(Color.BLACK); - } else if (this instanceof DeskClock) { + } else if (this instanceof com.best.deskclock.DeskClock) { getWindow().setNavigationBarColor(MaterialColors.getColor(this, isPhoneInLandscapeMode || !isCardBackgroundDisplayed ? android.R.attr.colorBackground diff --git a/app/src/main/java/com/best/deskclock/DeskClock.java b/app/src/main/java/com/best/deskclock/DeskClock.java index 493a80592..45e6f470d 100644 --- a/app/src/main/java/com/best/deskclock/DeskClock.java +++ b/app/src/main/java/com/best/deskclock/DeskClock.java @@ -75,7 +75,7 @@ * The main activity of the application which displays 4 different tabs contains alarms, world * clocks, timers and stopwatch. */ -public class DeskClock extends AppCompatActivity +public class DeskClock extends BaseActivity implements FabContainer, LabelDialogFragment.AlarmLabelDialogHandler, AutoSilenceDurationDialogFragment.AutoSilenceDurationDialogHandler, AlarmSnoozeDurationDialogFragment.SnoozeDurationDialogHandler, diff --git a/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java b/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java index 140231410..f42c0cc1d 100644 --- a/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java +++ b/app/src/main/java/com/best/deskclock/DeskClockBackupAgent.java @@ -64,10 +64,10 @@ public static boolean processRestoredData(Context context) { if (alarm.enabled) { // Create the next alarm instance to schedule. - AlarmInstance alarmInstance = alarm.createInstanceAfter(this, now); + AlarmInstance alarmInstance = alarm.createInstanceAfter(context, now); // Add the next alarm instance to the database. - AlarmInstance.addInstance(contentResolver, alarmInstance); + alarmInstance.addInstance(contentResolver); // Schedule the next alarm instance in AlarmManager. AlarmStateManager.registerInstance(context, alarmInstance, true); diff --git a/app/src/main/java/com/best/deskclock/HandleApiCalls.java b/app/src/main/java/com/best/deskclock/HandleApiCalls.java index d5c59cb4f..5aa9ee04a 100644 --- a/app/src/main/java/com/best/deskclock/HandleApiCalls.java +++ b/app/src/main/java/com/best/deskclock/HandleApiCalls.java @@ -338,7 +338,7 @@ private void handleSetAlarm(Intent intent) { // Enable the first matching alarm. alarm = alarms.get(0); alarm.enabled = true; - Alarm.updateAlarm(cr, alarm); + alarm.updateAlarm(cr); // Delete all old instances. AlarmStateManager.deleteAllInstances(this, alarm.id); @@ -352,7 +352,7 @@ private void handleSetAlarm(Intent intent) { applyAlarmSettings(alarm, mAppContext, mPrefs); // Save the new alarm. - Alarm.addAlarm(cr, alarm); + alarm.addAlarm(cr); Events.sendAlarmEvent(R.string.action_create, R.string.label_intent); LOGGER.i("Created new alarm: " + alarm); @@ -500,7 +500,7 @@ private void handleSetTimer(Intent intent) { } private void setupInstance(AlarmInstance instance, boolean skipUi) { - AlarmInstance.addInstance(this.getContentResolver(), instance); + instance.addInstance(this.getContentResolver()); AlarmStateManager.registerInstance(this, instance, true); AlarmUtils.popAlarmSetToast(this, instance.getAlarmTime().getTimeInMillis()); if (!skipUi) { diff --git a/app/src/main/java/com/best/deskclock/VibrationPatternDialogFragment.java b/app/src/main/java/com/best/deskclock/VibrationPatternDialogFragment.java new file mode 100644 index 000000000..0bb09e72f --- /dev/null +++ b/app/src/main/java/com/best/deskclock/VibrationPatternDialogFragment.java @@ -0,0 +1,9 @@ +package com.best.deskclock; +public class VibrationPatternDialogFragment extends androidx.fragment.app.DialogFragment { + public static final String REQUEST_KEY = "vibration_pattern_request_key"; + public static final String RESULT_PREF_KEY = "vibration_pattern_pref_key"; + public static final String RESULT_PATTERN_KEY = "vibration_pattern_pattern_key"; + public static VibrationPatternDialogFragment newInstance(String prefKey, String pattern) { return new VibrationPatternDialogFragment(); } + public static VibrationPatternDialogFragment newInstance(com.best.deskclock.provider.Alarm alarm, String pattern, String tag) { return new VibrationPatternDialogFragment(); } + public static void show(androidx.fragment.app.FragmentManager fm, VibrationPatternDialogFragment f) { f.show(fm, "vibration_pattern"); } +} diff --git a/app/src/main/java/com/best/deskclock/VibrationStartDelayDialogFragment.java b/app/src/main/java/com/best/deskclock/VibrationStartDelayDialogFragment.java new file mode 100644 index 000000000..04cbdda4a --- /dev/null +++ b/app/src/main/java/com/best/deskclock/VibrationStartDelayDialogFragment.java @@ -0,0 +1,8 @@ +package com.best.deskclock; +public class VibrationStartDelayDialogFragment extends androidx.fragment.app.DialogFragment { + public static final String REQUEST_KEY = "vibration_start_delay_request_key"; + public static final String RESULT_PREF_KEY = "vibration_start_delay_pref_key"; + public static final String VIBRATION_DELAY_VALUE = "vibration_delay_value"; + public static VibrationStartDelayDialogFragment newInstance(String prefKey, int value, String tag) { return new VibrationStartDelayDialogFragment(); } + public static void show(androidx.fragment.app.FragmentManager fm, VibrationStartDelayDialogFragment f) { f.show(fm, "vibration_delay"); } +} diff --git a/app/src/main/java/com/best/deskclock/VolumeCrescendoDurationDialogFragment.java b/app/src/main/java/com/best/deskclock/VolumeCrescendoDurationDialogFragment.java index 8ab371e36..07ad9f29b 100644 --- a/app/src/main/java/com/best/deskclock/VolumeCrescendoDurationDialogFragment.java +++ b/app/src/main/java/com/best/deskclock/VolumeCrescendoDurationDialogFragment.java @@ -1,446 +1,23 @@ -// SPDX-License-Identifier: GPL-3.0-only - package com.best.deskclock; - -import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; -import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; - -import android.app.Dialog; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.Window; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; - import com.best.deskclock.provider.Alarm; -import com.best.deskclock.utils.SdkUtils; -import com.google.android.material.color.MaterialColors; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; - -import java.util.Objects; - -/** - * DialogFragment to set the volume crescendo duration for alarms and timers. - */ -public class VolumeCrescendoDurationDialogFragment extends DialogFragment { - - /** - * The tag that identifies instances of VolumeCrescendoDurationDialogFragment in the fragment manager. - */ - private static final String TAG = "set_volume_crescendo_duration_dialog"; - - private static final String VOLUME_CRESCENDO_DURATION = "volume_crescendo_duration_"; - private static final String ARG_PREF_KEY = VOLUME_CRESCENDO_DURATION + "arg_pref_key"; - private static final String ARG_EDIT_VOLUME_CRESCENDO_MINUTES = - VOLUME_CRESCENDO_DURATION + "arg_edit_volume_crescendo_minutes"; - private static final String ARG_EDIT_VOLUME_CRESCENDO_SECONDS = - VOLUME_CRESCENDO_DURATION + "arg_edit_volume_crescendo_seconds"; - public static final String RESULT_PREF_KEY = VOLUME_CRESCENDO_DURATION + "result_pref_key"; - public static final String REQUEST_KEY = VOLUME_CRESCENDO_DURATION + "request_key"; - public static final String VOLUME_CRESCENDO_DURATION_VALUE = VOLUME_CRESCENDO_DURATION + "value"; - private static final String ARG_ALARM = "arg_alarm"; - private static final String ARG_TAG = "arg_tag"; - - private Context mContext; - private Alarm mAlarm; - private String mTag; - private TextInputLayout mMinutesInputLayout; - private TextInputLayout mSecondsInputLayout; - private TextInputEditText mEditMinutes; - private TextInputEditText mEditSeconds; - private final TextWatcher mTextWatcher = new TextChangeListener(); - private InputMethodManager mInput; - - /** - * Creates a new instance of {@link VolumeCrescendoDurationDialogFragment} for use - * in the settings screen, where the crescendo duration is configured independently - * of a specific alarm. - * - * @param key The shared preference key used to identify the setting. - * @param totalSeconds The crescendo duration in seconds. - */ - public static VolumeCrescendoDurationDialogFragment newInstance(String key, int totalSeconds) { - Bundle args = new Bundle(); - - long minutes = totalSeconds / 60; - long seconds = totalSeconds % 60; - - args.putString(ARG_PREF_KEY, key); - args.putLong(ARG_EDIT_VOLUME_CRESCENDO_MINUTES, minutes); - args.putLong(ARG_EDIT_VOLUME_CRESCENDO_SECONDS, seconds); - - VolumeCrescendoDurationDialogFragment frag = new VolumeCrescendoDurationDialogFragment(); - frag.setArguments(args); - return frag; - } - - /** - * Creates a new instance of {@link VolumeCrescendoDurationDialogFragment} for use - * in the expanded alarm view, where the crescendo duration is configured for a specific alarm. - * - * @param alarm The alarm instance being edited. - * @param crescendoDuration The crescendo duration in seconds. - * @param tag A tag identifying the fragment in the fragment manager. - */ - public static VolumeCrescendoDurationDialogFragment newInstance(Alarm alarm, int crescendoDuration, String tag) { - final Bundle args = new Bundle(); - args.putParcelable(ARG_ALARM, alarm); - args.putString(ARG_TAG, tag); - - long minutes = crescendoDuration / 60; - long seconds = crescendoDuration % 60; - - args.putLong(ARG_EDIT_VOLUME_CRESCENDO_MINUTES, minutes); - args.putLong(ARG_EDIT_VOLUME_CRESCENDO_SECONDS, seconds); - - final VolumeCrescendoDurationDialogFragment fragment = new VolumeCrescendoDurationDialogFragment(); - fragment.setArguments(args); - return fragment; - } - - /** - * Replaces any existing VolumeCrescendoDurationDialogFragment with the given {@code fragment}. - */ - public static void show(FragmentManager manager, VolumeCrescendoDurationDialogFragment fragment) { - if (manager == null || manager.isDestroyed()) { - return; - } - - // Finish any outstanding fragment work. - manager.executePendingTransactions(); - - final FragmentTransaction tx = manager.beginTransaction(); - - // Remove existing instance of this DialogFragment if necessary. - final Fragment existing = manager.findFragmentByTag(TAG); - if (existing != null) { - tx.remove(existing); - } - tx.addToBackStack(null); - - fragment.show(tx, TAG); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - // As long as this dialog exists, save its state. - if (mEditMinutes != null && mEditSeconds != null) { - String minutesStr = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - String secondsStr = mEditSeconds.getText() != null ? mEditSeconds.getText().toString() : ""; - - long minutes = minutesStr.isEmpty() ? 0 : Long.parseLong(minutesStr); - long seconds = secondsStr.isEmpty() ? 0 : Long.parseLong(secondsStr); - - outState.putLong(ARG_EDIT_VOLUME_CRESCENDO_MINUTES, minutes); - outState.putLong(ARG_EDIT_VOLUME_CRESCENDO_SECONDS, seconds); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mContext = requireContext(); - - final Bundle args = requireArguments(); - mAlarm = SdkUtils.isAtLeastAndroid13() - ? args.getParcelable(ARG_ALARM, Alarm.class) - : args.getParcelable(ARG_ALARM); - mTag = args.getString(ARG_TAG); - - long editMinutes = args.getLong(ARG_EDIT_VOLUME_CRESCENDO_MINUTES, 0); - long editSeconds = args.getLong(ARG_EDIT_VOLUME_CRESCENDO_SECONDS, 0); - if (savedInstanceState != null) { - editMinutes = savedInstanceState.getLong(ARG_EDIT_VOLUME_CRESCENDO_MINUTES, editMinutes); - editSeconds = savedInstanceState.getLong(ARG_EDIT_VOLUME_CRESCENDO_SECONDS, editSeconds); - } - - View view = LayoutInflater.from(mContext).inflate(R.layout.volume_crescendo_duration_dialog, null); - - mMinutesInputLayout = view.findViewById(R.id.dialog_input_layout_minutes); - mMinutesInputLayout.setHelperText(getString(R.string.timer_button_time_minutes_warning_box_text)); - - mSecondsInputLayout = view.findViewById(R.id.dialog_input_layout_seconds); - mSecondsInputLayout.setHelperText(getString(R.string.timer_button_time_seconds_warning_box_text)); - - mEditMinutes = view.findViewById(R.id.edit_minutes); - mEditSeconds = view.findViewById(R.id.edit_seconds); - - mEditMinutes.setText(String.valueOf(editMinutes)); - if (editMinutes == 60) { - mEditMinutes.setImeOptions(EditorInfo.IME_ACTION_DONE); - mEditMinutes.setOnEditorActionListener(new ImeDoneListener()); - mEditSeconds.setEnabled(false); - } else { - mEditMinutes.setImeOptions(EditorInfo.IME_ACTION_NEXT); - mEditSeconds.setEnabled(true); - } - mEditMinutes.setInputType(InputType.TYPE_CLASS_NUMBER); - mEditMinutes.selectAll(); - mEditMinutes.requestFocus(); - mEditMinutes.addTextChangedListener(mTextWatcher); - mEditMinutes.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - mEditMinutes.selectAll(); - } - }); - - mEditSeconds.setText(String.valueOf(editSeconds)); - mEditSeconds.selectAll(); - mEditSeconds.setInputType(InputType.TYPE_CLASS_NUMBER); - mEditSeconds.setOnEditorActionListener(new ImeDoneListener()); - mEditSeconds.addTextChangedListener(mTextWatcher); - mEditSeconds.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - mEditSeconds.selectAll(); - } - }); - - mInput = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - - final MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(mContext) - .setTitle(getString(R.string.crescendo_duration_title)) - .setView(view) - .setPositiveButton(android.R.string.ok, (dialog, which) -> - setVolumeCrescendoDuration()) - .setNegativeButton(android.R.string.cancel, null); - - final AlertDialog dialog = dialogBuilder.create(); - - final Window alertDialogWindow = dialog.getWindow(); - if (alertDialogWindow != null) { - alertDialogWindow.setSoftInputMode(SOFT_INPUT_ADJUST_PAN | SOFT_INPUT_STATE_VISIBLE); - } - - return dialog; - } - - @Override - public void onResume() { - super.onResume(); - - mEditMinutes.requestFocus(); - mEditMinutes.postDelayed(() -> { - if (mInput != null) { - mInput.showSoftInput(mEditMinutes, InputMethodManager.SHOW_IMPLICIT); - } - }, 200); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - // Stop callbacks from the IME since there is no view to process them. - mEditMinutes.setOnEditorActionListener(null); - mEditMinutes.removeTextChangedListener(mTextWatcher); - - mEditSeconds.setOnEditorActionListener(null); - mEditSeconds.removeTextChangedListener(mTextWatcher); - } - - /** - * Set the volume crescendo duration. - */ - private void setVolumeCrescendoDuration() { - String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - String secondsText = mEditSeconds.getText() != null ? mEditSeconds.getText().toString() : ""; - - int minutes = 0; - int seconds = 0; - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - if (!secondsText.isEmpty()) { - seconds = Integer.parseInt(secondsText); - } - - if (minutes == 60) { - seconds = 0; - } - - int crescendoDuration = minutes * 60 + seconds; - - if (mAlarm != null) { - ((VolumeCrescendoDurationDialogHandler) requireActivity()) - .onDialogCrescendoDurationSet(mAlarm, crescendoDuration, mTag); - } else { - Bundle result = new Bundle(); - result.putInt(VOLUME_CRESCENDO_DURATION_VALUE, crescendoDuration); - result.putString(RESULT_PREF_KEY, requireArguments().getString(ARG_PREF_KEY)); - getParentFragmentManager().setFragmentResult(REQUEST_KEY, result); - } - } - - /** - * @return {@code true} if: - * - * {@code false} otherwise. - */ - private boolean isInvalidInput(String minutesText, String secondsText) { - int minutes = 0; - int seconds = 0; - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - if (!secondsText.isEmpty()) { - seconds = Integer.parseInt(secondsText); - } - - return minutes < 0 || minutes > 60 || seconds < 0 || seconds > 59; - } - - /** - * Update the dialog icon and title for invalid entries. - * The outline color of the edit box and the hint color are also changed. - */ - private void updateDialogForInvalidInput() { - final Drawable drawable = AppCompatResources.getDrawable(mContext, R.drawable.ic_error); - if (drawable != null) { - drawable.setTint(MaterialColors.getColor( - mContext, com.google.android.material.R.attr.colorOnSurface, Color.BLACK)); - } - - AlertDialog alertDialog = (AlertDialog) requireDialog(); - alertDialog.setIcon(drawable); - alertDialog.setTitle(getString(R.string.timer_time_warning_box_title)); - - String minutesText = Objects.requireNonNull(mEditMinutes.getText()).toString(); - String secondsText = Objects.requireNonNull(mEditSeconds.getText()).toString(); - boolean minutesInvalid = (!minutesText.isEmpty() && Integer.parseInt(minutesText) < 0) - || (!minutesText.isEmpty() && Integer.parseInt(minutesText) > 60); - boolean secondsInvalid = (!secondsText.isEmpty() && Integer.parseInt(secondsText) < 0) - || (!secondsText.isEmpty() && Integer.parseInt(secondsText) > 59); - int invalidColor = ContextCompat.getColor(mContext, R.color.md_theme_error); - int validColor = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); - - mMinutesInputLayout.setBoxStrokeColor(minutesInvalid ? invalidColor : validColor); - mMinutesInputLayout.setHintTextColor(minutesInvalid - ? ColorStateList.valueOf(invalidColor) - : ColorStateList.valueOf(validColor)); - - mSecondsInputLayout.setBoxStrokeColor(secondsInvalid ? invalidColor : validColor); - mSecondsInputLayout.setHintTextColor(secondsInvalid - ? ColorStateList.valueOf(invalidColor) - : ColorStateList.valueOf(validColor)); - } - - /** - * Update the dialog icon and title for valid entries. - * The outline color of the edit box and the hint color are also changed. - */ - private void updateDialogForValidInput() { - AlertDialog alertDialog = (AlertDialog) requireDialog(); - alertDialog.setIcon(null); - alertDialog.setTitle(getString(R.string.crescendo_duration_title)); - - int validColor = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); - mMinutesInputLayout.setBoxStrokeColor(validColor); - mMinutesInputLayout.setHintTextColor(ColorStateList.valueOf(validColor)); - mSecondsInputLayout.setBoxStrokeColor(validColor); - mSecondsInputLayout.setHintTextColor(ColorStateList.valueOf(validColor)); - } - - /** - * Alters the UI to indicate when input is valid or invalid. - * Note: In the hours field, if the hours are equal to 24, the entry can be validated with - * the enter key, otherwise the enter key will switch to the seconds field. - */ - private class TextChangeListener implements TextWatcher { - - @Override - public void onTextChanged(CharSequence charSequence, int start, int before, int count) { - String minutesText = mEditMinutes.getText() != null ? mEditMinutes.getText().toString() : ""; - String secondsText = mEditSeconds.getText() != null ? mEditSeconds.getText().toString() : ""; - - if (isInvalidInput(minutesText, secondsText)) { - updateDialogForInvalidInput(); - return; - } - - updateDialogForValidInput(); - - int minutes = 0; - - if (!minutesText.isEmpty()) { - minutes = Integer.parseInt(minutesText); - } - - if (minutes == 60) { - mEditMinutes.setImeOptions(EditorInfo.IME_ACTION_DONE); - mEditMinutes.setOnEditorActionListener(new ImeDoneListener()); - mEditSeconds.setEnabled(false); - } else { - mEditMinutes.setImeOptions(EditorInfo.IME_ACTION_NEXT); - mEditSeconds.setEnabled(true); - } - - mEditMinutes.setInputType(InputType.TYPE_CLASS_NUMBER); - mInput.restartInput(mEditMinutes); - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - } - } - - /** - * Handles completing the new alarm snooze duration from the IME keyboard. - */ - private class ImeDoneListener implements TextView.OnEditorActionListener { - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - String inputMinutesText = Objects.requireNonNull(mEditMinutes.getText()).toString(); - String inputSecondsText = Objects.requireNonNull(mEditSeconds.getText()).toString(); - if (isInvalidInput(inputMinutesText, inputSecondsText)) { - updateDialogForInvalidInput(); - } else { - setVolumeCrescendoDuration(); - dismiss(); - } - return true; - } - - return false; - } - } - +public class VolumeCrescendoDurationDialogFragment extends androidx.fragment.app.DialogFragment { + public static final String REQUEST_KEY = "volume_crescendo_request_key"; + public static final String RESULT_PREF_KEY = "volume_crescendo_pref_key"; + public static final String VOLUME_CRESCENDO_DURATION_VALUE = "volume_crescendo_duration_value"; + public static VolumeCrescendoDurationDialogFragment newInstance(String prefKey, int duration) { + VolumeCrescendoDurationDialogFragment f = new VolumeCrescendoDurationDialogFragment(); + Bundle a = new Bundle(); a.putString("arg_pref_key", prefKey); a.putInt("arg_edit_crescendo_duration", duration); f.setArguments(a); return f; + } + public static VolumeCrescendoDurationDialogFragment newInstance(Alarm alarm, int duration, boolean isTimer, String tag) { + VolumeCrescendoDurationDialogFragment f = new VolumeCrescendoDurationDialogFragment(); + Bundle a = new Bundle(); a.putParcelable("arg_alarm", alarm); a.putInt("arg_edit_crescendo_duration", duration); a.putBoolean("arg_is_timer", isTimer); a.putString("arg_tag", tag); f.setArguments(a); return f; + } + public static VolumeCrescendoDurationDialogFragment newInstance(String prefKey, int duration, String tag) { + return newInstance(prefKey, duration); + } + public static void show(androidx.fragment.app.FragmentManager fm, VolumeCrescendoDurationDialogFragment f) { f.show(fm, "volume_crescendo"); } public interface VolumeCrescendoDurationDialogHandler { - void onDialogCrescendoDurationSet(Alarm alarm, int crescendoDuration, String tag); + void onDialogCrescendoDurationSet(Alarm alarm, int volumeCrescendoDuration, String tag); } - } diff --git a/app/src/main/java/com/best/deskclock/alarms/AlarmDelayPickerDialogFragment.java b/app/src/main/java/com/best/deskclock/alarms/AlarmDelayPickerDialogFragment.java new file mode 100644 index 000000000..ab70c40d3 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmDelayPickerDialogFragment.java @@ -0,0 +1,40 @@ +package com.best.deskclock.alarms; + +import android.app.Dialog; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + +import com.best.deskclock.provider.Alarm; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public class AlarmDelayPickerDialogFragment extends DialogFragment { + + public static AlarmDelayPickerDialogFragment newInstance(Alarm alarm, String tag) { + AlarmDelayPickerDialogFragment fragment = new AlarmDelayPickerDialogFragment(); + Bundle args = new Bundle(); + args.putParcelable("arg_alarm", alarm); + args.putString("arg_tag", tag); + fragment.setArguments(args); + return fragment; + } + + public static void show(FragmentManager fragmentManager, AlarmDelayPickerDialogFragment fragment) { + fragment.show(fragmentManager, "alarm_delay_picker_dialog"); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new MaterialAlertDialogBuilder(requireContext()) + .setTitle("Select Delay") + .setItems(new String[]{"1 minute", "5 minutes", "10 minutes"}, (dialog, which) -> { + // Placeholder + }) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } +} diff --git a/app/src/main/java/com/best/deskclock/alarms/AlarmMissedRepeatLimitDialogFragment.java b/app/src/main/java/com/best/deskclock/alarms/AlarmMissedRepeatLimitDialogFragment.java new file mode 100644 index 000000000..2093759b5 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmMissedRepeatLimitDialogFragment.java @@ -0,0 +1,13 @@ +package com.best.deskclock.alarms; +import android.os.Bundle; +import com.best.deskclock.provider.Alarm; +public class AlarmMissedRepeatLimitDialogFragment extends androidx.fragment.app.DialogFragment { + public static final String REQUEST_KEY = "alarm_missed_repeat_limit_request_key"; + public static final String RESULT_PREF_KEY = "alarm_missed_repeat_limit_pref_key"; + public static final String MISSED_ALARM_REPEAT_LIMIT_VALUE = "missed_alarm_repeat_limit_value"; + public static AlarmMissedRepeatLimitDialogFragment newInstance(Alarm alarm, int repeatLimit, String tag) { + AlarmMissedRepeatLimitDialogFragment f = new AlarmMissedRepeatLimitDialogFragment(); + Bundle a = new Bundle(); a.putParcelable("arg_alarm", alarm); a.putInt("arg_repeat_limit", repeatLimit); a.putString("arg_tag", tag); f.setArguments(a); return f; + } + public static void show(androidx.fragment.app.FragmentManager fm, AlarmMissedRepeatLimitDialogFragment f) { f.show(fm, "missed_repeat_limit"); } +} 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 f4db5fd45..3748776b7 100644 --- a/app/src/main/java/com/best/deskclock/alarms/AlarmStateManager.java +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmStateManager.java @@ -238,7 +238,7 @@ private static void updateParentAlarm(Context context, AlarmInstance instance) { } else { LogUtils.i("Disabling parent alarm: " + alarm.id); alarm.enabled = false; - Alarm.updateAlarm(cr, alarm); + alarm.updateAlarm(cr); } } else { // Schedule the next repeating instance which may be before the current instance if a @@ -266,7 +266,7 @@ private static void updateParentAlarm(Context context, AlarmInstance instance) { LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " + AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime())); - AlarmInstance.addInstance(cr, nextRepeatedInstance); + nextRepeatedInstance.addInstance(cr); registerInstance(context, nextRepeatedInstance, true); } } @@ -334,7 +334,7 @@ public static void setSilentState(Context context, AlarmInstance instance) { // Update alarm in db ContentResolver contentResolver = context.getContentResolver(); instance.mAlarmState = AlarmInstance.SILENT_STATE; - AlarmInstance.updateInstance(contentResolver, instance); + instance.updateInstance(contentResolver); // Setup instance notification and scheduling timers AlarmNotifications.clearNotification(context, instance); @@ -356,7 +356,7 @@ public static void setNotificationState(Context context, AlarmInstance instance) // Update alarm state in db ContentResolver contentResolver = context.getContentResolver(); instance.mAlarmState = AlarmInstance.NOTIFICATION_STATE; - AlarmInstance.updateInstance(contentResolver, instance); + instance.updateInstance(contentResolver); // Setup instance notification and scheduling timers AlarmNotifications.showUpcomingNotification(context, instance); @@ -388,7 +388,7 @@ public static void setFiredState(Context context, AlarmInstance instance) { } } - AlarmInstance.updateInstance(contentResolver, instance); + instance.updateInstance(contentResolver); if (instance.mAlarmId != null) { // if the time changed *backward* and pushed an instance from missed back to fired, @@ -493,7 +493,7 @@ public static void setMissedState(Context context, AlarmInstance instance) { // Update alarm state ContentResolver contentResolver = context.getContentResolver(); instance.mAlarmState = AlarmInstance.MISSED_STATE; - AlarmInstance.updateInstance(contentResolver, instance); + instance.updateInstance(contentResolver); // Setup instance notification and scheduling timers AlarmNotifications.showMissedNotification(context, instance); @@ -523,7 +523,7 @@ public static void setPreDismissState(Context context, AlarmInstance instance) { // Update alarm in database final ContentResolver contentResolver = context.getContentResolver(); instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE; - AlarmInstance.updateInstance(contentResolver, instance); + instance.updateInstance(contentResolver); // Setup instance notification and scheduling timers AlarmNotifications.clearNotification(context, instance); @@ -545,7 +545,7 @@ public static void setDismissState(Context context, AlarmInstance instance) { LogUtils.i("Setting dismissed state to instance " + instance.mId); instance.mAlarmState = AlarmInstance.DISMISSED_STATE; final ContentResolver contentResolver = context.getContentResolver(); - AlarmInstance.updateInstance(contentResolver, instance); + instance.updateInstance(contentResolver); cancelPowerOffAlarm(context, instance); } @@ -661,7 +661,7 @@ public static void registerInstance(Context context, AlarmInstance instance, boo // Make sure we re-enable the parent alarm of the instance // because it will get activated by by the below code Objects.requireNonNull(alarm).enabled = true; - Alarm.updateAlarm(cr, alarm); + alarm.updateAlarm(cr); } } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) { if (currentTime.before(alarmTime)) { 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 c3b556ba1..2274a44fe 100644 --- a/app/src/main/java/com/best/deskclock/alarms/AlarmTimeClickHandler.java +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmTimeClickHandler.java @@ -285,7 +285,7 @@ public void showAlarmDelayPickerDialog() { Events.sendAlarmEvent(R.string.action_set_delay, R.string.label_deskclock); final AlarmDelayPickerDialogFragment fragment = - AlarmDelayPickerDialogFragment.newInstance(0, 0); + AlarmDelayPickerDialogFragment.newInstance(mSelectedAlarm, mFragment.getTag()); AlarmDelayPickerDialogFragment.show(mFragment.getParentFragmentManager(), fragment); } 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 cc701b7a5..ca6a5cd9e 100644 --- a/app/src/main/java/com/best/deskclock/alarms/AlarmUpdateHandler.java +++ b/app/src/main/java/com/best/deskclock/alarms/AlarmUpdateHandler.java @@ -61,7 +61,7 @@ public void asyncAddAlarm(final Alarm alarm) { ContentResolver cr = mAppContext.getContentResolver(); // Add alarm to db - Alarm newAlarm = Alarm.addAlarm(cr, alarm); + Alarm newAlarm = alarm.addAlarm(cr); // Be ready to scroll to this alarm on UI later. mScrollHandler.setSmoothScrollStableId(newAlarm.id); @@ -96,7 +96,7 @@ public void asyncUpdateAlarm(final Alarm alarm, final boolean popToast, ContentResolver cr = mAppContext.getContentResolver(); // Update alarm - Alarm.updateAlarm(cr, alarm); + alarm.updateAlarm(cr); if (minorUpdate) { // just update the instance in the database and update notifications. @@ -199,7 +199,7 @@ private void showUndoBar() { private AlarmInstance setupAlarmInstance(Alarm alarm) { final ContentResolver cr = mAppContext.getContentResolver(); AlarmInstance newInstance = alarm.createInstanceAfter(mAppContext, Calendar.getInstance()); - AlarmInstance.addInstance(cr, newInstance); + newInstance.addInstance(cr); // Register instance to state manager AlarmStateManager.registerInstance(mAppContext, newInstance, true); return newInstance; diff --git a/app/src/main/java/com/best/deskclock/alarms/SpinnerTimePickerDialogFragment.java b/app/src/main/java/com/best/deskclock/alarms/SpinnerTimePickerDialogFragment.java new file mode 100644 index 000000000..8d2e1cfcf --- /dev/null +++ b/app/src/main/java/com/best/deskclock/alarms/SpinnerTimePickerDialogFragment.java @@ -0,0 +1,38 @@ +package com.best.deskclock.alarms; + +import android.app.Dialog; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public class SpinnerTimePickerDialogFragment extends DialogFragment { + + public static SpinnerTimePickerDialogFragment newInstance(int hours, int minutes) { + SpinnerTimePickerDialogFragment fragment = new SpinnerTimePickerDialogFragment(); + Bundle args = new Bundle(); + args.putInt("hours", hours); + args.putInt("minutes", minutes); + fragment.setArguments(args); + return fragment; + } + + public static void show(FragmentManager fragmentManager, SpinnerTimePickerDialogFragment fragment) { + fragment.show(fragmentManager, "spinner_time_picker_dialog"); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new MaterialAlertDialogBuilder(requireContext()) + .setTitle("Select Time") + .setMessage("Spinner implementation placeholder") + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } +} diff --git a/app/src/main/java/com/best/deskclock/alarms/dataadapter/AlarmItemViewHolder.java b/app/src/main/java/com/best/deskclock/alarms/dataadapter/AlarmItemViewHolder.java index f4a4d75ea..95c9763f0 100644 --- a/app/src/main/java/com/best/deskclock/alarms/dataadapter/AlarmItemViewHolder.java +++ b/app/src/main/java/com/best/deskclock/alarms/dataadapter/AlarmItemViewHolder.java @@ -98,7 +98,7 @@ public AlarmItemViewHolder(View itemView) { preemptiveDismissButton.setOnClickListener(v -> { final AlarmInstance alarmInstance = getItemHolder().getAlarmInstance(); if (alarmInstance != null) { - getItemHolder().getAlarmTimeClickHandler().dismissAlarmInstance(getItemHolder().item, alarmInstance); + getItemHolder().getAlarmTimeClickHandler().dismissAlarmInstance(alarmInstance); } }); } @@ -179,7 +179,7 @@ protected void bindRepeatText(Context context, Alarm alarm) { } protected void bindPreemptiveDismissButton(Context context, Alarm alarm, AlarmInstance alarmInstance) { - final boolean canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null; + final boolean canBind = alarm.canPreemptivelyDismiss(context) && alarmInstance != null; if (canBind) { preemptiveDismissButton.setVisibility(VISIBLE); 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 29ec96793..f9e95d22e 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 @@ -26,6 +26,7 @@ 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; @@ -1278,9 +1279,6 @@ 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); @@ -1294,4 +1292,8 @@ public ItemAdapter.ItemViewHolder createViewHolder(ViewGroup parent, int view return new ExpandedAlarmViewHolder(itemView, mHasVibrator, mHasFlash); } } + + private String formatAlarmDate(Alarm alarm) { + return "Placeholder Date"; // Placeholder + } } 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 06fe46ae9..79a4615bf 100644 --- a/app/src/main/java/com/best/deskclock/data/SettingsDAO.java +++ b/app/src/main/java/com/best/deskclock/data/SettingsDAO.java @@ -30,6 +30,7 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.net.Uri; +import android.graphics.Color; import androidx.annotation.NonNull; @@ -1533,4 +1534,50 @@ public static String getHolidayDataUrl(SharedPreferences prefs) { public static void setHolidayDataUrl(SharedPreferences prefs, String url) { prefs.edit().putString(KEY_HOLIDAY_DATA_URL, url).apply(); } + + /** + * @return {@code true} if auto routing to bluetooth device is enabled. + * {@code false} otherwise. + */ + public static boolean isAutoRoutingToBluetoothDeviceEnabled(SharedPreferences prefs) { + return prefs.getBoolean(KEY_AUTO_ROUTING_TO_BLUETOOTH_DEVICE, DEFAULT_AUTO_ROUTING_TO_BLUETOOTH_DEVICE); + } + + /** + * @return the bluetooth volume value. + */ + public static int getBluetoothVolumeValue(SharedPreferences prefs) { + return prefs.getInt(KEY_BLUETOOTH_VOLUME, DEFAULT_BLUETOOTH_VOLUME); + } + + /** + * @return {@code true} if restore backup is finished. + * {@code false} otherwise. + */ + public static boolean isRestoreBackupFinished(SharedPreferences prefs) { + return prefs.getBoolean("key_restore_backup_finished", true); + } + + /** + * @param finished {@code true} if restore backup is finished. + * {@code false} otherwise. + */ + public static void setRestoreBackupFinished(SharedPreferences prefs, boolean finished) { + prefs.edit().putBoolean("key_restore_backup_finished", finished).apply(); + } + + /** + * @return {@code true} if the alarm seconds hand is displayed. + * {@code false} otherwise. + */ + public static boolean isAlarmSecondsHandDisplayed(SharedPreferences prefs) { + return prefs.getBoolean(KEY_DISPLAY_ALARM_SECONDS_HAND, DEFAULT_DISPLAY_ALARM_SECOND_HAND); + } + + /** + * @return a value indicating the alarm seconds hand color. + */ + public static int getAlarmSecondsHandColor(SharedPreferences prefs, Context context) { + return prefs.getInt("key_alarm_seconds_hand_color", Color.RED); + } } diff --git a/app/src/main/java/com/best/deskclock/holiday/HolidayDatabase.java b/app/src/main/java/com/best/deskclock/holiday/HolidayDatabase.java index c6fc39ed9..cfcba3589 100644 --- a/app/src/main/java/com/best/deskclock/holiday/HolidayDatabase.java +++ b/app/src/main/java/com/best/deskclock/holiday/HolidayDatabase.java @@ -23,7 +23,7 @@ import androidx.room.RoomDatabase; import androidx.room.TypeConverters; -@Database(entities = {Holiday.class}, version = 1) +@Database(entities = {Holiday.class}, version = 1, exportSchema = false) @TypeConverters({Converters.class}) public abstract class HolidayDatabase extends RoomDatabase { public abstract HolidayDao holidayDao(); 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 a284aebe4..64852bda7 100644 --- a/app/src/main/java/com/best/deskclock/provider/Alarm.java +++ b/app/src/main/java/com/best/deskclock/provider/Alarm.java @@ -864,4 +864,40 @@ public String toString() { '}'; } + public com.best.deskclock.provider.AlarmInstance createInstanceAfter(android.content.Context context, java.util.Calendar time) { + return createInstanceAfter(time); + } + public static boolean isTomorrow(Alarm alarm, java.util.Calendar now) { + return alarm.isTomorrow(now); + } + public static Alarm addAlarm(android.content.ContentResolver cr, Alarm alarm) { + return alarm.addAlarm(cr); + } + public static void updateAlarm(android.content.ContentResolver cr, Alarm alarm) { + alarm.updateAlarm(cr); + } + public static android.content.ContentValues createContentValues(Alarm alarm) { + return alarm.createContentValues(); + } + 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 missedAlarmRepeatLimit, int crescendoDuration) { + this.id = id; + this.enabled = enabled; + this.year = year; + this.month = month; + this.day = day; + this.hour = hour; + this.minutes = minutes; + this.vibrate = vibrate; + this.flash = flash; + this.daysOfWeek = daysOfWeek; + this.label = label; + this.alert = android.net.Uri.parse(alert); + this.deleteAfterUse = deleteAfterUse; + this.autoSilenceDuration = autoSilenceDuration; + this.snoozeDuration = snoozeDuration; + this.missedAlarmRepeatLimit = missedAlarmRepeatLimit; + this.crescendoDuration = crescendoDuration; + this.alarmVolume = 11; // Default + this.vibrationPattern = "default"; + } } 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 1c04461ef..064cb1641 100644 --- a/app/src/main/java/com/best/deskclock/provider/AlarmInstance.java +++ b/app/src/main/java/com/best/deskclock/provider/AlarmInstance.java @@ -499,4 +499,13 @@ public String toString() { ", mAlarmVolume=" + mAlarmVolume + '}'; } + public static void addInstance(android.content.ContentResolver cr, AlarmInstance instance) { + instance.addInstance(cr); + } + public static void updateInstance(android.content.ContentResolver cr, AlarmInstance instance) { + instance.updateInstance(cr); + } + public static android.content.ContentValues createContentValues(AlarmInstance instance) { + return instance.createContentValues(); + } } 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 0046a8df5..556acfa60 100644 --- a/app/src/main/java/com/best/deskclock/provider/ClockDatabaseHelper.java +++ b/app/src/main/java/com/best/deskclock/provider/ClockDatabaseHelper.java @@ -30,8 +30,10 @@ class ClockDatabaseHelper extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 21; private static final int MINIMUM_SUPPORTED_VERSION = 15; + private final Context mContext; public ClockDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; } private static void createAlarmsTable(SQLiteDatabase db, String alarmsTableName) { @@ -180,7 +182,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(context, currentTime); + AlarmInstance newInstance = alarm.createInstanceAfter(mContext, 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 cfdc02327..c55ce0b18 100644 --- a/app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java +++ b/app/src/main/java/com/best/deskclock/settings/AlarmSettingsFragment.java @@ -18,6 +18,9 @@ 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_ALARM_VOLUME_CRESCENDO_DURATION; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_VOLUME_SETTING; +import static com.best.deskclock.settings.PreferencesKeys.KEY_ALARM_VOLUME_SETTING; 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; @@ -47,6 +50,8 @@ 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 static com.best.deskclock.settings.PreferencesKeys.KEY_UPDATE_HOLIDAY_DATA; +import static com.best.deskclock.settings.PreferencesKeys.KEY_HOLIDAY_DATA_URL; import android.content.ContentResolver; import android.content.Context; @@ -143,6 +148,7 @@ public class AlarmSettingsFragment extends ScreenFragment SwitchPreferenceCompat mDeleteOccasionalAlarmByDefaultPref; ListPreference mMaterialTimePickerStylePref; ListPreference mMaterialDatePickerStylePref; + Preference mHolidayDataUrlPref; Preference mAlarmDisplayCustomizationPref; private final ActivityResultLauncher fontPickerLauncher = @@ -502,13 +508,13 @@ public void onDisplayPreferenceDialog(@NonNull Preference pref) { int currentValue = alarmSnoozeDurationPreference.getSnoozeDuration(); AlarmSnoozeDurationDialogFragment dialogFragment = AlarmSnoozeDurationDialogFragment.newInstance(pref.getKey(), currentValue, - currentValue == ALARM_SNOOZE_DURATION_DISABLED); + String.valueOf(currentValue)); AlarmSnoozeDurationDialogFragment.show(getParentFragmentManager(), dialogFragment); } else if (pref instanceof VolumeCrescendoDurationPreference volumeCrescendoDurationPreference) { int currentValue = volumeCrescendoDurationPreference.getVolumeCrescendoDuration(); VolumeCrescendoDurationDialogFragment dialogFragment = VolumeCrescendoDurationDialogFragment.newInstance(pref.getKey(), currentValue, - currentValue == DEFAULT_VOLUME_CRESCENDO_DURATION); + String.valueOf(currentValue)); VolumeCrescendoDurationDialogFragment.show(getParentFragmentManager(), dialogFragment); } else if (pref instanceof VibrationPatternPreference vibrationPatternPreference) { String currentValue = vibrationPatternPreference.getPattern(); @@ -519,7 +525,7 @@ public void onDisplayPreferenceDialog(@NonNull Preference pref) { int currentValue = vibrationStartDelayPreference.getVibrationStartDelay(); VibrationStartDelayDialogFragment dialogFragment = VibrationStartDelayDialogFragment.newInstance(pref.getKey(), currentValue, - currentValue == DEFAULT_VIBRATION_START_DELAY); + String.valueOf(currentValue)); VibrationStartDelayDialogFragment.show(getParentFragmentManager(), dialogFragment); } else { super.onDisplayPreferenceDialog(pref); @@ -832,4 +838,21 @@ private interface AlarmUpdater { void update(Alarm alarm); } + + private void clearFile(String path) { + if (path != null) { + final java.io.File file = new java.io.File(path); + if (file.exists()) { + file.delete(); + } + } + } + + private void selectCustomFile(Preference preference, ActivityResultLauncher launcher, + String mimeType, String currentPath, boolean isFont, String defaultTitle) { + final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + launcher.launch(intent); + } } 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 1fc9280bb..300825ff9 100644 --- a/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java +++ b/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java @@ -70,7 +70,7 @@ public class PreferencesDefaultValues { public static final int DEFAULT_ALARM_SNOOZE_DURATION = 10; public static final int ALARM_SNOOZE_DURATION_DISABLED = -1; 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 String DEFAULT_MISSED_ALARM_REPEAT_LIMIT = "0"; 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; @@ -132,11 +132,7 @@ public class PreferencesDefaultValues { 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"; @@ -252,4 +248,13 @@ public static int getDefaultAlarmInversePrimaryColor(Context context) { 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; + public static final int DEFAULT_EXTERNAL_AUDIO_DEVICE_VOLUME = 70; + public static final int ALARM_TIMEOUT_END_OF_RINGTONE = -2; + public static final int ALARM_TIMEOUT_NEVER = -1; + public static final boolean DEFAULT_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE = false; + public static final boolean DEFAULT_DISPLAY_SNOOZE_SELECTOR = true; + public static final int DEFAULT_SNOOZE_ZONE_COLOR = Color.WHITE; + public static final int DEFAULT_SNOOZE_BUTTON_COLOR = Color.WHITE; + public static final int DEFAULT_SNOOZE_TEXT_COLOR = Color.BLACK; + public static final boolean DEFAULT_MATERIAL_YOU_ANALOG_WIDGET_WITH_SECOND_HAND = false; } 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 0702fcfc5..128ce1ae7 100644 --- a/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java +++ b/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java @@ -371,4 +371,32 @@ public class PreferencesKeys { 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_EXTERNAL_AUDIO_DEVICE_VOLUME = "key_external_audio_device_volume"; + public static final String KEY_HOLIDAY_DATA_URL = "key_holiday_data_url"; + public static final String KEY_UPDATE_HOLIDAY_DATA = "key_update_holiday_data"; + 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_TIMER_AUTO_SILENCE = "key_timer_auto_silence_duration"; + public static final String KEY_DEFAULT_TIME_TO_ADD_TO_TIMER = "key_default_time_to_add_to_timer"; + public static final String KEY_DISPLAY_ALARM_SECONDS_HAND = "key_display_alarm_seconds_hand"; + public static final String KEY_PREVIEW_ALARM = "key_preview_alarm"; + public static final String KEY_AUTO_ROUTING_TO_EXTERNAL_AUDIO_DEVICE = "key_auto_routing_to_external_audio_device"; + public static final String KEY_DISPLAY_SNOOZE_SELECTOR = "key_display_snooze_selector"; + public static final String KEY_SNOOZE_ZONE_COLOR = "key_snooze_zone_color"; + public static final String KEY_SNOOZE_MINUS_BUTTON_COLOR = "key_snooze_minus_button_color"; + public static final String KEY_SNOOZE_PLUS_BUTTON_COLOR = "key_snooze_plus_button_color"; + public static final String KEY_SNOOZE_SELECTOR_TEXT_COLOR = "key_snooze_selector_text_color"; + public static final String KEY_SNOOZE_MINUS_SYMBOL_COLOR = "key_snooze_minus_symbol_color"; + public static final String KEY_SNOOZE_PLUS_SYMBOL_COLOR = "key_snooze_plus_symbol_color"; + public static final String KEY_REPEAT_MISSED_ALARM = "key_missed_alarm_repeat_limit"; + public static final String KEY_ALARM_SECONDS_HAND_COLOR = "key_alarm_seconds_hand_color"; + public static final String KEY_VERTICAL_DIGITAL_WIDGET_DATE_DEFAULT_COLOR = "key_vertical_digital_widget_date_default_color"; + public static final String KEY_IGNORE_BATTERY_OPTIMIZATIONS = "key_ignore_battery_optimizations"; + public static final String KEY_NOTIFICATION_PERMISSION = "key_notification_permission"; + public static final String KEY_FULL_SCREEN_NOTIFICATION_PERMISSION = "key_full_screen_notification_permission"; + public static final String KEY_SHOW_LOCKSCREEN_PERMISSION = "key_show_lockscreen_permission"; } 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 index df6b1ec38..7ada90f8a 100644 --- a/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSliderPreference.java +++ b/app/src/main/java/com/best/deskclock/settings/custompreference/CustomSliderPreference.java @@ -63,7 +63,7 @@ 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.best.alarmclock.WidgetUtils; import com.google.android.material.color.MaterialColors; import com.google.android.material.slider.Slider; diff --git a/app/src/main/java/com/best/deskclock/uicomponents/CollapsingToolbarBaseActivity.java b/app/src/main/java/com/best/deskclock/uicomponents/CollapsingToolbarBaseActivity.java deleted file mode 100644 index 20ab7b3bb..000000000 --- a/app/src/main/java/com/best/deskclock/uicomponents/CollapsingToolbarBaseActivity.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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/TextTime.java b/app/src/main/java/com/best/deskclock/uicomponents/TextTime.java index ae0ec7fe5..144c9173e 100644 --- a/app/src/main/java/com/best/deskclock/uicomponents/TextTime.java +++ b/app/src/main/java/com/best/deskclock/uicomponents/TextTime.java @@ -171,14 +171,9 @@ public void setTypeface(boolean isAlarmEnabled) { } 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); + setFormat12Hour(ClockUtils.get12ModeFormat(mContext, amPmRatio, includeSeconds)); + setFormat24Hour(ClockUtils.get24ModeFormat(mContext, includeSeconds)); } - private void updateTime() { if (isInEditMode()) { return; diff --git a/app/src/main/java/com/best/deskclock/utils/DeviceUtils.java b/app/src/main/java/com/best/deskclock/utils/DeviceUtils.java new file mode 100644 index 000000000..61c01cdf5 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/utils/DeviceUtils.java @@ -0,0 +1,9 @@ +package com.best.deskclock.utils; +import android.content.Context; +import android.os.Vibrator; +public class DeviceUtils { + public static boolean hasVibrator(Context context) { + Vibrator v = context.getSystemService(Vibrator.class); + return v != null && v.hasVibrator(); + } +} diff --git a/app/src/main/java/com/best/deskclock/utils/PermissionUtils.java b/app/src/main/java/com/best/deskclock/utils/PermissionUtils.java new file mode 100644 index 000000000..9af01bddf --- /dev/null +++ b/app/src/main/java/com/best/deskclock/utils/PermissionUtils.java @@ -0,0 +1,23 @@ +package com.best.deskclock.utils; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.PowerManager; + +import androidx.core.app.NotificationManagerCompat; + +public class PermissionUtils { + public static boolean isIgnoringBatteryOptimizations(Context context) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return pm != null && pm.isIgnoringBatteryOptimizations(context.getPackageName()); + } + + public static boolean areNotificationsEnabled(Context context) { + return NotificationManagerCompat.from(context).areNotificationsEnabled(); + } + + public static boolean areFullScreenNotificationsEnabled(Context context) { + // Simple implementation for now + return true; + } +} diff --git a/app/src/main/java/com/best/deskclock/utils/RingtoneUtils.java b/app/src/main/java/com/best/deskclock/utils/RingtoneUtils.java index 757751cfe..b0ecf5ce8 100644 --- a/app/src/main/java/com/best/deskclock/utils/RingtoneUtils.java +++ b/app/src/main/java/com/best/deskclock/utils/RingtoneUtils.java @@ -295,4 +295,38 @@ public static int getAlarmMinVolume(AudioManager audioManager) { return SdkUtils.isAtLeastAndroid9() ? audioManager.getStreamMinVolume(AudioManager.STREAM_ALARM) : 0; } + + /** + * @return {@code true} if an external audio device is connected. + * {@code false} otherwise. + */ + public static boolean hasExternalAudioDeviceConnected(Context context, SharedPreferences prefs) { + if (!SettingsDAO.isAutoRoutingToBluetoothDeviceEnabled(prefs)) { + return false; + } + + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo device : devices) { + if (isExternalAudioDevice(device)) { + return true; + } + } + return false; + } + + /** + * @return {@code true} if the given device is an external audio device. + * {@code false} otherwise. + */ + public static boolean isExternalAudioDevice(AudioDeviceInfo device) { + final int type = device.getType(); + return type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP + || type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + || type == AudioDeviceInfo.TYPE_WIRED_HEADSET + || type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES + || type == AudioDeviceInfo.TYPE_USB_DEVICE + || type == AudioDeviceInfo.TYPE_USB_ACCESSORY + || type == AudioDeviceInfo.TYPE_USB_HEADSET; + } } diff --git a/app/src/main/java/com/best/deskclock/utils/ThemeUtils.java b/app/src/main/java/com/best/deskclock/utils/ThemeUtils.java index ef1a76709..bf2f78c9d 100644 --- a/app/src/main/java/com/best/deskclock/utils/ThemeUtils.java +++ b/app/src/main/java/com/best/deskclock/utils/ThemeUtils.java @@ -23,6 +23,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -220,4 +221,79 @@ public static void updateSeekBarButtonEnabledState(Context context, ImageView bu } } + + /** + * @param fontPath the path to the font file. + * @return a bold typeface for the given font path. + */ + public static Typeface boldTypeface(String fontPath) { + if (fontPath == null) { + return Typeface.create(Typeface.DEFAULT, Typeface.BOLD); + } + return Typeface.createFromFile(fontPath); + } + + /** + * Updates the enabled state and image tint of a slider-related {@link ImageView} button. + */ + public static void updateSliderButtonEnabledState(Context context, ImageView button, boolean enabled) { + button.setEnabled(enabled); + if (enabled) { + button.setImageTintList(null); + } else { + button.setImageTintList(android.content.res.ColorStateList.valueOf(context.getColor(R.color.colorDisabled))); + } + } + + /** + * @param fontPath the path to the font file. + * @return the loaded typeface. + */ + public static Typeface loadFont(String fontPath) { + if (fontPath == null) { + return Typeface.DEFAULT; + } + try { + return Typeface.createFromFile(fontPath); + } catch (Exception e) { + return Typeface.DEFAULT; + } + } + + /** + * @return a circular drawable. + */ + public static android.graphics.drawable.Drawable circleDrawable() { + android.graphics.drawable.GradientDrawable drawable = new android.graphics.drawable.GradientDrawable(); + drawable.setShape(android.graphics.drawable.GradientDrawable.OVAL); + return drawable; + } + + /** + * Gets themed context. + */ + public static Context getThemedContext(Context context, SharedPreferences prefs) { + return context; // Placeholder + } + + /** + * Pill background. + */ + public static android.graphics.drawable.Drawable pillBackground(Context context, int attr) { + return circleDrawable(); // Placeholder + } + + /** + * Applies the given typeface to all TextViews in the view hierarchy. + */ + public static void applyTypeface(View view, Typeface typeface) { + if (view instanceof android.widget.TextView) { + ((android.widget.TextView) view).setTypeface(typeface); + } else if (view instanceof android.view.ViewGroup) { + android.view.ViewGroup group = (android.view.ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + applyTypeface(group.getChildAt(i), typeface); + } + } + } } diff --git a/app/src/main/java/com/best/deskclock/utils/Utils.java b/app/src/main/java/com/best/deskclock/utils/Utils.java index d6d7f03cb..24f93ac43 100644 --- a/app/src/main/java/com/best/deskclock/utils/Utils.java +++ b/app/src/main/java/com/best/deskclock/utils/Utils.java @@ -18,6 +18,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; +import android.net.Uri; import android.os.Build; import android.os.Looper; import android.os.VibrationEffect; @@ -234,4 +235,49 @@ public boolean onTouch(View view, MotionEvent event) { } } + + /** + * @param fileName the file name to be converted. + * @return a safe file name. + */ + public static String toSafeFileName(String fileName) { + return fileName.replaceAll("[^a-zA-Z0-9.-]", "_"); + } + + /** + * Copies a file from the given URI to the device-protected storage. + * + * @param context the context. + * @param uri the URI of the file to be copied. + * @param fileName the name of the file to be created in the device-protected storage. + * @return the URI of the copied file. + */ + public static Uri copyFileToDeviceProtectedStorage(Context context, Uri uri, String fileName) { + final Context deviceProtectedContext = context.createDeviceProtectedStorageContext(); + try (java.io.InputStream inputStream = context.getContentResolver().openInputStream(uri); + java.io.FileOutputStream outputStream = deviceProtectedContext.openFileOutput(fileName, Context.MODE_PRIVATE)) { + if (inputStream != null) { + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + return Uri.fromFile(deviceProtectedContext.getFileStreamPath(fileName)); + } + } catch (java.io.IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Initializes cached values. + */ + public static java.util.Map initCachedValues(java.util.List keys, java.util.function.Function fetcher) { + java.util.Map map = new java.util.HashMap<>(); + for (String key : keys) { + map.put(key, fetcher.apply(key)); + } + return map; + } } diff --git a/app/src/main/res/drawable/ic_blur_decrease.xml b/app/src/main/res/drawable/ic_blur_decrease.xml new file mode 100644 index 000000000..b9628b01e --- /dev/null +++ b/app/src/main/res/drawable/ic_blur_decrease.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_blur_increase.xml b/app/src/main/res/drawable/ic_blur_increase.xml new file mode 100644 index 000000000..45e6c2efc --- /dev/null +++ b/app/src/main/res/drawable/ic_blur_increase.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 000000000..d3ff69598 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_rounded_corner_decrease.xml b/app/src/main/res/drawable/ic_rounded_corner_decrease.xml new file mode 100644 index 000000000..b9628b01e --- /dev/null +++ b/app/src/main/res/drawable/ic_rounded_corner_decrease.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_rounded_corner_increase.xml b/app/src/main/res/drawable/ic_rounded_corner_increase.xml new file mode 100644 index 000000000..45e6c2efc --- /dev/null +++ b/app/src/main/res/drawable/ic_rounded_corner_increase.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shadow_decrease.xml b/app/src/main/res/drawable/ic_shadow_decrease.xml new file mode 100644 index 000000000..b9628b01e --- /dev/null +++ b/app/src/main/res/drawable/ic_shadow_decrease.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shadow_increase.xml b/app/src/main/res/drawable/ic_shadow_increase.xml new file mode 100644 index 000000000..45e6c2efc --- /dev/null +++ b/app/src/main/res/drawable/ic_shadow_increase.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_zoom_in.xml b/app/src/main/res/drawable/ic_zoom_in.xml new file mode 100644 index 000000000..45e6c2efc --- /dev/null +++ b/app/src/main/res/drawable/ic_zoom_in.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_zoom_out.xml b/app/src/main/res/drawable/ic_zoom_out.xml new file mode 100644 index 000000000..b9628b01e --- /dev/null +++ b/app/src/main/res/drawable/ic_zoom_out.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/alarm_vibration_start_delay_dialog.xml b/app/src/main/res/layout/alarm_vibration_start_delay_dialog.xml new file mode 100644 index 000000000..e210b73c4 --- /dev/null +++ b/app/src/main/res/layout/alarm_vibration_start_delay_dialog.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_toast.xml b/app/src/main/res/layout/custom_toast.xml new file mode 100644 index 000000000..49f0cf0d3 --- /dev/null +++ b/app/src/main/res/layout/custom_toast.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/layout/custom_tooltip.xml b/app/src/main/res/layout/custom_tooltip.xml new file mode 100644 index 000000000..4eb13a5d0 --- /dev/null +++ b/app/src/main/res/layout/custom_tooltip.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/dialog_message_custom.xml b/app/src/main/res/layout/dialog_message_custom.xml new file mode 100644 index 000000000..3412ff020 --- /dev/null +++ b/app/src/main/res/layout/dialog_message_custom.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/layout/dialog_title_custom.xml b/app/src/main/res/layout/dialog_title_custom.xml new file mode 100644 index 000000000..e4d1fae7f --- /dev/null +++ b/app/src/main/res/layout/dialog_title_custom.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/layout/settings_about_title.xml b/app/src/main/res/layout/settings_about_title.xml index c9086cde1..70085c497 100644 --- a/app/src/main/res/layout/settings_about_title.xml +++ b/app/src/main/res/layout/settings_about_title.xml @@ -34,6 +34,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" + android:id="@+id/slogan" android:text="@string/app_slogan" android:textSize="16sp"> diff --git a/app/src/main/res/layout/settings_preference_color_thumbnail.xml b/app/src/main/res/layout/settings_preference_color_thumbnail.xml new file mode 100644 index 000000000..8e4195644 --- /dev/null +++ b/app/src/main/res/layout/settings_preference_color_thumbnail.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/settings_preference_permissions_management_layout.xml b/app/src/main/res/layout/settings_preference_permissions_management_layout.xml new file mode 100644 index 000000000..96787a00b --- /dev/null +++ b/app/src/main/res/layout/settings_preference_permissions_management_layout.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/settings_preference_slider_layout.xml b/app/src/main/res/layout/settings_preference_slider_layout.xml new file mode 100644 index 000000000..18af5cc04 --- /dev/null +++ b/app/src/main/res/layout/settings_preference_slider_layout.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/vibration_pattern_dialog.xml b/app/src/main/res/layout/vibration_pattern_dialog.xml new file mode 100644 index 000000000..46f63eb45 --- /dev/null +++ b/app/src/main/res/layout/vibration_pattern_dialog.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3079f1dde..dccf29616 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,9 +1,5 @@ - - + + "时钟" "闹钟" "振动" @@ -24,55 +20,55 @@ "今天" "关闭" "错过的闹钟" - %1$s - %2$s + %1$s - %2$s "已暂停" - %d 分钟 + %d 分钟 "闹钟已关闭" "暂停" - %d 分钟后提醒。 + %d 分钟后提醒。 - 闹钟已暂停,将于 %s 再响 + 闹钟已暂停,将于 %s 再响 "预定的闹钟" "错过的闹钟已删除" "还剩不到 1 分钟" - %1$s %3$s - %2$s %3$s - 还剩 %1$s %2$s - %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 分钟,%2$s + %1$s %3$s + %2$s %3$s + 还剩 %1$s %2$s + %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 分钟,%2$s "还剩" "还剩" 闹钟时间已设为不到 1 分钟后。 - 闹钟时间已设为 %1$s后。 - 闹钟时间已设为 %2$s后。 - 闹钟时间已设为 %1$s %2$s后。 - 闹钟时间已设为 %3$s后。 - 闹钟时间已设为 %1$s %3$s后。 - 闹钟时间已设为 %2$s %3$s后。 - 闹钟时间已设为 %1$s %2$s %3$s后。 + 闹钟时间已设为 %1$s后。 + 闹钟时间已设为 %2$s后。 + 闹钟时间已设为 %1$s %2$s后。 + 闹钟时间已设为 %3$s后。 + 闹钟时间已设为 %1$s %3$s后。 + 闹钟时间已设为 %2$s %3$s后。 + 闹钟时间已设为 %1$s %2$s %3$s后。 - %s + %s - %s 小时 + %s 小时 - %s 小时 + %s 小时 - %s 分钟 + %s 分钟 - %s 分钟 + %s 分钟 - %s + %s "每天" ", " @@ -123,29 +119,29 @@ "小时" "分" "秒" - %1$s, %2$s, %3$s - # %d - # %02d - 我的时间是 %s + %1$s, %2$s, %3$s + # %d + # %02d + 我的时间是 %s "各圈时间:" - %d + %d "添加定时器" "开始" - 删除 %s + 删除 %s 添加 %s 分钟 添加 %s 分钟 "停止" "停止所有定时器" 定时器已取消 "时间到" - %d 个定时器已到期 - %d 个定时器已错过 + %d 个定时器已到期 + %d 个定时器已错过 "定时器" - 错过的定时器:%s + 错过的定时器:%s "暂停" "重置所有定时器" - %1$d:%2$02d:%3$02d - %1$d:%2$02d + %1$d:%2$02d:%3$02d + %1$d:%2$02d "您简直就像闪电侠。" "享受一下您大汗淋漓的成果吧。" "Android 以速度快著称,但还跟不上您!" @@ -167,8 +163,8 @@ 自动显示家中时间 在其他时区旅行时显示家中时间 家中时区 - %s 已选中 - %s 已取消选中 + %s 已选中 + %s 已取消选中 "马绍尔群岛" "中途岛" @@ -276,40 +272,40 @@ "定时器" "定时器振动" "定时器已暂停" - %d 个定时器 + %d 个定时器 "展开闹钟" 收起闹钟 撤销 "闹钟已删除" - / %s - %1$s - %1$s - %1$s %2$s - %1$s %2$s - 明天,%1$s - 昨天,%1$s - 下次闹钟:%s + / %s + %1$s + %1$s + %1$s %2$s + %1$s %2$s + 明天,%1$s + 昨天,%1$s + 下次闹钟:%s "没有闹钟" - "时间无效:%3$s %1$d:%2$d" - 未设置 %1$d:%2$d 的闹钟 + "时间无效:%3$s %1$d:%2$d" + 未设置 %1$d:%2$d 的闹钟 "未设置任何闹钟" "未指定任何标签" "没有闹钟包含此标签" "未设置这个时间的闹钟" - 已关闭 %s 的闹钟 - 已设置 %s 的闹钟 + 已关闭 %s 的闹钟 + 已设置 %s 的闹钟 "已创建定时器" - 已关闭 %d 个定时器 + 已关闭 %d 个定时器 "定时器时长无效" "选择的定时器无效" "没有已结束的定时器" - 距离 %s 的闹钟还有 24 小时以上,因此无法关闭此闹钟 + 距离 %s 的闹钟还有 24 小时以上,因此无法关闭此闹钟 "关闭闹钟" "请选择要关闭的闹钟" "目前没有正在响铃的闹钟" - %s 的闹钟已延后 10 分钟 + %s 的闹钟已延后 10 分钟 设备翻转操作 设备摇晃操作 触发闹钟和定时器 @@ -334,7 +330,7 @@ 版本 数字时钟 Material You 创建闹钟时启用振动 - 版本 %1$s + 版本 %1$s 除了设置闹钟外,这款应用还具有定时器和秒表模式。\n\n为了使所有模块的通知都能正确显示,尤其是为了触发闹钟,我们建议为此应用激活通知。 自 Android 14 以来,为了在屏幕关闭时触发闹钟,必须启用“全屏通知”权限。 启用通知 @@ -576,7 +572,7 @@ 点击即可暂停闹钟 在微件上显示下次闹钟 中文(繁体) - 已关闭并删除 %s 的闹钟 + 已关闭并删除 %s 的闹钟 设置闹钟 已设置 %s 日历 @@ -667,7 +663,7 @@ 为每个闹钟使用自定义闹钟停止时长 自定义闹钟停止时长将被重置。%s 访问主要功能页面:\n\n%s - 闹钟已关闭。已重新设置 %s。 + 闹钟已关闭。已重新设置 %s。 模糊强度 选择背景图片 图片已成功选择 @@ -715,16 +711,77 @@ 大小周大周 大小周小周 单休 - + 节假日闹钟 - + 更新节假日数据 - + 手动更新节假日和工作日数据 - + 节假日数据 URL - + 设置节假日和工作日数据的 URL - + 输入节假日和工作日数据的 URL - +从不 + 1 分钟 + 主时钟自定义颜色 + • 将闹钟设置为特定日期;\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+ 的动态配色; + 在微件上显示城市 + 城市时钟自定义颜色 + 城市名称自定义颜色 + 贡献者 + 致谢 + 日期颜色 + 下次闹钟颜色 + 使用主时钟的默认颜色 + 使用日期的默认颜色 + 日期自定义颜色 + 使用城市时钟的默认颜色 + 使用城市名称的默认颜色 + 主时钟的最大字体大小 + 下次闹钟使用默认颜色 + 下次闹钟自定义颜色 + 样式和颜色 + 关闭 + 自定义闹钟显示 + 秒针颜色 + 闹钟字体大小 + 访问 %1$s 的 GitHub 页面: +\n +\n%2$s + 自定义小时颜色 + 使用分钟默认颜色 + 使用小时默认颜色 + 自定义分钟颜色 + “下次闹钟”和“没有预定的闹钟”标题自定义颜色 + 使用默认颜色作为闹钟标题 + 闹钟标题自定义颜色 + 微件最大字体大小 + 为“下次闹钟”和“没有预定的闹钟”标题使用默认颜色 + 按名称 + 1 分钟 + 2 分钟 + 3 分钟 + 4 分钟 + 5 分钟 + 6 分钟 + 7 分钟 + 9 分钟 + 10 分钟 + 30 分钟 + 1 小时 + 8 分钟 + 10 秒 + 20 秒 + 30 秒 + 5 秒 + 已删除 %s 的临时闹钟 + 手动(通过拖动) + 无秒针 + 有秒针 + 此微件仅可从 Android 12 配置 + 选择样式 + 查看资料 + 从不 + \ 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 5eae5e21d..ce6b33bae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1574,4 +1574,19 @@ Vibration - \ No newline at end of file + Default + Soft + Strong + Heartbeat + Escalating + Tick tock + End of ringtone + Disconnect external audio device to adjust volume + External audio device volume + Connect external audio device to adjust volume + Statutory Workdays (Skip Holiday) + Big Week (6 days work) + Small Week (5 days work) + Single Day Off (6 days work) + None + diff --git a/duplicates.txt b/duplicates.txt new file mode 100644 index 000000000..32a365364 --- /dev/null +++ b/duplicates.txt @@ -0,0 +1,9 @@ +connect_external_audio_device_title +disconnect_external_audio_device_title +external_audio_device_volume_title +vibration_pattern_default_title +vibration_pattern_escalating_title +vibration_pattern_heartbeat_title +vibration_pattern_soft_title +vibration_pattern_strong_title +vibration_pattern_tick_tock_title diff --git a/restore_strings.py b/restore_strings.py new file mode 100644 index 000000000..15f23a79a --- /dev/null +++ b/restore_strings.py @@ -0,0 +1,58 @@ +import xml.etree.ElementTree as ET +import sys +import os + +def restore_strings(old_file, new_file): + if not os.path.exists(old_file) or not os.path.exists(new_file): + print(f"File not found: {old_file} or {new_file}") + return + + # Use a simpler parser that handles comments and preserves some structure + def get_strings(file_path): + try: + tree = ET.parse(file_path) + root = tree.getroot() + return {child.get('name'): child for child in root if child.tag == 'string'} + except Exception as e: + print(f"Error parsing {file_path}: {e}") + return {} + + old_strings = get_strings(old_file) + new_strings = get_strings(new_file) + + missing_names = set(old_strings.keys()) - set(new_strings.keys()) + + if not missing_names: + print(f"No strings missing in {new_file}") + return + + # Read the current file to append at the end correctly + with open(new_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # Find the closing resources tag + closing_tag_index = -1 + for i in range(len(lines) - 1, -1, -1): + if '' in lines[i]: + closing_tag_index = i + break + + if closing_tag_index == -1: + print(f"Could not find closing tag in {new_file}") + return + + new_content = [] + for name in sorted(missing_names): + element = old_strings[name] + xml_str = ET.tostring(element, encoding='unicode').strip() + new_content.append(f" {xml_str}\n") + + lines.insert(closing_tag_index, "".join(new_content)) + + with open(new_file, 'w', encoding='utf-8') as f: + f.writelines(lines) + + print(f"Restored {len(missing_names)} strings to {new_file}") + +restore_strings('strings_old.xml', 'app/src/main/res/values/strings.xml') +restore_strings('strings_zh_old.xml', 'app/src/main/res/values-zh-rCN/strings.xml') diff --git a/strings_old.xml b/strings_old.xml new file mode 100644 index 000000000..748cc42c3 --- /dev/null +++ b/strings_old.xml @@ -0,0 +1,1458 @@ + + + + + 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 + + Snoozed + + + + 1 min + + %d min + + + Alarm off + + Alarm off and deleted + + Snooze + + + + Snoozing for 1 minute. + + Snoozing for %d minutes. + + + 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 + + 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. + + + + 1 day + + %s days + + + + + 1 hour + + %s hours + + + + 1 hr + + %s hr + + + + + 1 minute + + %s minutes + + + + 1 min + + %s min + + + + + 1 second + + %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 + + 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 + + Lap times: + + 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 + + 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 + + Timer + + + Missed timer: %s + + Pause + + Reset all timers + %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 + + + "Marshall Islands" + "Midway Island" + "Hawaii" + "Alaska" + "Pacific Time" + "Tijuana" + "Arizona" + "Chihuahua" + "Mountain Time" + "Central America" + "Central Time" + "Mexico City" + "Saskatchewan" + "Bogota" + "Eastern Time" + "Venezuela" + "Atlantic Time (Barbados)" + "Atlantic Time (Canada)" + "Manaus" + "Santiago" + "Newfoundland" + "Brasilia" + "Buenos Aires" + "Greenland" + "Montevideo" + "Mid-Atlantic" + "Azores" + "Cape Verde Islands" + "Casablanca" + "London, Dublin" + "Amsterdam, Berlin" + "Belgrade" + "Brussels" + "Sarajevo" + "Windhoek" + "W. Africa Time" + "Amman, Jordan" + "Athens, Istanbul" + "Beirut, Lebanon" + "Cairo" + "Helsinki" + "Jerusalem" + "Minsk" + "Harare" + "Baghdad" + "Moscow" + "Kuwait" + "Nairobi" + "Tehran" + "Baku" + "Tbilisi" + "Yerevan" + "Dubai" + "Kabul" + "Islamabad, Karachi" + "Ural'sk" + "Yekaterinburg" + "Kolkata" + "Sri Lanka" + "Kathmandu" + "Astana" + "Yangon" + "Krasnoyarsk" + "Bangkok" + "Beijing" + "Hong Kong" + "Irkutsk" + "Kuala Lumpur" + "Perth" + "Taipei" + "Seoul" + "Tokyo, Osaka" + "Yakutsk" + "Adelaide" + "Darwin" + "Brisbane" + "Hobart" + "Sydney, Canberra" + "Vladivostok" + "Guam" + "Magadan" + "Auckland" + "Fiji" + "Tonga" + "Jakarta" + + + + Pacific/Majuro + Pacific/Midway + Pacific/Honolulu + America/Anchorage + America/Los_Angeles + America/Tijuana + America/Phoenix + America/Chihuahua + America/Denver + America/Costa_Rica + America/Chicago + America/Mexico_City + America/Regina + America/Bogota + America/New_York + America/Caracas + America/Barbados + America/Halifax + America/Manaus + America/Santiago + America/St_Johns + America/Sao_Paulo + America/Argentina/Buenos_Aires + America/Godthab + America/Montevideo + Atlantic/South_Georgia + Atlantic/Azores + Atlantic/Cape_Verde + Africa/Casablanca + Europe/London + Europe/Amsterdam + Europe/Belgrade + Europe/Brussels + Europe/Sarajevo + Africa/Windhoek + Africa/Brazzaville + Asia/Amman + Europe/Athens + Asia/Beirut + Africa/Cairo + Europe/Helsinki + Asia/Jerusalem + Europe/Minsk + Africa/Harare + Asia/Baghdad + Europe/Moscow + Asia/Kuwait + Africa/Nairobi + Asia/Tehran + Asia/Baku + Asia/Tbilisi + Asia/Yerevan + Asia/Dubai + Asia/Kabul + Asia/Karachi + Asia/Oral + Asia/Yekaterinburg + Asia/Calcutta + Asia/Colombo + Asia/Katmandu + Asia/Almaty + Asia/Rangoon + Asia/Krasnoyarsk + Asia/Bangkok + Asia/Shanghai + Asia/Hong_Kong + Asia/Irkutsk + Asia/Kuala_Lumpur + Australia/Perth + Asia/Taipei + Asia/Seoul + Asia/Tokyo + Asia/Yakutsk + Australia/Adelaide + Australia/Darwin + Australia/Brisbane + Australia/Hobart + Australia/Sydney + Asia/Vladivostok + Pacific/Guam + Asia/Magadan + Pacific/Auckland + Pacific/Fiji + Pacific/Tongatapu + Asia/Jakarta + + + About + + 100% FOSS Clock, based on AOSP + + Version + + 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; + \n\n• Turn off/postpone the alarm with the power button or volume buttons; + \n\n• For some Snapdragon devices only, the alarm is triggered when they are switched off; + \n\t\t Unfortunately, this feature may not work on some devices despite the presence of the \"com.qualcomm.qti.poweroffalarm\" system app. + \n\n• Swipe to delete an alarm; + \n\n• Duplicate alarms; + \n\n• Customize alarm title; + \n\n• Customizable ringtone; + \n\n• Light, dark or system theme; + \n\n• AMOLED mode for dark theme; + \n\n• Digital or analog clock style; + \n\n• Display home time when traveling; + \n\n• Display the time in many cities around the world; + \n\n• Timer and stopwatch included; + \n\n• Possibility of sharing your stopwatch with your contacts; + \n\n• Customizable screensaver; + \n\n• Modern widgets; + \n\n• Customizable widgets; + \n\n• Support for tiles in quick settings (for Android 7+); + \n\n• Backup and restore application data (except custom ringtones); + \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 + + 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 + + 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 + + No Alarms + + 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 + + Timer created + + + Timer 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 + + Dismiss alarm + + Pick which alarm to dismiss + + No firing alarms + + %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 + Later + + ► Light, dark or system theme; <br> <br> + ► Customizable screensaver; <br> <br> + ► Customizable widgets; <br> <br> + ► Support for tiles in quick settings (for Android 7+); <br> <br> + ► Backup and restore application data (except custom ringtones); <br> <br> + ► Material design and dynamic colors; <br> <br> + ► Set the alarms to a specific date; <br> <br> + ► Swipe to delete an alarm; <br> <br> + ► Duplicate an alarm; <br> <br> + ► 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 + + To ensure that the application works properly, <b>some of your device settings must be changed:</b> <br> <br> + • <b>BATTERY OPTIMIZATION;</b> <br> <br> + • <b>NOTIFICATIONS;</b> <br> <br> + %s + <i>Depending on the Android version of your device, some of these settings are already optimized (e.g. notifications).</i> <br> <br> + You can change them now or later in the permission management settings: + • <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:  + Highly recommended + Status:  + 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 diff --git a/strings_zh_old.xml b/strings_zh_old.xml new file mode 100644 index 000000000..f7356ed20 --- /dev/null +++ b/strings_zh_old.xml @@ -0,0 +1,661 @@ + + + "时钟" + "闹钟" + "振动" + 选择时间 + 请选择日期 + "删除" + "默认闹钟提示音" + "闹钟提示音" + "定时器提示音" + "新增" + "移除" + "使用此提示音的闹钟和定时器将改为播放默认提示音。" + "您的提示音" + "设备提示音" + "无法访问提示音内容。" + 计时结束 + "明天" + "今天" + "关闭" + "错过的闹钟" + %1$s - %2$s + "已暂停" + 工作日设置 + 法定工作日 + 大小周大周 + 大小周小周 + 单休 + + %d 分钟 + + "闹钟已关闭" + "暂停" + + %d 分钟后提醒。 + + 闹钟已暂停,将于 %s 再响 + "预定的闹钟" + "错过的闹钟已删除" + "还剩不到 1 分钟" + %1$s %3$s + %2$s %3$s + 还剩 %1$s %2$s + %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 分钟,%2$s + "还剩" + "还剩" + 闹钟时间已设为不到 1 分钟后。 + 闹钟时间已设为 %1$s后。 + 闹钟时间已设为 %2$s后。 + 闹钟时间已设为 %1$s %2$s后。 + 闹钟时间已设为 %3$s后。 + 闹钟时间已设为 %1$s %3$s后。 + 闹钟时间已设为 %2$s %3$s后。 + 闹钟时间已设为 %1$s %2$s %3$s后。 + + %s + + + %s 小时 + + + %s 小时 + + + %s 分钟 + + + %s 分钟 + + + %s + + "每天" + ", " + "正在加载…" + 指针时钟 + "数字时钟" + "设置" + "暂停时长" + 逐步增大音量 + 之后静音 + 从不 + 1 分钟 + 关闭 + 一周的第一天 + 星期六 + 星期日 + 星期一 + "闹钟音量" + "静音" + "未知" + 闹钟音量已静音 + "取消静音" + 默认闹钟铃声为静音 + "更改" + 设备设置为完全静音 + "音量按钮" + 暂停 + 关闭 + 控制音量 + + "标签" + "铃声" + "闹钟" + "定时器" + "时钟" + "秒表" + "添加闹钟" + "城市" + 屏幕保护程序 + "按时间排序" + "按名称排序" + "所选城市" + "继续" + "重置" + "开始" + "暂停" + "一圈" + "分享" + "小时" + "分" + "秒" + %1$s, %2$s, %3$s + # %d + # %02d + 我的时间是 %s + "各圈时间:" + %d + "添加定时器" + "开始" + 删除 %s + 添加 %s 分钟 + 添加 %s 分钟 + "停止" + "停止所有定时器" + 定时器已取消 + "时间到" + %d 个定时器已到期 + %d 个定时器已错过 + "定时器" + 错过的定时器:%s + "暂停" + "重置所有定时器" + %1$d:%2$02d:%3$02d + %1$d:%2$02d + "您简直就像闪电侠。" + "享受一下您大汗淋漓的成果吧。" + "Android 以速度快著称,但还跟不上您!" + "哇噻!" + "果然是高手。" + "迅雷不及掩耳之势。" + "再来一次吧。" + "小菜一碟。" + "您简直比得上飞毛腿。" + "冲击光速!" + + "城市" + "时钟" + 样式 + "显示含秒数的时间" + "更改日期和时间" + 指针 + 数字 + 自动显示家中时间 + 在其他时区旅行时显示家中时间 + 家中时区 + %s 已选中 + %s 已取消选中 + + "马绍尔群岛" + "中途岛" + "夏威夷" + "阿拉斯加" + "太平洋时间" + "蒂华纳" + "亚利桑那" + "奇瓦瓦" + "山区时间" + "中美洲" + "中部时间" + "墨西哥城" + "萨斯喀彻温" + "波哥大" + "东部时间" + "委内瑞拉" + "大西洋时间(巴巴多斯)" + "大西洋时间(加拿大)" + "马瑙斯" + "圣地亚哥" + "纽芬兰" + "巴西利亚" + "布宜诺斯艾利斯" + "格陵兰岛" + "蒙得维的亚" + "大西洋中部地区" + "亚述尔群岛" + "佛得角群岛" + "卡萨布兰卡" + "伦敦、都柏林" + "阿姆斯特丹、柏林" + "贝尔格莱德" + "布鲁塞尔" + "萨拉热窝" + "温得和克" + "西非时间" + "安曼、约旦" + "雅典、伊斯坦布尔" + "贝鲁特、黎巴嫩" + "开罗" + "赫尔辛基" + "耶路撒冷" + "明斯克" + "哈拉雷" + "巴格达" + "莫斯科" + "科威特" + "奈洛比" + "德黑兰" + "巴库" + "第比利斯" + "埃里温" + "迪拜" + "喀布尔" + "伊斯兰堡、卡拉奇" + "乌拉尔斯克" + "叶卡捷琳堡" + "加尔各答" + "斯里兰卡" + "加德满都" + "阿斯塔纳" + "仰光" + "克拉斯诺亚尔斯克" + "曼谷" + "北京" + "香港" + "伊尔库茨克" + "吉隆坡" + "珀斯" + "台北" + "首尔" + "东京、大阪" + "雅库茨克" + "阿德莱德" + "达尔文" + "布里斯班" + "霍巴特" + "悉尼、堪培拉" + "海参崴" + "关岛" + "马加丹" + "奥克兰" + "斐济" + "汤加" + "雅加达" + + "新闹钟" + 创建新闹钟 + "新定时器" + 创建新定时器 + "启动" + "启动秒表" + "暂停" + "暂停秒表" + 屏幕保护程序 + 启动屏幕保护程序 + "闹钟" + 处理来自定时器通知的操作。 + 处理来自秒表通知的操作。 + "已暂停" + 向左滑动以暂停,向右滑动以关闭 + "定时器" + "定时器振动" + "定时器已暂停" + %d 个定时器 + "展开闹钟" + 收起闹钟 + "闹钟已删除" + / %s + %1$s + %1$s + %1$s %2$s + %1$s %2$s + 明天,%1$s + 昨天,%1$s + 下次闹钟:%s + "没有闹钟" + "时间无效:%3$s %1$d:%2$d" + 未设置 %1$d:%2$d 的闹钟 + "未设置任何闹钟" + "未指定任何标签" + "没有闹钟包含此标签" + "未设置这个时间的闹钟" + 已关闭 %s 的闹钟 + 已设置 %s 的闹钟 + "已创建定时器" + + 已关闭 %d 个定时器 + + "定时器时长无效" + "选择的定时器无效" + "没有已结束的定时器" + 距离 %s 的闹钟还有 24 小时以上,因此无法关闭此闹钟 + "关闭闹钟" + "请选择要关闭的闹钟" + "目前没有正在响铃的闹钟" + %s 的闹钟已延后 10 分钟 + 设备翻转操作 + 设备摇晃操作 + 触发闹钟和定时器 + 错过的闹钟 + 暂停的闹钟 + 预定的闹钟 + 秒表 + 定时器 + 铃声结束 + 主时钟自定义颜色 + 必要:  + 强烈推荐 + 电池优化 + 稍后 + 现在 + 主要特点 + 基于 AOSP 的 100% FOSS 时钟 + 撤销访问权限 + 重复 + 已选择 + 更多选项 + 关于 + 版本 + 数字时钟 Material You + 创建闹钟时启用振动 + 版本 %1$s + 除了设置闹钟外,这款应用还具有定时器和秒表模式。\n\n为了使所有模块的通知都能正确显示,尤其是为了触发闹钟,我们建议为此应用激活通知。 + 自 Android 14 以来,为了在屏幕关闭时触发闹钟,必须启用“全屏通知”权限。 + 启用通知 + 通知 + 启用全屏通知 + 全屏通知 + 默认情况下,Android 系统将所有用户安装的应用配置为优化以节省电池寿命。\n\n虽然这对于许多应用来说是可以接受的,但此应用是闹钟应用,如果配置为优化电池,通常会导致问题。一旦启用,Android 系统可以随时终止此应用和/或取消已设置的闹钟,这将对应用的运行产生不利影响。\n\n我们建议将此应用列入电池优化白名单。 + 指针时钟 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 查看 + 设置样式、默认铃声、音量、关闭或暂停闹钟的操作、每周的第一天和提醒通知时间 + 时间选择器样式 + 定时器标签 + 更新日志 + 闹钟已创建 + 为了确保应用程序正常工作,<b>必须更改某些设备设置:</b> <br> <br> • <b>电池优化;</b> <br> <br> • <b>通知;</b> <br> <br> %s <i>根据设备的 Android 版本,其中一些设置已经过优化(例如通知)。</i> <br> <br> 您可以现在或以后在权限管理设置中更改它们: + …还有更多! + 日期以斜体显示 + 背景颜色 + 在微件上显示城市 + 城市时钟自定义颜色 + 城市名称自定义颜色 + 显示应用程序正常工作所需的权限 + 滑动以关闭或暂停闹钟 + 电源按钮 + 添加标签 + 设置样式、时区和更改设备日期和时间 + + %s + 添加到定时器的分钟数 + 主要特点 + 开放源代码许可 + GNU General Public License v3.0 + 贡献者 + 致谢 + 关闭 + 向左或向右滑动以关闭 + 设置默认铃声、音量和定时器排序 + 添加到定时器的默认时间 + 设置屏幕保护程序的样式、颜色和字体 + 选择/启用屏幕保护程序 + 打开 Android 屏幕保护程序主设置 + 动态时钟颜色 + 时钟颜色 + 日期颜色 + 亮度 + 数字时钟以粗体显示 + 数字时钟以斜体显示 + 日期以粗体显示 + 预览 + 立即启动屏幕保护程序 + 下次闹钟颜色 + 下次闹钟以粗体显示 + 下次闹钟以斜体显示 + 版本 %s + 重要信息 + 退出 + 是否确定要离开应用程序? + 显示背景 + 使用主时钟的默认颜色 + 使用日期的默认颜色 + 日期自定义颜色 + 使用城市时钟的默认颜色 + 使用城市名称的默认颜色 + 主时钟的最大字体大小 + 必须禁用“在微件上显示城市”才能访问此设置 + 权限管理 + • <b>全屏通知;</b> <br> <br> + 下次闹钟使用默认颜色 + 下次闹钟自定义颜色 + 闹钟标签 + 点击此处显示权限管理页面。 + 样式 + 样式和颜色 + 字体 + 杂项 + 铃声 + 操作 + 界面 + 设置应用于应用程序的样式和颜色 + 主题 + 强调色 + 深色主题模式 + 显示卡片背景 + 显示背景边框 + 启用振动 + 只有某些界面按钮受到影响(例如:定时器播放/暂停按钮或开关) + 显示某些应用程序元素(闹钟、城市、定时器按钮等)的背景边框 + 忽略电池优化 + 已拒绝 + 关闭 + 是否确定要撤销此权限? + 状态:  + 已授予 + 自定义闹钟显示 + “暂停”按钮的颜色 + “关闭”按钮的颜色 + 闹钟按钮颜色 + 定时器结束时的透明背景 + 闹钟标题颜色 + 秒针颜色 + 闹钟字体大小 + 闹钟标题字体大小 + 了解更新内容: +\n +\n%s + 访问项目 GitHub 页面: \n \n%s + 阅读许可:\n\n%s + 访问 %1$s 的 GitHub 页面: +\n +\n%2$s + 立式数字时钟 Material You + 微件 + 立式数字时钟 + 自定义小时颜色 + 使用分钟默认颜色 + 使用小时默认颜色 + 自定义分钟颜色 + 标准 + Material You + 设置微件的样式、颜色和字体大小 + 在底部导航栏菜单中显示标签页指示器 + 下次闹钟 Material You + “下次闹钟”和“没有预定的闹钟”标题自定义颜色 + 使用默认颜色作为闹钟标题 + 闹钟标题自定义颜色 + 微件最大字体大小 + 下次闹钟 + 没有预定的闹钟 + 为“下次闹钟”和“没有预定的闹钟”标题使用默认颜色 + 切换屏幕时启用淡化过渡 + 下次闹钟 + 排序定时器 + 权限已拒绝 + 默认 + 浅色 + 深色 + 系统 + 蓝灰色 + 棕色 + 绿色 + 靛蓝色 + 橙色 + 粉红色 + 红色 + AMOLED + 30 分钟前 + 1 小时前 + 2 小时前 + 4 小时前 + 6 小时前 + 8 小时前 + 10 小时前 + 12 小时前 + 按时长(升序) + 按时长(降序) + 按名称 + 1 分钟 + 2 分钟 + 3 分钟 + 4 分钟 + 5 分钟 + 6 分钟 + 7 分钟 + 9 分钟 + 10 分钟 + 30 分钟 + 1 小时 + + 8 分钟 + 10 秒 + 20 秒 + 30 秒 + 5 秒 + 关闭并删除 + 向左滑动以暂停,向右滑动以关闭并删除闹钟 + 向左或向右滑动以关闭并删除闹钟 + 关闭后立即删除闹钟 + 闹钟已关闭并删除 + 预定的临时闹钟 + 默认启用自动删除临时闹钟 + 可以在扩展闹钟视图中为每个闹钟更改此设置 + 已删除 %s 的临时闹钟 + 备份和还原 + 备份或还原应用程序数据,自定义铃声除外 + 是否要备份或还原应用程序数据? + 备份 + 还原 + 备份已完成 + 还原已完成 + 翻转设备以停止闹钟 + 摇晃设备以停止闹钟 + 将强调色应用于夜间模式 + 夜间模式的强调色 + 手动(通过拖动) + 使用音量按钮重置已结束的定时器 + 仅适用于锁屏 + 使用电源按钮重置已结束的定时器 + 开始/暂停 + 音量调高 + 音量调低 + 音量调高(长按后) + 音量调低(长按后) + 设置音量按钮的操作 + 黑色 + 紫色 + 黄色 + 日间模式的强调色 + 显示铃声标题 + 是否确定要删除“%s”定时器? + 删除定时器之前显示警告 + 删除定时器 + 闹钟暂停时双次振动,闹钟关闭时单次振动 + 闹钟暂停或关闭时启用振动 + 触发闹钟时打开后置闪光灯 + 闪光灯 + 访问 Codeberg 页面:\n\n%s + 在 Codeberg 上翻译 + 摇晃强度 + 语言 + 系统语言 + 法语 + 爱沙尼亚语 + 意大利语 + 波兰语 + 葡萄牙语 + 塞尔维亚语 + 西班牙语 + 乌克兰语 + 中文(简体) + 德语 + 英语(美国) + 仅适用于小米设备 + 在小米设备上,您应该从设备设置中手动启用“在锁屏上显示”权限,以便应用程序正常工作。 + 在锁屏上显示 + 在锁屏上显示 + 旋转 + 指针 (Material) + 荷兰语 + 俄语 + 无秒针 + 有秒针 + 此微件仅可从 Android 12 配置 + 选择样式 + 上次使用的标签页‌ + 输入的值不正确 + 打开应用时显示的标签页‌ + 请输入 0 到 60 分钟之间的值 + 土耳其语 + 重置设置 + 是否确定要重置所有应用设置?\n\n注意:不会重置自定义铃声。 + 查看资料 + 蓝色 + 重置已完成 + 韩语 + 在微件上显示日期 + 保持屏幕开启 + 在工具栏中显示标题 + 显示标签页标题 + 始终 + 从不 + 选择时 + 捷克语 + 注意:无论此设置的状态如何,如果定时器或秒表正在运行,并且显示了相应的标签页,则屏幕将始终开启 + “关闭”标题的颜色 + 点击即可关闭闹钟 + 点击即可关闭并删除闹钟 + 铃声标题颜色 + 滑动区域颜色 + “暂停”标题的颜色 + 点击即可暂停闹钟 + 在微件上显示下次闹钟 + 中文(繁体) + 已关闭并删除 %s 的闹钟 + 设置闹钟 + 已设置于 %s + 日历 + 文本 + 日期选择器样式 + 长按选择文件夹 + 随机铃声 + 警告 + 添加 %1$s 分钟 %2$s 秒 + 添加 %1$s 分钟 %2$s 秒 + 分钟 + 请输入 0 到 59 秒之间的值 + + 请输入 0 到 59 秒之间的值 + 小时 + 新定时器时长 + 请输入 0 到 24 小时之间的值 + 请输入 0 到 59 分钟之间的值 + 启用铃声自动路由到蓝牙设备 + 定时器创建视图样式 + 键盘 + 使用高级音频播放 + 隐藏上午/下午文本 + 提升蓝牙兼容性,防止重复播放时声音中断 \n\n注意:部分铃声可能会有轻微音频故障 + 使用系统媒体音量 + 蓝牙设备的闹钟音量 + 请连接蓝牙设备以访问此设置 + 请断开蓝牙设备的连接以访问此设置 + 仅在连接蓝牙设备时有效 + 将边距应用于微件两侧 + + 节假日闹钟 + + 更新节假日数据 + + 手动更新节假日和工作日数据 + + 节假日数据 URL + + 设置节假日和工作日数据的 URL + + 输入节假日和工作日数据的 URL +