From 21db56c4e9ee4a16aad3010c7fb3eda7caaba571 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 29 Dec 2021 12:19:28 -0600 Subject: [PATCH] Completion sound --- .../todoroo/astrid/service/TaskCompleter.kt | 25 +++++- .../main/java/org/tasks/extensions/Context.kt | 9 ++ .../org/tasks/injection/ApplicationModule.kt | 5 ++ .../java/org/tasks/preferences/Preferences.kt | 26 ++++-- .../preferences/fragments/Notifications.kt | 85 +++++++++++++----- app/src/main/res/raw/long_rising_tone.m4a | Bin 0 -> 12069 bytes app/src/main/res/values/keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../res/xml/preferences_notifications.xml | 5 ++ 9 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 app/src/main/res/raw/long_rising_tone.m4a diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt index 01c104c97..1a9c9bc7a 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt @@ -1,16 +1,25 @@ package com.todoroo.astrid.service +import android.app.NotificationManager +import android.app.NotificationManager.INTERRUPTION_FILTER_ALL +import android.content.Context +import android.media.MediaPlayer import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.data.Task +import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.data.GoogleTaskDao +import org.tasks.preferences.Preferences import timber.log.Timber import javax.inject.Inject class TaskCompleter @Inject internal constructor( - private val taskDao: TaskDao, - private val googleTaskDao: GoogleTaskDao) { - + @ApplicationContext private val context: Context, + private val taskDao: TaskDao, + private val googleTaskDao: GoogleTaskDao, + private val preferences: Preferences, + private val notificationManager: NotificationManager, +) { suspend fun setComplete(taskId: Long) = taskDao .fetch(taskId) @@ -43,5 +52,15 @@ class TaskCompleter @Inject internal constructor( } taskDao.save(task) } + if ( + tasks.size == 1 && + completionDate > 0 && + notificationManager.currentInterruptionFilter == INTERRUPTION_FILTER_ALL + ) { + preferences + .completionSound + ?.takeUnless { preferences.isCurrentlyQuietHours } + ?.let { MediaPlayer.create(context, it).start() } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/extensions/Context.kt b/app/src/main/java/org/tasks/extensions/Context.kt index 73cb53648..f45cb50cf 100644 --- a/app/src/main/java/org/tasks/extensions/Context.kt +++ b/app/src/main/java/org/tasks/extensions/Context.kt @@ -1,11 +1,13 @@ package org.tasks.extensions import android.content.ActivityNotFoundException +import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.Intent.ACTION_VIEW import android.net.Uri import android.widget.Toast +import androidx.annotation.AnyRes import androidx.browser.customtabs.CustomTabsIntent import org.tasks.R @@ -46,4 +48,11 @@ object Context { fun Context.toast(text: String?, duration: Int = Toast.LENGTH_LONG) = text?.let { Toast.makeText(this, it, duration).show() } + + fun Context.getResourceUri(@AnyRes res: Int) = + Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(packageName) + .path(res.toString()) + .build() } diff --git a/app/src/main/java/org/tasks/injection/ApplicationModule.kt b/app/src/main/java/org/tasks/injection/ApplicationModule.kt index 0358b9fdb..0a7cec470 100644 --- a/app/src/main/java/org/tasks/injection/ApplicationModule.kt +++ b/app/src/main/java/org/tasks/injection/ApplicationModule.kt @@ -1,5 +1,6 @@ package org.tasks.injection +import android.app.NotificationManager import android.content.Context import com.todoroo.astrid.dao.Database import dagger.Module @@ -113,4 +114,8 @@ class ApplicationModule { fun providesCoroutineScope( @DefaultDispatcher defaultDispatcher: CoroutineDispatcher ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher) + + @Provides + fun providesNotificationManager(@ApplicationContext context: Context) = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/preferences/Preferences.kt b/app/src/main/java/org/tasks/preferences/Preferences.kt index 6b7d86b54..448af898d 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.kt +++ b/app/src/main/java/org/tasks/preferences/Preferences.kt @@ -10,6 +10,7 @@ import android.media.RingtoneManager import android.net.Uri import android.os.Binder import androidx.core.app.NotificationCompat +import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager import com.todoroo.andlib.utility.DateUtilities @@ -25,6 +26,7 @@ import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.billing.Purchase import org.tasks.data.TaskAttachment +import org.tasks.extensions.Context.getResourceUri import org.tasks.themes.ColorProvider import org.tasks.themes.ThemeBase import org.tasks.time.DateTime @@ -141,13 +143,25 @@ class Preferences @JvmOverloads constructor( } val ringtone: Uri? - get() { - val ringtone = getStringValue(R.string.p_rmd_ringtone) - ?: return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - return if ("" == ringtone) { - null - } else Uri.parse(ringtone) + get() = getRingtone( + R.string.p_rmd_ringtone, + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + ) + + val completionSound: Uri? + get() = getRingtone( + R.string.p_completion_ringtone, + context.getResourceUri(R.raw.long_rising_tone) + ) + + private fun getRingtone(pref: Int, default: Uri): Uri? { + val ringtone = getStringValue(pref) + return when { + ringtone == null -> default + ringtone.isNotBlank() -> ringtone.toUri() + else -> null } + } val isTrackingEnabled: Boolean get() = getBoolean(R.string.p_collect_statistics, true) diff --git a/app/src/main/java/org/tasks/preferences/fragments/Notifications.kt b/app/src/main/java/org/tasks/preferences/fragments/Notifications.kt index c2607bdf3..206db5310 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/Notifications.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/Notifications.kt @@ -9,6 +9,7 @@ import android.os.Bundle import android.os.PowerManager import android.provider.Settings import android.speech.tts.TextToSpeech +import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.SwitchPreferenceCompat @@ -21,6 +22,7 @@ import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.activities.FilterSelectionActivity import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker +import org.tasks.extensions.Context.getResourceUri import org.tasks.injection.InjectingPreferenceFragment import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.Preferences @@ -37,6 +39,7 @@ private const val REQUEST_DEFAULT_REMIND = 10003 private const val REQUEST_BADGE_LIST = 10004 private const val REQUEST_CODE_ALERT_RINGTONE = 10005 private const val REQUEST_CODE_TTS_CHECK = 10006 +private const val REQUEST_CODE_COMPLETION_SOUND = 10007 @AndroidEntryPoint class Notifications : InjectingPreferenceFragment() { @@ -59,6 +62,7 @@ class Notifications : InjectingPreferenceFragment() { rescheduleNotificationsOnChange(true, R.string.p_bundle_notifications) initializeRingtonePreference() + initializeCompletionSoundPreference() initializeTimePreference(getDefaultRemindTimePreference()!!, REQUEST_DEFAULT_REMIND) initializeTimePreference(getQuietStartPreference()!!, REQUEST_QUIET_START) initializeTimePreference(getQuietEndPreference()!!, REQUEST_QUIET_END) @@ -175,19 +179,27 @@ class Notifications : InjectingPreferenceFragment() { } override fun onPreferenceTreeClick(preference: Preference?): Boolean = - if (preference!!.key == getString(R.string.p_rmd_ringtone)) { + when (preference!!.key) { + getString(R.string.p_rmd_ringtone), + getString(R.string.p_completion_ringtone) -> { val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtoneManager.TYPE_NOTIFICATION + ) intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) intent.putExtra( RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI ) - val existingValue: String? = preferences.getStringValue(R.string.p_rmd_ringtone) + val existingValue: String? = preferences.getStringValue(preference.key) if (existingValue != null) { if (existingValue.isEmpty()) { - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, null as Uri?) + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, + null as Uri? + ) } else { intent.putExtra( RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, @@ -200,11 +212,20 @@ class Notifications : InjectingPreferenceFragment() { Settings.System.DEFAULT_NOTIFICATION_URI ) } - startActivityForResult(intent, REQUEST_CODE_ALERT_RINGTONE) + startActivityForResult( + intent, + if (preference.key == getString(R.string.p_rmd_ringtone)) { + REQUEST_CODE_ALERT_RINGTONE + } else { + REQUEST_CODE_COMPLETION_SOUND + } + ) true - } else { + } + else -> { super.onPreferenceTreeClick(preference) } + } private fun getQuietStartPreference(): TimePreference? = getTimePreference(R.string.p_rmd_quietStart) @@ -227,29 +248,41 @@ class Notifications : InjectingPreferenceFragment() { } } - private fun initializeRingtonePreference() { + private fun initializeRingtonePreference() = + initializeRingtonePreference( + R.string.p_rmd_ringtone, + R.string.silent, + ) + + private fun initializeCompletionSoundPreference() = + initializeRingtonePreference( + R.string.p_completion_ringtone, + R.string.none, + requireContext().getResourceUri(R.raw.long_rising_tone) + ) + + private fun initializeRingtonePreference(pref: Int, noneRes: Int, default: Uri? = null) { val ringtoneChangedListener = Preference.OnPreferenceChangeListener { preference: Preference, value: Any? -> if ("" == value) { - preference.setSummary(R.string.silent) + preference.setSummary(noneRes) } else { - val ringtone = RingtoneManager.getRingtone( - context, - if (value == null) Settings.System.DEFAULT_NOTIFICATION_URI else Uri.parse( - value as String? - ) - ) - preference.summary = if (ringtone == null) "" else ringtone.getTitle(context) + val uri = + (value as? String?)?.toUri() + ?: default + ?: Settings.System.DEFAULT_RINGTONE_URI + preference.summary = if (uri == default) { + getString(R.string.settings_default) + } else { + RingtoneManager.getRingtone(context, uri).getTitle(context) + } } true } - val ringtoneKey = R.string.p_rmd_ringtone - val ringtonePreference: Preference = findPreference(ringtoneKey) + val ringtonePreference = findPreference(pref) ringtonePreference.onPreferenceChangeListener = ringtoneChangedListener - ringtoneChangedListener.onPreferenceChange( - ringtonePreference, - preferences.getStringValue(ringtoneKey) - ) + ringtoneChangedListener + .onPreferenceChange(ringtonePreference, preferences.getStringValue(pref)) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -264,6 +297,16 @@ class Notifications : InjectingPreferenceFragment() { } initializeRingtonePreference() } + REQUEST_CODE_COMPLETION_SOUND -> if (resultCode == RESULT_OK && data != null) { + val ringtone: Uri? = + data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + if (ringtone != null) { + preferences.setString(R.string.p_completion_ringtone, ringtone.toString()) + } else { + preferences.setString(R.string.p_completion_ringtone, "") + } + initializeCompletionSoundPreference() + } REQUEST_QUIET_START -> if (resultCode == RESULT_OK) { getQuietStartPreference()!!.handleTimePickerActivityIntent(data) } diff --git a/app/src/main/res/raw/long_rising_tone.m4a b/app/src/main/res/raw/long_rising_tone.m4a new file mode 100644 index 0000000000000000000000000000000000000000..ef870a2960c603e58daf1189193cab4fdc4c8a2f GIT binary patch literal 12069 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 notification_ringtone + completion_ringtone notif_default_reminder diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f4c8a2ac..841b84b79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -712,4 +712,5 @@ File %1$s contained %2$s.\n\n More options Markdown Enable Markdown in title and description + Play completion sound diff --git a/app/src/main/res/xml/preferences_notifications.xml b/app/src/main/res/xml/preferences_notifications.xml index 5e3354f47..2fd77a633 100644 --- a/app/src/main/res/xml/preferences_notifications.xml +++ b/app/src/main/res/xml/preferences_notifications.xml @@ -60,6 +60,11 @@ android:key="@string/p_rmd_ringtone" android:title="@string/sound" /> + +