Voice announcements are now triggered by the TrackRecordingService's timer that is also used for UI updates.

This commit is contained in:
Dennis Guse
2021-08-26 16:25:44 +02:00
parent b5165de7f0
commit 4f31607e3e
5 changed files with 72 additions and 192 deletions
@@ -25,7 +25,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class PeriodicTaskExecutorTest { public class VoiceAnnouncementManagerTest {
@Rule @Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@@ -53,17 +53,17 @@ public class PeriodicTaskExecutorTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService(); .getService();
PeriodicTaskExecutor periodicTaskExecutor = new PeriodicTaskExecutor(service, new AnnouncementPeriodicTask.Factory()); VoiceAnnouncementManager voiceAnnouncementManager = new VoiceAnnouncementManager(service);
periodicTaskExecutor.setMetricUnits(true); voiceAnnouncementManager.setMetricUnits(true);
periodicTaskExecutor.setTaskFrequency(-5); voiceAnnouncementManager.setTaskFrequency(-5);
// when // when
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
statistics.setTotalDistance(Distance.of(13000)); statistics.setTotalDistance(Distance.of(13000));
assertEquals(Distance.of(15000), periodicTaskExecutor.calculateNextTaskDistance(statistics)); assertEquals(Distance.of(15000), voiceAnnouncementManager.calculateNextTaskDistance(statistics));
statistics.setTotalDistance(Distance.of(15100)); statistics.setTotalDistance(Distance.of(15100));
assertEquals(Distance.of(20000), periodicTaskExecutor.calculateNextTaskDistance(statistics)); assertEquals(Distance.of(20000), voiceAnnouncementManager.calculateNextTaskDistance(statistics));
} }
} }
@@ -49,9 +49,7 @@ import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver;
import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager; import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue; import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTask; import de.dennisguse.opentracks.services.tasks.VoiceAnnouncementManager;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskFactory;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ExportUtils; import de.dennisguse.opentracks.util.ExportUtils;
@@ -76,7 +74,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE; public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
// The following variables are set in onCreate: // The following variables are set in onCreate:
private PeriodicTaskExecutor voiceExecutor; private VoiceAnnouncementManager voiceAnnouncementManager;
private TrackRecordingServiceNotificationManager notificationManager; private TrackRecordingServiceNotificationManager notificationManager;
private TrackRecordingManager trackRecordingManager; private TrackRecordingManager trackRecordingManager;
@@ -106,11 +104,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
Context context = TrackRecordingService.this; Context context = TrackRecordingService.this;
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context); boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
voiceExecutor.setMetricUnits(metricUnits); voiceAnnouncementManager.setMetricUnits(metricUnits);
notificationManager.setMetricUnits(metricUnits); notificationManager.setMetricUnits(metricUnits);
} }
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) { if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(sharedPreferences, context)); voiceAnnouncementManager.setTaskFrequency(PreferencesUtils.getVoiceFrequency(sharedPreferences, context));
} }
@@ -145,7 +143,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
trackRecordingManager = new TrackRecordingManager(this); trackRecordingManager = new TrackRecordingManager(this);
handlerServer = new TrackPointCreator(this); handlerServer = new TrackPointCreator(this);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTask.Factory()); voiceAnnouncementManager = new VoiceAnnouncementManager(this);
notificationManager = new TrackRecordingServiceNotificationManager(this); notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -178,9 +176,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
sharedPreferences = null; sharedPreferences = null;
try { try {
voiceExecutor.shutdown(); voiceAnnouncementManager.shutdown();
} finally { } finally {
voiceExecutor = null; voiceAnnouncementManager = null;
} }
// This should be the next to last operation // This should be the next to last operation
@@ -209,10 +207,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return trackRecordingManager.insertMarker(name, category, description, photoUrl); return trackRecordingManager.insertMarker(name, category, description, photoUrl);
} }
public void run(@NonNull PeriodicTaskFactory.Task periodicTask) {
periodicTask.run(recordingStatus.getTrackId(), trackRecordingManager.getTrackStatistics());
}
/** /**
* Starts a new track. * Starts a new track.
* *
@@ -267,8 +261,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
startGps(); startGps();
// Restore periodic tasks voiceAnnouncementManager.restore(trackRecordingManager.getTrackStatistics());
voiceExecutor.restore();
} }
public void tryStartGps() { public void tryStartGps() {
@@ -337,7 +330,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
// Shutdown periodic tasks // Shutdown periodic tasks
voiceExecutor.shutdown(); voiceAnnouncementManager.shutdown();
// Update instance variables // Update instance variables
handlerServer.stop(); handlerServer.stop();
@@ -374,7 +367,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
trackRecordingManager.onNewTrackPoint(trackPoint, thresholdHorizontalAccuracy); trackRecordingManager.onNewTrackPoint(trackPoint, thresholdHorizontalAccuracy);
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy); notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
voiceExecutor.update(recordingStatus.getTrackId(), trackRecordingManager.getTrackStatistics());
} }
@Override @Override
@@ -416,10 +408,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
} }
public Duration getTotalTime() {
return trackRecordingManager.getTrackStatistics().getTotalTime();
}
@Deprecated @Deprecated
@VisibleForTesting @VisibleForTesting
public TrackPointCreator getHandlerServer() { public TrackPointCreator getHandlerServer() {
@@ -430,7 +418,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return gpsStatusObservable; return gpsStatusObservable;
} }
public MutableLiveData<RecordingData> getRecordingDataObservable() { public LiveData<RecordingData> getRecordingDataObservable() {
return recordingDataObservable; return recordingDataObservable;
} }
@@ -457,6 +445,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
TrackPoint trackPoint = data.second.first; TrackPoint trackPoint = data.second.first;
egm2008CorrectionManager.correctAltitude(this, trackPoint); egm2008CorrectionManager.correctAltitude(this, trackPoint);
voiceAnnouncementManager.update(data.first);
recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second)); recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second));
} }
@@ -1,59 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* An interface for classes that can create {@link Task}.
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
@NonNull
Task create(Context context);
/**
* This is interface for a task that will be executed on some schedule.
*
* @author Sandor Dornbush
*/
interface Task {
/**
* Sets up this task for subsequent calls to the run method.
*/
void start();
/**
* This method will be called periodically.
*/
@Deprecated
void run(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics);
/**
* Shuts down this task and clean up resources.
*/
void shutdown();
}
}
@@ -38,13 +38,13 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
/** /**
* This class will periodically announce the user's {@link TrackStatistics}. * This class will announce the user's {@link TrackStatistics}.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class AnnouncementPeriodicTask implements PeriodicTaskFactory.Task { public class VoiceAnnouncement {
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName(); private static final String TAG = VoiceAnnouncement.class.getSimpleName();
private final Context context; private final Context context;
@@ -107,14 +107,13 @@ public class AnnouncementPeriodicTask implements PeriodicTaskFactory.Task {
private boolean ttsReady = false; private boolean ttsReady = false;
AnnouncementPeriodicTask(Context context) { VoiceAnnouncement(Context context) {
this.context = context; this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context); contentProviderUtils = new ContentProviderUtils(context);
} }
@Override
public void start() { public void start() {
Log.d(TAG, "Start"); Log.d(TAG, "Start");
@@ -128,8 +127,7 @@ public class AnnouncementPeriodicTask implements PeriodicTaskFactory.Task {
} }
} }
@Override public void announce(@NonNull Track track) {
public void run(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics) {
synchronized (this) { synchronized (this) {
if (!ttsReady) { if (!ttsReady) {
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS; ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
@@ -149,27 +147,20 @@ public class AnnouncementPeriodicTask implements PeriodicTaskFactory.Task {
return; return;
} }
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.i(TAG, "It doesn't exists a track with trackid = " + track);
return;
}
String category = track.getCategory();
boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context); boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category); boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, track.getCategory());
Distance minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); Distance minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
//TODO Do not load all trackpoints for every announcement
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null); TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits), minGPSDistance); IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits), minGPSDistance);
intervalStatistics.addTrackPoints(trackPointIterator); intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval); String announcement = AnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval);
speakAnnouncement(announcement); speakAnnouncement(announcement);
} }
@Override
public void shutdown() { public void shutdown() {
if (tts != null) { if (tts != null) {
tts.shutdown(); tts.shutdown();
@@ -199,18 +190,4 @@ public class AnnouncementPeriodicTask implements PeriodicTaskFactory.Task {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called. // 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"); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
} }
/**
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
*
* @author Rodrigo Damazio
*/
public static class Factory implements PeriodicTaskFactory {
@Override
@NonNull
public Task create(Context context) {
return new AnnouncementPeriodicTask(context);
}
}
} }
@@ -15,7 +15,6 @@
*/ */
package de.dennisguse.opentracks.services.tasks; package de.dennisguse.opentracks.services.tasks;
import android.os.Handler;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -34,12 +33,11 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class PeriodicTaskExecutor { public class VoiceAnnouncementManager {
private static final String TAG = PeriodicTaskExecutor.class.getSimpleName(); private static final String TAG = VoiceAnnouncementManager.class.getSimpleName();
private final TrackRecordingService trackRecordingService; private final TrackRecordingService trackRecordingService;
private final PeriodicTaskFactory periodicTaskFactory;
private final int TASK_FREQUENCY_OFF; private final int TASK_FREQUENCY_OFF;
/** /**
@@ -50,102 +48,76 @@ public class PeriodicTaskExecutor {
*/ */
private int taskFrequency; private int taskFrequency;
private PeriodicTaskFactory.Task periodicTask; private VoiceAnnouncement voiceAnnouncement;
private Handler handler;
private boolean metricUnits; private boolean metricUnits;
private TrackStatistics trackStatistics; private TrackStatistics trackStatistics;
private Distance nextTaskDistance = Distance.of(Double.MAX_VALUE); private Distance nextTotalDistance = Distance.of(Double.MAX_VALUE);
private Duration nextTotalTime = Duration.ofSeconds(Long.MAX_VALUE);
private final Runnable timer = new Runnable() { public VoiceAnnouncementManager(@NonNull TrackRecordingService trackRecordingService) {
@Override
public void run() {
if (trackRecordingService != null && periodicTask != null) {
trackRecordingService.run(periodicTask);
handler.postDelayed(this, getNextDuration());
}
}
};
public PeriodicTaskExecutor(@NonNull TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService; this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getString(R.string.frequency_off)); TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getString(R.string.frequency_off));
taskFrequency = TASK_FREQUENCY_OFF; taskFrequency = TASK_FREQUENCY_OFF;
} }
public void restore() { public void restore(TrackStatistics trackStatistics) {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
Log.d(TAG, "Not recording or paused.");
return;
}
if (!isTimeFrequency() && handler != null) {
handler.removeCallbacks(timer);
handler = null;
}
if (taskFrequency == TASK_FREQUENCY_OFF) { if (taskFrequency == TASK_FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off."); Log.d(TAG, "Task frequency is off.");
return; return;
} }
periodicTask = periodicTaskFactory.create(trackRecordingService); voiceAnnouncement = new VoiceAnnouncement(trackRecordingService);
periodicTask.start(); voiceAnnouncement.start();
if (isTimeFrequency()) { if (isTimeFrequency()) {
if (handler == null) { nextTotalTime = calculateNextDuration(trackStatistics);
handler = new Handler();
}
handler.postDelayed(timer, getNextDuration());
} else { } else {
// For distance periodic task // For distance periodic task
updateNextTaskDistance(); updateNextTaskDistance(trackStatistics);
}
}
public void update(@NonNull Track track) {
boolean announce = false;
this.trackStatistics = track.getTrackStatistics();
if (trackStatistics.getTotalDistance().greaterThan(nextTotalDistance)) {
updateNextTaskDistance(trackStatistics);
announce = true;
}
if (!trackStatistics.getTotalTime().minus(nextTotalTime).isNegative()) {
nextTotalTime = calculateNextDuration(trackStatistics);
announce = true;
}
if (announce) {
this.trackStatistics = track.getTrackStatistics();
voiceAnnouncement.announce(track);
} }
} }
public void shutdown() { public void shutdown() {
if (periodicTask != null) { if (voiceAnnouncement != null) {
periodicTask.shutdown(); voiceAnnouncement.shutdown();
periodicTask = null; voiceAnnouncement = null;
}
if (handler != null) {
handler.removeCallbacks(timer);
handler = null;
}
}
public void update(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics) {
if (!isDistanceFrequency() || periodicTask == null) {
return;
}
if (trackStatistics.getTotalDistance().greaterThan(nextTaskDistance)) {
periodicTask.run(trackId, trackStatistics);
this.trackStatistics = trackStatistics;
updateNextTaskDistance();
} }
} }
public void setTaskFrequency(int taskFrequency) { public void setTaskFrequency(int taskFrequency) {
this.taskFrequency = taskFrequency; this.taskFrequency = taskFrequency;
restore(); restore(this.trackStatistics);
} }
public void setMetricUnits(boolean metricUnits) { public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits; this.metricUnits = metricUnits;
updateNextTaskDistance(); updateNextTaskDistance(this.trackStatistics);
} }
private void updateNextTaskDistance() { private void updateNextTaskDistance(TrackStatistics trackStatistics) {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) {
return;
}
if (!isDistanceFrequency()) { if (!isDistanceFrequency()) {
nextTaskDistance = Distance.of(Double.MAX_VALUE); nextTotalDistance = Distance.of(Double.MAX_VALUE);
Log.d(TAG, "SplitManager: Distance splits disabled."); Log.d(TAG, "SplitManager: Distance splits disabled.");
return; return;
} }
@@ -154,11 +126,11 @@ public class PeriodicTaskExecutor {
return; return;
} }
nextTaskDistance = calculateNextTaskDistance(trackStatistics); nextTotalDistance = calculateNextTaskDistance(trackStatistics);
} }
@VisibleForTesting @VisibleForTesting
public Distance calculateNextTaskDistance(TrackStatistics trackStatistics) { public Distance calculateNextTaskDistance(@NonNull TrackStatistics trackStatistics) {
Distance distance = trackStatistics.getTotalDistance(); Distance distance = trackStatistics.getTotalDistance();
Distance announcementInterval = Distance.one(metricUnits).multipliedBy(Math.abs(taskFrequency)); Distance announcementInterval = Distance.one(metricUnits).multipliedBy(Math.abs(taskFrequency));
@@ -166,6 +138,14 @@ public class PeriodicTaskExecutor {
return announcementInterval.multipliedBy(index + 1); return announcementInterval.multipliedBy(index + 1);
} }
private Duration calculateNextDuration(@NonNull TrackStatistics trackStatistics) {
if (!isTimeFrequency()) {
throw new RuntimeException("Using distance frequency as time frequency is impossible.");
}
Duration interval = Duration.ofMinutes(taskFrequency);
return interval.minus(Duration.ofMillis(trackStatistics.getTotalTime().toMillis() % interval.toMillis()));
}
private boolean isTimeFrequency() { private boolean isTimeFrequency() {
return taskFrequency > 0; return taskFrequency > 0;
} }
@@ -173,12 +153,4 @@ public class PeriodicTaskExecutor {
private boolean isDistanceFrequency() { private boolean isDistanceFrequency() {
return taskFrequency < 0; return taskFrequency < 0;
} }
private long getNextDuration() {
if (!isTimeFrequency()) {
throw new RuntimeException("Using distance frequency as time frequency is impossible.");
}
Duration interval = Duration.ofMinutes(taskFrequency);
return interval.toMillis() - (trackRecordingService.getTotalTime().toMillis() % interval.toMillis());
}
} }