diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java index fbc640998..9865e2c13 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java @@ -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.
- * 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 SPEECH_PARAMS = new HashMap(); + + private static final String TAG = Api8StatusAnnouncerTask.class.getSimpleName(); + private static final HashMap SPEECH_PARAMS = new HashMap(); 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); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java index 1d351bec4..640e7bc1f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java @@ -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. diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java index 228ca27c8..a02b201e6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java @@ -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. - * < 0 Use the absolute value as a distance in the current measurement km - * or mi - * 0 Turn off the task - * > 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; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java index f3f8be445..6f974cb28 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java @@ -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); } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java index 763a922be..1f1ed95b4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java @@ -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 { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java index 7e8b56b37..650423cf1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java index 36667ae80..ed6472bd4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java @@ -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(); - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java index ec7d66c05..86a61f9e4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java @@ -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); } } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java index 48d52bc5b..f678c82cf 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java @@ -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); }