Merge heads

This commit is contained in:
youtaol
2012-08-01 10:12:17 +08:00
9 changed files with 311 additions and 405 deletions
@@ -16,8 +16,6 @@
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
@@ -27,57 +25,57 @@ import android.util.Log;
import java.util.HashMap;
/**
* This class will periodically announce the user's trip statistics. This class
* will request and release audio focus. <br>
* For API Level 8 or higher.
*
* will request and release audio focus.
*
* @author Sandor Dornbush
*/
@TargetApi(8)
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask {
private final static HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
private static final String TAG = Api8StatusAnnouncerTask.class.getSimpleName();
private static final HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
static {
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
}
private final AudioManager audioManager;
private final OnUtteranceCompletedListener utteranceListener =
new OnUtteranceCompletedListener() {
@Override
private final OnUtteranceCompletedListener
utteranceListener = new OnUtteranceCompletedListener() {
@Override
public void onUtteranceCompleted(String utteranceId) {
int result = audioManager.abandonAudioFocus(null);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Failed to relinquish audio focus");
Log.w(TAG, "Failed to relinquish audio focus.");
}
}
};
private final AudioManager audioManager;
public Api8StatusAnnouncerTask(Context context) {
super(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
@Override
protected void onTtsReady() {
super.onTtsReady();
tts.setOnUtteranceCompletedListener(utteranceListener);
}
@Override
protected synchronized void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null,
TextToSpeech.Engine.DEFAULT_STREAM,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
int result = audioManager.requestAudioFocus(
null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Request for audio focus failed.");
Log.w(TAG, "Failed to request audio focus.");
}
// 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, SPEECH_PARAMS);
}
}
@@ -20,7 +20,7 @@ import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This is interface for a task that will be executed on some schedule.
*
*
* @author Sandor Dornbush
*/
public interface PeriodicTask {
@@ -28,11 +28,13 @@ public interface PeriodicTask {
* Sets up this task for subsequent calls to the run method.
*/
public void start();
/**
* This method will be called periodically.
*
* @param trackRecordingService the track recording service
*/
public void run(TrackRecordingService service);
public void run(TrackRecordingService trackRecordingService);
/**
* Shuts down this task and clean up resources.
@@ -15,8 +15,6 @@
*/
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
@@ -24,100 +22,139 @@ import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
/**
* Execute a task on a time or distance schedule.
*
* Execute a periodic task on a time or distance schedule.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecutor {
private static final String TAG = PeriodicTaskExecutor.class.getSimpleName();
private static final long MINUTE_TO_MILLISECONDS = 60000L;
private final TrackRecordingService trackRecordingService;
private final PeriodicTaskFactory periodicTaskFactory;
/**
* The frequency of the task.
* A value greater than zero is a frequency in time.
* A value less than zero is considered a frequency in distance.
* The task frequency. A positive value is a time frequency (minutes). A
* negative value is a distance frequency (km or mi). A zero value is to turn
* off periodic task.
*/
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
/**
* The next distance when the task should execute.
*/
private double nextTaskDistance = 0;
private PeriodicTask periodicTask;
/**
* Time based executor.
*/
private TimerTaskExecutor timerExecutor = null;
// Time periodic task executor
private TimerTaskExecutor timerTaskExecutor = null;
private boolean metricUnits;
private final TrackRecordingService service;
// The next distance for the distance periodic task
private double nextTaskDistance = Double.MAX_VALUE;
private final PeriodicTaskFactory factory;
private PeriodicTask task;
public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) {
this.service = service;
this.factory = factory;
public PeriodicTaskExecutor(
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
}
/**
* Restores the manager.
* Restores the executor.
*/
public void restore() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
if (!trackRecordingService.isRecording()) {
Log.d(TAG, "Not recording.");
return;
}
if (!isTimeFrequency()) {
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
if (timerTaskExecutor != null) {
timerTaskExecutor.shutdown();
timerTaskExecutor = null;
}
}
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off.");
return;
}
// Try to make the task.
task = factory.create(service);
// Returning null is ok.
if (task == null) {
periodicTask = periodicTaskFactory.create(trackRecordingService);
// Returning null is ok
if (periodicTask == null) {
Log.d(TAG, "Peridoic task is null.");
return;
}
task.start();
periodicTask.start();
if (isTimeFrequency()) {
if (timerExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service);
if (timerTaskExecutor == null) {
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
}
timerExecutor.scheduleTask(taskFrequency * 60000L);
timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
} else {
// For distance based splits.
// For distance periodic task
calculateNextTaskDistance();
}
}
/**
* Shuts down the manager.
* Shuts down the executor.
*/
public void shutdown() {
if (task != null) {
task.shutdown();
task = null;
if (periodicTask != null) {
periodicTask.shutdown();
periodicTask = null;
}
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
if (timerTaskExecutor != null) {
timerTaskExecutor.shutdown();
timerTaskExecutor = null;
}
}
/**
* Calculates the next distance when the task should execute.
* Updates the executor.
*/
void calculateNextTaskDistance() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording() || task == null) {
public void update() {
if (!isDistanceFrequency() || periodicTask == null) {
return;
}
double distance = trackRecordingService.getTripStatistics().getTotalDistance()
* UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) {
periodicTask.run(trackRecordingService);
calculateNextTaskDistance();
}
}
/**
* Sets task frequency.
*
* @param taskFrequency the task frequency
*/
public void setTaskFrequency(int taskFrequency) {
this.taskFrequency = taskFrequency;
restore();
}
/**
* Sets metricUnits.
*
* @param metricUnits true to use metric units
*/
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
}
/**
* Calculates the next distance for the distance periodic task.
*/
private void calculateNextTaskDistance() {
if (!trackRecordingService.isRecording() || periodicTask == null) {
return;
}
@@ -127,7 +164,8 @@ public class PeriodicTaskExecutor {
return;
}
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
double distance = trackRecordingService.getTripStatistics().getTotalDistance()
* UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
@@ -135,56 +173,19 @@ public class PeriodicTaskExecutor {
int index = (int) (distance / taskFrequency);
index -= 1;
nextTaskDistance = taskFrequency * index;
Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance);
}
/**
* Updates executer with new trip statistics.
* True if time frequency.
*/
public void update() {
if (!isDistanceFrequency() || task == null) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) {
task.run(service);
calculateNextTaskDistance();
}
}
private boolean isTimeFrequency() {
return taskFrequency > 0;
}
/**
* True if distance frequency.
*/
private boolean isDistanceFrequency() {
return taskFrequency < 0;
}
/**
* Sets the task frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off the task
* &gt; 0 Use the value as a time in minutes
* @param taskFrequency The frequency in time or distance
*/
public void setTaskFrequency(int taskFrequency) {
Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency);
this.taskFrequency = taskFrequency;
restore();
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
}
double getNextTaskDistance() {
return nextTaskDistance;
}
}
@@ -13,21 +13,22 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import android.content.Context;
/**
* An interface for classes that can create periodic tasks.
*
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
/**
* Creates a periodic task which does voice announcements.
*
* Creates a periodic task.
*
* @return the task, or null if task is not supported
*/
PeriodicTask create(Context context);
public PeriodicTask create(Context context);
}
@@ -21,15 +21,17 @@ import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.Context;
/**
* A simple task to insert statistics markers periodically.
*
* @author Sandor Dornbush
*/
public class SplitTask implements PeriodicTask {
private SplitTask() {
}
private SplitTask() {}
@Override
public void start() {}
@Override
public void run(TrackRecordingService service) {
@@ -37,15 +39,10 @@ public class SplitTask implements PeriodicTask {
}
@Override
public void shutdown() {
}
public void shutdown() {}
@Override
public void start() {
}
/**
* Create new SplitTasks.
* Create new {@link SplitTask}.
*/
public static class Factory implements PeriodicTaskFactory {
@@ -27,9 +27,6 @@ import android.content.Context;
*/
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
public StatusAnnouncerFactory() {
}
@Override
public PeriodicTask create(Context context) {
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
@@ -16,9 +16,6 @@
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -38,7 +35,7 @@ import java.util.Locale;
/**
* This class will periodically announce the user's trip statistics.
*
*
* @author Sandor Dornbush
*/
public class StatusAnnouncerTask implements PeriodicTask {
@@ -46,42 +43,31 @@ public class StatusAnnouncerTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
// @VisibleForTesting
@VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
*/
private final Context context;
private static final String TAG = StatusAnnouncerTask.class.getSimpleName();
private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000;
/**
* The interface to the text to speech engine.
*/
private final Context context;
protected TextToSpeech tts;
/**
* The response received from the TTS engine after initialization.
*/
// Response from TTS after its initialization
private int initStatus = TextToSpeech.ERROR;
/**
* Whether the TTS engine is ready.
*/
// True if TTS engine is ready
private boolean ready = false;
/**
* Whether we're allowed to speak right now.
*/
// True if speech is allowed
private boolean speechAllowed;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneListener = new PhoneStateListener() {
@Override
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts != null && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
@@ -93,107 +79,197 @@ public class StatusAnnouncerTask implements PeriodicTask {
this.context = context;
}
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (service == null) {
Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized");
public void start() {
if (tts == null) {
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
initStatus = status;
}
});
}
speechAllowed = true;
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void run(TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
runWithStatistics(service.getTripStatistics());
announce(trackRecordingService.getTripStatistics());
}
/**
* This method exists as a convenience for testing code, allowing said code
* to avoid needing to instantiate an entire {@link TrackRecordingService}
* just to test the announcer.
* Runs this task.
*
* @param tripStatistics the trip statistics
*/
// @VisibleForTesting
void runWithStatistics(TripStatistics statistics) {
if (statistics == null) {
Log.e(TAG, "StatusAnnouncer stats not initialized.");
@VisibleForTesting
void announce(TripStatistics tripStatistics) {
if (tripStatistics == null) {
Log.e(TAG, "TripStatistics is null.");
return;
}
synchronized (this) {
checkReady();
if (!ready) {
Log.e(TAG, "StatusAnnouncer Tts not ready.");
ready = initStatus == TextToSpeech.SUCCESS;
if (ready) {
onTtsReady();
}
}
if (!ready) {
Log.i(TAG, "TTS not ready.");
return;
}
}
if (!speechAllowed) {
Log.i(Constants.TAG,
"Not making announcement - not allowed at this time");
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
String announcement = getAnnouncement(statistics);
Log.d(Constants.TAG, "Announcement: " + announcement);
speakAnnouncement(announcement);
speakAnnouncement(getAnnouncement(tripStatistics));
}
@Override
public void shutdown() {
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
}
/**
* Called when TTS is ready.
*/
protected void onTtsReady() {
Locale locale = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(locale);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.w(TAG, "Default locale not available, use English.");
locale = Locale.ENGLISH;
/*
* TODO: instead of using english, load the language if missing and show a
* toast if not supported. Not able to change the resource strings to
* English.
*/
}
tts.setLanguage(locale);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
/**
* Speaks the announcement.
*
* @param announcement the announcement
*/
protected void speakAnnouncement(String announcement) {
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
}
/**
* Builds the announcement string.
*
* @return The string that will be read to the user
* Create a new {@link TextToSpeech}.
*
* @param aContext a context
* @param onInitListener an on init listener
*/
// @VisibleForTesting
protected String getAnnouncement(TripStatistics stats) {
@VisibleForTesting
protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) {
return new TextToSpeech(aContext, onInitListener);
}
/**
* Gets the announcement.
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
protected String getAnnouncement(TripStatistics tripStatistics) {
boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
boolean reportSpeed = PreferencesUtils.getBoolean(
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
double d = stats.getTotalDistance() * UnitConversions.M_TO_KM;
double s = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (d == 0) {
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (distance == 0) {
return context.getString(R.string.voice_total_distance_zero);
}
if (!metricUnits) {
d *= UnitConversions.KM_TO_MI;
s *= UnitConversions.KM_TO_MI;
distance *= UnitConversions.KM_TO_MI;
speed *= UnitConversions.KM_TO_MI;
}
if (!reportSpeed) {
s = 3600000.0 / s; // converts from speed to pace
}
// Makes sure s is not NaN.
if (Double.isNaN(s)) {
s = 0;
}
String speed;
String rate;
if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
: R.plurals.voiceSpeedMilesPerHour;
speed = context.getResources().getQuantityString(speedId, getQuantityCount(s), s);
rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed);
} else {
speed = speed == 0 ? 0.0 : 1 / speed;
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
speed = context.getString(paceId, getAnnounceTime((long) s));
rate = context.getString(paceId, getAnnounceTime((long) (speed * HOUR_TO_MILLISECOND)));
}
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
: R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(
totalDistanceId, getQuantityCount(d), d);
String totalDistance = context.getResources()
.getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
return context.getString(
R.string.voice_template, totalDistance, getAnnounceTime(stats.getMovingTime()), speed);
return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(
tripStatistics.getMovingTime()), rate);
}
/**
* Listens to phone state.
*
* @param listener the listener
* @param events the interested events
*/
@VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony = (TelephonyManager) context.getSystemService(
Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Gets the announce time.
*
* @param time the time
*/
@VisibleForTesting
String getAnnounceTime(long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources()
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
String minutes = context.getResources()
.getQuantityString(R.plurals.voiceMinutes, parts[1], parts[1]);
String hours = context.getResources()
.getQuantityString(R.plurals.voiceHours, parts[2], parts[2]);
StringBuilder sb = new StringBuilder();
if (parts[2] != 0) {
sb.append(hours);
sb.append(" ");
sb.append(minutes);
} else {
sb.append(minutes);
sb.append(" ");
sb.append(seconds);
}
return sb.toString();
}
/**
* Gets the plural count to be used by getQuantityString. getQuantityString
* only supports integer quantities, not a double quantity like "2.2".
@@ -204,7 +280,7 @@ public class StatusAnnouncerTask implements PeriodicTask {
* integer quantity. However, we need to make sure that if the casted value is
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
* 3.
*
*
* @param d the double value
*/
private int getQuantityCount(double d) {
@@ -219,138 +295,4 @@ public class StatusAnnouncerTask implements PeriodicTask {
return count < 3 ? 3 : count;
}
}
@Override
public void start() {
Log.i(Constants.TAG, "Starting TTS");
if (tts == null) {
// We can't have this class also be the listener, otherwise it's unsafe to
// reference it in Cupcake (even if we don't instantiate it).
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
onTtsInit(status);
}
});
}
speechAllowed = true;
// Register ourselves as a listener so we won't speak during a call.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
}
/**
* Called when the TTS engine is initialized.
*/
private void onTtsInit(int status) {
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
synchronized (this) {
// TTS should be valid here but NPE exceptions were reported to the market.
initStatus = status;
checkReady();
}
}
/**
* Ensures that the TTS is ready (finishing its initialization if needed).
*/
private void checkReady() {
synchronized (this) {
if (ready) {
// Already done;
return;
}
ready = initStatus == TextToSpeech.SUCCESS && tts != null;
Log.d(TAG, "Status announcer ready: " + ready);
if (ready) {
onTtsReady();
}
}
}
/**
* Finishes the TTS engine initialization.
* Called once (and only once) when the TTS engine is ready.
*/
protected void onTtsReady() {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported.
// TODO: Somehow use announcement strings from English too.
Log.w(TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
@Override
public void shutdown() {
// Stop listening to phone state.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
Log.i(Constants.TAG, "TTS shut down");
}
/**
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
* several issues during testing.
*/
// @VisibleForTesting
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return new TextToSpeech(ctx, onInitListener);
}
/**
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
*/
// @VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Gets a string to announce the time.
*
* @param time the time
*/
@VisibleForTesting
String getAnnounceTime(long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources().getQuantityString(
R.plurals.voiceSeconds, parts[0], parts[0]);
String minutes = context.getResources().getQuantityString(
R.plurals.voiceMinutes, parts[1], parts[1]);
String hours = context.getResources().getQuantityString(
R.plurals.voiceHours, parts[2], parts[2]);
StringBuilder sb = new StringBuilder();
if (parts[2] != 0) {
sb.append(hours);
sb.append(" ");
sb.append(minutes);
} else {
sb.append(minutes);
sb.append(" ");
sb.append(seconds);
}
return sb.toString();
}
}
@@ -16,46 +16,40 @@
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This class will periodically perform a task.
*
*
* @author Sandor Dornbush
*/
public class TimerTaskExecutor {
private final PeriodicTask task;
private final TrackRecordingService service;
private final PeriodicTask periodicTask;
private final TrackRecordingService trackRecordingService;
/**
* A timer to schedule the announcements.
* This is non-null if the task is in started (scheduled) state.
* A timer to schedule the announcements. This is non-null if the task is in
* started (scheduled) state.
*/
private Timer timer;
public TimerTaskExecutor(PeriodicTask task,
TrackRecordingService service) {
this.task = task;
this.service = service;
public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) {
this.periodicTask = periodicTask;
this.trackRecordingService = trackRecordingService;
}
/**
* Schedules the task at the given interval.
*
* @param interval The interval in milliseconds
* Schedules the periodic task at an interval.
*
* @param interval the interval in milliseconds
*/
public void scheduleTask(long interval) {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
if (!trackRecordingService.isRecording()) {
return;
}
@@ -64,7 +58,7 @@ public class TimerTaskExecutor {
timer.purge();
} else {
// First start, or we were previously shut down.
task.start();
periodicTask.start();
}
timer = new Timer();
@@ -73,27 +67,22 @@ public class TimerTaskExecutor {
}
long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime();
long next = trackRecordingService.getTripStatistics().getStartTime();
if (next < now) {
next = now + interval - ((now - next) % interval);
}
Date start = new Date(next);
Log.i(TAG, task.getClass().getSimpleName() + " scheduled to start at " + start
+ " every " + interval + " milliseconds.");
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
timer.scheduleAtFixedRate(new PeriodicTimerTask(), new Date(next), interval);
}
/**
* Cleans up this object.
* Shuts down.
*/
public void shutdown() {
Log.i(TAG, task.getClass().getSimpleName() + " shutting down.");
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
task.shutdown();
periodicTask.shutdown();
}
}
@@ -101,9 +90,10 @@ public class TimerTaskExecutor {
* The timer task to announce the trip status.
*/
private class PeriodicTimerTask extends TimerTask {
@Override
public void run() {
task.run(service);
periodicTask.run(trackRecordingService);
}
}
}
@@ -322,32 +322,6 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE))
.andStubReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setLanguage(DEFAULT_LOCALE))
.andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE))
.andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
AndroidMock.verify(mockTask, tts);
}
public void testStart_languageNotSupported() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE))
.andStubReturn(TextToSpeech.LANG_NOT_SUPPORTED);
expect(tts.setLanguage(Locale.ENGLISH))
.andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE))
.andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
@@ -393,6 +367,10 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Put task in "ready" state
startTask(TextToSpeech.SUCCESS);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE)).andStubReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setLanguage(DEFAULT_LOCALE)).andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE)).andReturn(TextToSpeech.SUCCESS);
// Expect actual announcement call
expect(tts.speak(
eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
@@ -401,7 +379,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement
AndroidMock.replay(tts);
task.runWithStatistics(stats);
task.announce(stats);
AndroidMock.verify(mockTask, tts);
}
@@ -411,7 +389,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement
AndroidMock.replay(tts);
task.runWithStatistics(null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
@@ -424,7 +402,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.runWithStatistics(null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
@@ -441,7 +419,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement - this should do nothing.
task.runWithStatistics(null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
@@ -455,7 +433,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.runWithStatistics(null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
@@ -475,7 +453,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement
AndroidMock.replay(tts);
task.runWithStatistics(null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}