forked from upstream-mirrors/OpenTracks
298 lines
8.5 KiB
Java
298 lines
8.5 KiB
Java
/*
|
|
* Copyright 2009 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
* use this file except in compliance with the License. You may obtain a copy of
|
|
* the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations under
|
|
* the License.
|
|
*/
|
|
|
|
package com.google.android.apps.mytracks.services;
|
|
|
|
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
|
|
import com.google.android.apps.mytracks.Constants;
|
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
|
import com.google.android.apps.mytracks.util.StringUtils;
|
|
import com.google.android.apps.mytracks.util.UnitConversions;
|
|
import com.google.android.maps.mytracks.R;
|
|
|
|
import android.content.Context;
|
|
import android.content.SharedPreferences;
|
|
import android.speech.tts.TextToSpeech;
|
|
import android.speech.tts.TextToSpeech.OnInitListener;
|
|
import android.telephony.PhoneStateListener;
|
|
import android.telephony.TelephonyManager;
|
|
import android.util.Log;
|
|
|
|
import java.util.Locale;
|
|
|
|
/**
|
|
* This class will periodically announce the user's trip statistics.
|
|
*
|
|
* @author Sandor Dornbush
|
|
*/
|
|
public class StatusAnnouncerTask implements PeriodicTask {
|
|
|
|
/**
|
|
* The rate at which announcements are spoken.
|
|
*/
|
|
// @VisibleForTesting
|
|
static final float TTS_SPEECH_RATE = 0.9f;
|
|
|
|
/**
|
|
* A pointer to the service context.
|
|
*/
|
|
private final Context context;
|
|
|
|
/**
|
|
* String utilities.
|
|
*/
|
|
private final StringUtils stringUtils;
|
|
|
|
/**
|
|
* The interface to the text to speech engine.
|
|
*/
|
|
protected TextToSpeech tts;
|
|
|
|
/**
|
|
* The response received from the TTS engine after initialization.
|
|
*/
|
|
private boolean ready = false;
|
|
|
|
/**
|
|
* Whether we're allowed to speak right now.
|
|
*/
|
|
private boolean speechAllowed;
|
|
|
|
/**
|
|
* Listener which updates {@link #speechAllowed} when the phone state changes.
|
|
*/
|
|
private final PhoneStateListener phoneListener = new PhoneStateListener() {
|
|
@Override
|
|
public void onCallStateChanged(int state, String incomingNumber) {
|
|
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
|
|
|
|
if (!speechAllowed && tts.isSpeaking()) {
|
|
// If we're already speaking, stop it.
|
|
tts.stop();
|
|
}
|
|
}
|
|
};
|
|
|
|
public StatusAnnouncerTask(Context context) {
|
|
this(context, new StringUtils(context));
|
|
}
|
|
|
|
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
|
|
this.context = context;
|
|
this.stringUtils = stringUtils;
|
|
}
|
|
|
|
/**
|
|
* Called when the TTS engine is initialized.
|
|
*/
|
|
protected void onTtsInit(int status) {
|
|
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
|
|
this.ready = status == TextToSpeech.SUCCESS;
|
|
|
|
if (ready) {
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* Announces the trip status.
|
|
*/
|
|
@Override
|
|
public void run(TrackRecordingService service) {
|
|
if (service == null) {
|
|
Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized");
|
|
return;
|
|
}
|
|
|
|
runWithStatistics(service.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.
|
|
*/
|
|
protected void runWithStatistics(TripStatistics statistics) {
|
|
if (statistics == null) {
|
|
Log.e(TAG, "StatusAnnouncer stats not initialized.");
|
|
return;
|
|
}
|
|
if (!ready || tts == null) {
|
|
Log.e(TAG, "StatusAnnouncer Tts not ready.");
|
|
return;
|
|
}
|
|
|
|
if (!speechAllowed) {
|
|
Log.i(Constants.TAG,
|
|
"Not making announcement - not allowed at this time");
|
|
return;
|
|
}
|
|
|
|
String announcement = getAnnouncement(statistics);
|
|
Log.d(Constants.TAG, "Announcement: " + announcement);
|
|
speakAnnouncment(announcement);
|
|
}
|
|
|
|
protected void speakAnnouncment(String announcement) {
|
|
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
|
}
|
|
|
|
/**
|
|
* Builds the announcement string.
|
|
*
|
|
* @return The string that will be read to the user
|
|
*/
|
|
// @VisibleForTesting
|
|
protected String getAnnouncement(TripStatistics stats) {
|
|
SharedPreferences preferences =
|
|
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
|
|
boolean metricUnits = true;
|
|
boolean reportSpeed = true;
|
|
if (preferences != null) {
|
|
metricUnits =
|
|
preferences.getBoolean(context.getString(R.string.metric_units_key),
|
|
true);
|
|
reportSpeed =
|
|
preferences.getBoolean(context.getString(R.string.report_speed_key),
|
|
true);
|
|
}
|
|
|
|
double d = stats.getTotalDistance() / 1000;
|
|
double s = stats.getAverageMovingSpeed() * 3.6;
|
|
if (d == 0) {
|
|
return context.getString(R.string.announce_no_distance);
|
|
}
|
|
|
|
int speedLabel;
|
|
if (metricUnits) {
|
|
if (reportSpeed) {
|
|
speedLabel = R.string.kilometer_per_hour_long;
|
|
} else {
|
|
speedLabel = R.string.per_kilometer;
|
|
}
|
|
} else {
|
|
s *= UnitConversions.KMH_TO_MPH;
|
|
d *= UnitConversions.KM_TO_MI;
|
|
if (reportSpeed) {
|
|
speedLabel = R.string.mile_per_hour_long;
|
|
} else {
|
|
speedLabel = R.string.per_mile;
|
|
}
|
|
}
|
|
|
|
String speed = null;
|
|
if ((s == 0) || Double.isNaN(s)) {
|
|
speed = context.getString(R.string.unknown);
|
|
} else {
|
|
if (reportSpeed) {
|
|
speed = String.format("%.1f", s);
|
|
} else {
|
|
double pace = 3600000.0 / s;
|
|
Log.w(Constants.TAG,
|
|
"Converted speed: " + s + " to pace: " + pace);
|
|
speed = stringUtils.formatTimeLong((long) pace);
|
|
}
|
|
}
|
|
|
|
return context.getString(R.string.announce_template,
|
|
context.getString(R.string.total_distance_label),
|
|
d,
|
|
context.getString(metricUnits
|
|
? R.string.kilometers_long
|
|
: R.string.miles_long),
|
|
stringUtils.formatTimeLong(stats.getMovingTime()),
|
|
speed,
|
|
context.getString(speedLabel));
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
@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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the volume stream to use for controlling announcement volume.
|
|
*/
|
|
public static int getVolumeStream() {
|
|
return TextToSpeech.Engine.DEFAULT_STREAM;
|
|
}
|
|
}
|