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; package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
@@ -27,57 +25,57 @@ import android.util.Log;
import java.util.HashMap; import java.util.HashMap;
/** /**
* This class will periodically announce the user's trip statistics. This class * This class will periodically announce the user's trip statistics. This class
* will request and release audio focus. <br> * will request and release audio focus.
* For API Level 8 or higher.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@TargetApi(8) @TargetApi(8)
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask { 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 { static {
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used"); SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
} }
private final AudioManager audioManager; private final OnUtteranceCompletedListener
private final OnUtteranceCompletedListener utteranceListener = utteranceListener = new OnUtteranceCompletedListener() {
new OnUtteranceCompletedListener() { @Override
@Override
public void onUtteranceCompleted(String utteranceId) { public void onUtteranceCompleted(String utteranceId) {
int result = audioManager.abandonAudioFocus(null); int result = audioManager.abandonAudioFocus(null);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { 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) { public Api8StatusAnnouncerTask(Context context) {
super(context); super(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
} }
@Override @Override
protected void onTtsReady() { protected void onTtsReady() {
super.onTtsReady(); super.onTtsReady();
tts.setOnUtteranceCompletedListener(utteranceListener); tts.setOnUtteranceCompletedListener(utteranceListener);
} }
@Override @Override
protected synchronized void speakAnnouncement(String announcement) { protected synchronized void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, int result = audioManager.requestAudioFocus(
TextToSpeech.Engine.DEFAULT_STREAM, null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { 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); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
} }
} }
@@ -31,8 +31,10 @@ public interface PeriodicTask {
/** /**
* This method will be called periodically. * 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. * Shuts down this task and clean up resources.
@@ -15,8 +15,6 @@
*/ */
package com.google.android.apps.mytracks.services.tasks; 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.services.TrackRecordingService;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
@@ -24,100 +22,139 @@ import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log; 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 * @author Sandor Dornbush
*/ */
public class PeriodicTaskExecutor { 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. * The task frequency. A positive value is a time frequency (minutes). A
* A value greater than zero is a frequency in time. * negative value is a distance frequency (km or mi). A zero value is to turn
* A value less than zero is considered a frequency in distance. * off periodic task.
*/ */
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF; private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
/** private PeriodicTask periodicTask;
* The next distance when the task should execute.
*/
private double nextTaskDistance = 0;
/** // Time periodic task executor
* Time based executor. private TimerTaskExecutor timerTaskExecutor = null;
*/
private TimerTaskExecutor timerExecutor = null;
private boolean metricUnits; 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; public PeriodicTaskExecutor(
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
private PeriodicTask task; this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) {
this.service = service;
this.factory = factory;
} }
/** /**
* Restores the manager. * Restores the executor.
*/ */
public void restore() { public void restore() {
// TODO: Decouple service from this class once and forever. if (!trackRecordingService.isRecording()) {
if (!service.isRecording()) { Log.d(TAG, "Not recording.");
return; return;
} }
if (!isTimeFrequency()) { if (!isTimeFrequency()) {
if (timerExecutor != null) { if (timerTaskExecutor != null) {
timerExecutor.shutdown(); timerTaskExecutor.shutdown();
timerExecutor = null; timerTaskExecutor = null;
} }
} }
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) { if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off.");
return; return;
} }
// Try to make the task. periodicTask = periodicTaskFactory.create(trackRecordingService);
task = factory.create(service);
// Returning null is ok. // Returning null is ok
if (task == null) { if (periodicTask == null) {
Log.d(TAG, "Peridoic task is null.");
return; return;
} }
task.start(); periodicTask.start();
if (isTimeFrequency()) { if (isTimeFrequency()) {
if (timerExecutor == null) { if (timerTaskExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service); timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
} }
timerExecutor.scheduleTask(taskFrequency * 60000L); timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
} else { } else {
// For distance based splits. // For distance periodic task
calculateNextTaskDistance(); calculateNextTaskDistance();
} }
} }
/** /**
* Shuts down the manager. * Shuts down the executor.
*/ */
public void shutdown() { public void shutdown() {
if (task != null) { if (periodicTask != null) {
task.shutdown(); periodicTask.shutdown();
task = null; periodicTask = null;
} }
if (timerExecutor != null) { if (timerTaskExecutor != null) {
timerExecutor.shutdown(); timerTaskExecutor.shutdown();
timerExecutor = null; timerTaskExecutor = null;
} }
} }
/** /**
* Calculates the next distance when the task should execute. * Updates the executor.
*/ */
void calculateNextTaskDistance() { public void update() {
// TODO: Decouple service from this class once and forever. if (!isDistanceFrequency() || periodicTask == null) {
if (!service.isRecording() || task == 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; return;
} }
@@ -127,7 +164,8 @@ public class PeriodicTaskExecutor {
return; return;
} }
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM; double distance = trackRecordingService.getTripStatistics().getTotalDistance()
* UnitConversions.M_TO_KM;
if (!metricUnits) { if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI; distance *= UnitConversions.KM_TO_MI;
} }
@@ -135,56 +173,19 @@ public class PeriodicTaskExecutor {
int index = (int) (distance / taskFrequency); int index = (int) (distance / taskFrequency);
index -= 1; index -= 1;
nextTaskDistance = taskFrequency * index; 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() { private boolean isTimeFrequency() {
return taskFrequency > 0; return taskFrequency > 0;
} }
/**
* True if distance frequency.
*/
private boolean isDistanceFrequency() { private boolean isDistanceFrequency() {
return taskFrequency < 0; 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,6 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import android.content.Context; import android.content.Context;
@@ -25,9 +26,9 @@ import android.content.Context;
public interface PeriodicTaskFactory { 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 * @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; import android.content.Context;
/** /**
* A simple task to insert statistics markers periodically. * A simple task to insert statistics markers periodically.
*
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class SplitTask implements PeriodicTask { public class SplitTask implements PeriodicTask {
private SplitTask() { private SplitTask() {}
}
@Override
public void start() {}
@Override @Override
public void run(TrackRecordingService service) { public void run(TrackRecordingService service) {
@@ -37,15 +39,10 @@ public class SplitTask implements PeriodicTask {
} }
@Override @Override
public void shutdown() { public void shutdown() {}
}
@Override
public void start() {
}
/** /**
* Create new SplitTasks. * Create new {@link SplitTask}.
*/ */
public static class Factory implements PeriodicTaskFactory { public static class Factory implements PeriodicTaskFactory {
@@ -27,9 +27,6 @@ import android.content.Context;
*/ */
public class StatusAnnouncerFactory implements PeriodicTaskFactory { public class StatusAnnouncerFactory implements PeriodicTaskFactory {
public StatusAnnouncerFactory() {
}
@Override @Override
public PeriodicTask create(Context context) { public PeriodicTask create(Context context) {
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context); return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
@@ -16,9 +16,6 @@
package com.google.android.apps.mytracks.services.tasks; 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.services.TrackRecordingService;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -46,42 +43,31 @@ public class StatusAnnouncerTask implements PeriodicTask {
/** /**
* The rate at which announcements are spoken. * The rate at which announcements are spoken.
*/ */
// @VisibleForTesting @VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f; static final float TTS_SPEECH_RATE = 0.9f;
/** private static final String TAG = StatusAnnouncerTask.class.getSimpleName();
* A pointer to the service context. private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000;
*/
private final Context context;
/** private final Context context;
* The interface to the text to speech engine.
*/
protected TextToSpeech tts; protected TextToSpeech tts;
/** // Response from TTS after its initialization
* The response received from the TTS engine after initialization.
*/
private int initStatus = TextToSpeech.ERROR; private int initStatus = TextToSpeech.ERROR;
/** // True if TTS engine is ready
* Whether the TTS engine is ready.
*/
private boolean ready = false; private boolean ready = false;
/** // True if speech is allowed
* Whether we're allowed to speak right now.
*/
private boolean speechAllowed; private boolean speechAllowed;
/** /**
* Listener which updates {@link #speechAllowed} when the phone state changes. * Listener which updates {@link #speechAllowed} when the phone state changes.
*/ */
private final PhoneStateListener phoneListener = new PhoneStateListener() { private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override @Override
public void onCallStateChanged(int state, String incomingNumber) { public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE; speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts != null && tts.isSpeaking()) { if (!speechAllowed && tts != null && tts.isSpeaking()) {
// If we're already speaking, stop it. // If we're already speaking, stop it.
tts.stop(); tts.stop();
@@ -93,105 +79,195 @@ public class StatusAnnouncerTask implements PeriodicTask {
this.context = context; this.context = context;
} }
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override @Override
public void run(TrackRecordingService service) { public void start() {
if (service == null) { if (tts == null) {
Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized"); 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; return;
} }
announce(trackRecordingService.getTripStatistics());
runWithStatistics(service.getTripStatistics());
} }
/** /**
* This method exists as a convenience for testing code, allowing said code * Runs this task.
* to avoid needing to instantiate an entire {@link TrackRecordingService} *
* just to test the announcer. * @param tripStatistics the trip statistics
*/ */
// @VisibleForTesting @VisibleForTesting
void runWithStatistics(TripStatistics statistics) { void announce(TripStatistics tripStatistics) {
if (statistics == null) { if (tripStatistics == null) {
Log.e(TAG, "StatusAnnouncer stats not initialized."); Log.e(TAG, "TripStatistics is null.");
return; return;
} }
synchronized (this) { synchronized (this) {
checkReady();
if (!ready) { 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; return;
} }
} }
if (!speechAllowed) { if (!speechAllowed) {
Log.i(Constants.TAG, Log.i(TAG, "Speech is not allowed at this time.");
"Not making announcement - not allowed at this time");
return; return;
} }
speakAnnouncement(getAnnouncement(tripStatistics));
String announcement = getAnnouncement(statistics);
Log.d(Constants.TAG, "Announcement: " + announcement);
speakAnnouncement(announcement);
} }
@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) { protected void speakAnnouncement(String announcement) {
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
} }
/** /**
* Builds the announcement string. * Create a new {@link TextToSpeech}.
* *
* @return The string that will be read to the user * @param aContext a context
* @param onInitListener an on init listener
*/ */
// @VisibleForTesting @VisibleForTesting
protected String getAnnouncement(TripStatistics stats) { 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( boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
boolean reportSpeed = PreferencesUtils.getBoolean( boolean reportSpeed = PreferencesUtils.getBoolean(
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
double d = stats.getTotalDistance() * UnitConversions.M_TO_KM; double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double s = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH; double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (d == 0) { if (distance == 0) {
return context.getString(R.string.voice_total_distance_zero); return context.getString(R.string.voice_total_distance_zero);
} }
if (!metricUnits) { if (!metricUnits) {
d *= UnitConversions.KM_TO_MI; distance *= UnitConversions.KM_TO_MI;
s *= UnitConversions.KM_TO_MI; speed *= UnitConversions.KM_TO_MI;
} }
if (!reportSpeed) { String rate;
s = 3600000.0 / s; // converts from speed to pace
}
// Makes sure s is not NaN.
if (Double.isNaN(s)) {
s = 0;
}
String speed;
if (reportSpeed) { if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
: R.plurals.voiceSpeedMilesPerHour; : R.plurals.voiceSpeedMilesPerHour;
speed = context.getResources().getQuantityString(speedId, getQuantityCount(s), s); rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed);
} else { } else {
speed = speed == 0 ? 0.0 : 1 / speed;
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile; 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 int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
: R.plurals.voiceTotalDistanceMiles; : R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString( String totalDistance = context.getResources()
totalDistanceId, getQuantityCount(d), d); .getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
return context.getString( return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(
R.string.voice_template, totalDistance, getAnnounceTime(stats.getMovingTime()), speed); 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();
} }
/** /**
@@ -219,138 +295,4 @@ public class StatusAnnouncerTask implements PeriodicTask {
return count < 3 ? 3 : count; 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,16 +16,12 @@
package com.google.android.apps.mytracks.services.tasks; 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 android.util.Log;
import java.util.Date; import java.util.Date;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/** /**
* This class will periodically perform a task. * This class will periodically perform a task.
* *
@@ -33,29 +29,27 @@ import com.google.android.apps.mytracks.services.TrackRecordingService;
*/ */
public class TimerTaskExecutor { public class TimerTaskExecutor {
private final PeriodicTask task; private final PeriodicTask periodicTask;
private final TrackRecordingService service; private final TrackRecordingService trackRecordingService;
/** /**
* A timer to schedule the announcements. * A timer to schedule the announcements. This is non-null if the task is in
* This is non-null if the task is in started (scheduled) state. * started (scheduled) state.
*/ */
private Timer timer; private Timer timer;
public TimerTaskExecutor(PeriodicTask task, public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) {
TrackRecordingService service) { this.periodicTask = periodicTask;
this.task = task; this.trackRecordingService = trackRecordingService;
this.service = service;
} }
/** /**
* Schedules the task at the given interval. * Schedules the periodic task at an interval.
* *
* @param interval The interval in milliseconds * @param interval the interval in milliseconds
*/ */
public void scheduleTask(long interval) { public void scheduleTask(long interval) {
// TODO: Decouple service from this class once and forever. if (!trackRecordingService.isRecording()) {
if (!service.isRecording()) {
return; return;
} }
@@ -64,7 +58,7 @@ public class TimerTaskExecutor {
timer.purge(); timer.purge();
} else { } else {
// First start, or we were previously shut down. // First start, or we were previously shut down.
task.start(); periodicTask.start();
} }
timer = new Timer(); timer = new Timer();
@@ -73,27 +67,22 @@ public class TimerTaskExecutor {
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime(); long next = trackRecordingService.getTripStatistics().getStartTime();
if (next < now) { if (next < now) {
next = now + interval - ((now - next) % interval); next = now + interval - ((now - next) % interval);
} }
timer.scheduleAtFixedRate(new PeriodicTimerTask(), new Date(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);
} }
/** /**
* Cleans up this object. * Shuts down.
*/ */
public void shutdown() { public void shutdown() {
Log.i(TAG, task.getClass().getSimpleName() + " shutting down.");
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
timer.purge(); timer.purge();
timer = null; timer = null;
task.shutdown(); periodicTask.shutdown();
} }
} }
@@ -101,9 +90,10 @@ public class TimerTaskExecutor {
* The timer task to announce the trip status. * The timer task to announce the trip status.
*/ */
private class PeriodicTimerTask extends TimerTask { private class PeriodicTimerTask extends TimerTask {
@Override @Override
public void run() { public void run() {
task.run(service); periodicTask.run(trackRecordingService);
} }
} }
} }
@@ -322,32 +322,6 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
OnInitListener ttsInitListener = initListenerCapture.getValue(); OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener); 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); AndroidMock.replay(tts);
ttsInitListener.onInit(TextToSpeech.SUCCESS); ttsInitListener.onInit(TextToSpeech.SUCCESS);
@@ -393,6 +367,10 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Put task in "ready" state // Put task in "ready" state
startTask(TextToSpeech.SUCCESS); 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 actual announcement call
expect(tts.speak( expect(tts.speak(
eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH), eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
@@ -401,7 +379,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.runWithStatistics(stats); task.announce(stats);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -411,7 +389,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -424,7 +402,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
AndroidMock.replay(tts); AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -441,7 +419,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement - this should do nothing. // Run the announcement - this should do nothing.
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -455,7 +433,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
AndroidMock.replay(tts); AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -475,7 +453,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }