Idle: add voice announcement (user configurable).

Fixes of #1174
This commit is contained in:
Dennis Guse
2023-09-23 18:52:28 +02:00
parent 9a50d7bedf
commit 83da4f8f34
40 changed files with 192 additions and 132 deletions
@@ -21,7 +21,7 @@ import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncement;
import de.dennisguse.opentracks.services.announcement.TTSManager;
/**
* @author Jimmy Shih
@@ -33,7 +33,7 @@ public abstract class AbstractActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
// Set volume control stream for text to speech
setVolumeControlStream(VoiceAnnouncement.AUDIO_STREAM);
setVolumeControlStream(TTSManager.AUDIO_STREAM);
setContentView(getRootView());
}
@@ -40,6 +40,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
private final ContentProviderUtils contentProviderUtils;
private final Context context;
private final IdleObserver idleObserver;
private final Handler handler;
@@ -59,8 +60,9 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
private TrackPoint lastStoredTrackPoint;
private TrackPoint lastStoredTrackPointWithLocation;
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, Handler handler) {
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, Handler handler) {
this.context = context;
this.idleObserver = idleObserver;
this.trackPointCreator = trackPointCreator;
this.handler = handler;
contentProviderUtils = new ContentProviderUtils(context);
@@ -165,6 +167,8 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
public void onIdle() {
Log.d(TAG, "Becoming idle");
onNewTrackPoint(trackPointCreator.createIdle());
idleObserver.onIdle();
}
/**
@@ -270,6 +274,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
lastStoredTrackPointWithLocation = lastStoredTrackPoint;
}
} catch (SQLiteException e) {
// TODO Remove; if this is a problem; use a synchronized method.
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
* This is expected to happen extremely rarely (if our listener gets invoked twice at about the same time).
@@ -299,4 +304,8 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
idleDuration = PreferencesUtils.getIdleDurationTimeout();
}
}
public interface IdleObserver {
void onIdle();
}
}
@@ -47,7 +47,7 @@ import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener {
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
private static final String TAG = TrackRecordingService.class.getSimpleName();
@@ -108,7 +108,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
trackPointCreator = new TrackPointCreator(this);
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, handler);
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this , handler);
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -296,11 +296,15 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// Compute temporary track statistics using sensorData and update time.
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.getDataForUI();
voiceAnnouncementManager.update(this, data.first);
voiceAnnouncementManager.announceStatisticsIfNeeded(data.first);
recordingDataObservable.postValue(new RecordingData(data.first, data.second.first, data.second.second));
}
public void onIdle() {
voiceAnnouncementManager.announceIdle();
}
@VisibleForTesting
public void stopUpdateRecordingData() {
handler.removeCallbacks(updateRecordingData);
@@ -31,37 +31,19 @@ import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
/**
* This class will announce the user's {@link TrackStatistics}.
*
* @author Sandor Dornbush
*/
public class VoiceAnnouncement {
public class TTSManager {
public final static int AUDIO_STREAM = TextToSpeech.Engine.DEFAULT_STREAM;
private static final String TAG = VoiceAnnouncement.class.getSimpleName();
private static final String TAG = TTSManager.class.getSimpleName();
private final Context context;
private final AudioManager audioManager;
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private Distance intervalDistance;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
@@ -108,12 +90,9 @@ public class VoiceAnnouncement {
private MediaPlayer ttsFallback;
VoiceAnnouncement(Context context) {
TTSManager(Context context) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
}
public void start() {
@@ -139,7 +118,7 @@ public class VoiceAnnouncement {
}
}
public void announce(@NonNull Track track) {
public void announce(@NonNull Spannable announcement) {
synchronized (this) {
if (!ttsReady) {
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
@@ -166,23 +145,6 @@ public class VoiceAnnouncement {
return;
}
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
if (announcement.length() > 0) {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
@@ -17,6 +17,7 @@ package de.dennisguse.opentracks.services.announcement;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.Spannable;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -27,11 +28,15 @@ import androidx.mediarouter.media.MediaRouter;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
/**
* Execute a periodic task on a time or distance schedule.
@@ -42,9 +47,9 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
private static final String TAG = VoiceAnnouncementManager.class.getSimpleName();
private final TrackRecordingService trackRecordingService;
private final Context context;
private VoiceAnnouncement voiceAnnouncement;
private TTSManager voiceAnnouncement;
private TrackStatistics trackStatistics;
@@ -58,12 +63,22 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
@NonNull
private Duration nextTotalTime = TOTALTIME_OFF;
public VoiceAnnouncementManager(@NonNull TrackRecordingService trackRecordingService) {
this.trackRecordingService = trackRecordingService;
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private Distance intervalDistance;
public VoiceAnnouncementManager(@NonNull Context context) {
this.context = context;
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
}
public void start(@Nullable TrackStatistics trackStatistics) {
voiceAnnouncement = new VoiceAnnouncement(trackRecordingService);
voiceAnnouncement = new TTSManager(context);
voiceAnnouncement.start();
update(trackStatistics);
}
@@ -74,10 +89,10 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
updateNextTaskDistance();
}
public void update(@NonNull Context context, @NonNull Track track) {
private boolean shouldNotAnnounce() {
if (voiceAnnouncement == null) {
Log.e(TAG, "Cannot update when in status shutdown.");
return;
return true;
}
if (!PreferencesUtils.shouldVoiceAnnouncementOnDeviceSpeaker()
@@ -85,6 +100,26 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
.getSelectedRoute()
.isDeviceSpeaker()) {
Log.i(TAG, "No voice announcement on device speaker.");
return true;
}
return false;
}
public void announceIdle() {
if (shouldNotAnnounce()) {
return;
}
if (!PreferencesUtils.shouldVoiceAnnouncementIdle()) {
return;
}
voiceAnnouncement.announce(VoiceAnnouncementUtils.createIdle(context));
}
public void announceStatisticsIfNeeded(@NonNull Track track) {
if (shouldNotAnnounce()) {
return;
}
@@ -100,10 +135,29 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
}
if (announce) {
voiceAnnouncement.announce(track);
voiceAnnouncement.announce(createAnnouncement(track));
}
}
private Spannable createAnnouncement(Track track) {
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
return VoiceAnnouncementUtils.createStatistics(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
}
public void stop() {
if (voiceAnnouncement != null) {
voiceAnnouncement.stop();
@@ -33,7 +33,12 @@ class VoiceAnnouncementUtils {
private VoiceAnnouncementUtils() {
}
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
static Spannable createIdle(Context context) {
return new SpannableStringBuilder()
.append(context.getString(R.string.voiceIdle));
}
static Spannable createStatistics(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
SpannableStringBuilder builder = new SpannableStringBuilder();
Distance totalDistance = trackStatistics.getTotalDistance();
Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed();
@@ -17,7 +17,7 @@ public class AnnouncementsSettingsFragment extends PreferenceFragmentCompat {
@Override
public void onStart() {
super.onStart();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_announcements_title);
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_announcements_statistics_title);
}
@Override
@@ -312,6 +312,10 @@ public class PreferencesUtils {
return getBoolean(R.string.voice_on_device_speaker_key, DEFAULT);
}
public static boolean shouldVoiceAnnouncementIdle() {
return getBoolean(R.string.voice_announce_idle_key, true);
}
public static Duration getVoiceAnnouncementFrequency() {
final int DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_frequency_default));
int value = getInt(R.string.voice_announcement_frequency_key, DEFAULT);