package cgeo.geocaching.speech;
import cgeo.geocaching.Intents;
import cgeo.geocaching.R;
import cgeo.geocaching.activity.ActivityMixin;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.sensors.GeoData;
import cgeo.geocaching.sensors.GeoDirHandler;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.Log;
import android.app.Activity;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.Engine;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import io.reactivex.disposables.CompositeDisposable;
import org.apache.commons.lang3.StringUtils;
/**
* Service to speak the compass directions.
*
*/
public class SpeechService extends Service implements OnInitListener {
private static final int SPEECH_MINPAUSE_SECONDS = 5;
private static final int SPEECH_MAXPAUSE_SECONDS = 30;
@Nullable
private static Activity startingActivity;
private static final Object startingActivityLock = new Object();
/**
* Text to speech API of Android
*/
private TextToSpeech tts;
/**
* TTS has been initialized and we can speak.
*/
private boolean initialized = false;
protected float direction;
protected Geopoint position;
private final GeoDirHandler geoDirHandler = new GeoDirHandler() {
@Override
public void updateGeoDir(@NonNull final GeoData newGeo, final float newDirection) {
// We might receive a location update before the target has been set. In this case, do nothing.
if (target == null) {
return;
}
position = newGeo.getCoords();
direction = newDirection;
// avoid any calculation, if the delay since the last output is not long enough
final long now = System.currentTimeMillis();
if (now - lastSpeechTime <= SPEECH_MINPAUSE_SECONDS * 1000) {
return;
}
// to speak, we want max pause to have elapsed or distance to geopoint to have changed by a given amount
final float distance = position.distanceTo(target);
if (now - lastSpeechTime <= SPEECH_MAXPAUSE_SECONDS * 1000 && Math.abs(lastSpeechDistance - distance) < getDeltaForDistance(distance)) {
return;
}
final String text = TextFactory.getText(position, target, direction);
if (StringUtils.isNotEmpty(text)) {
lastSpeechTime = System.currentTimeMillis();
lastSpeechDistance = distance;
speak(text);
}
}
};
/**
* remember when we talked the last time
*/
private long lastSpeechTime = 0;
private float lastSpeechDistance = 0.0f;
private Geopoint target;
private final CompositeDisposable initDisposable = new CompositeDisposable();
@Override
public IBinder onBind(final Intent intent) {
return null;
}
/**
* Return distance required to be moved based on overall distance.<br>
*
* @param distance
* in km
* @return delta in km
*/
private static float getDeltaForDistance(final float distance) {
if (distance > 1.0) {
return 0.2f;
}
if (distance > 0.05) {
return distance / 5.0f;
}
return 0f;
}
@Override
public void onCreate() {
super.onCreate();
tts = new TextToSpeech(this, this);
}
@Override
public void onDestroy() {
initDisposable.clear();
if (tts != null) {
tts.stop();
tts.shutdown();
}
super.onDestroy();
}
@Override
public void onInit(final int status) {
// The text to speech system takes some time to initialize.
if (status != TextToSpeech.SUCCESS) {
Log.e("Text to speech cannot be initialized.");
return;
}
final int switchLocale = tts.setLanguage(Settings.getApplicationLocale());
if (switchLocale == TextToSpeech.LANG_MISSING_DATA) {
synchronized (startingActivityLock) {
if (startingActivity != null) {
startingActivity.startActivity(new Intent(Engine.ACTION_INSTALL_TTS_DATA));
}
}
return;
}
if (switchLocale == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.e("Current language not supported by text to speech.");
synchronized (startingActivityLock) {
if (startingActivity != null) {
ActivityMixin.showToast(startingActivity, R.string.err_tts_lang_not_supported);
}
}
return;
}
initialized = true;
synchronized (startingActivityLock) {
final Activity startingActivityChecked = startingActivity;
if (startingActivityChecked != null) {
initDisposable.add(geoDirHandler.start(GeoDirHandler.UPDATE_GEODIR));
ActivityMixin.showShortToast(startingActivity, startingActivityChecked.getString(R.string.tts_started));
}
}
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent != null) {
target = intent.getParcelableExtra(Intents.EXTRA_COORDS);
}
return START_NOT_STICKY; // service can be stopped by system, if under memory pressure
}
@SuppressWarnings("deprecation")
private void speak(final String text) {
if (!initialized) {
return;
}
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
}
public static void startService(final Activity activity, final Geopoint dstCoords) {
synchronized (startingActivityLock) {
startingActivity = activity;
}
final Intent talkingService = new Intent(activity, SpeechService.class);
talkingService.putExtra(Intents.EXTRA_COORDS, dstCoords);
activity.startService(talkingService);
}
public static void stopService(final Activity activity) {
synchronized (startingActivityLock) {
if (activity.stopService(new Intent(activity, SpeechService.class))) {
ActivityMixin.showShortToast(activity, activity.getString(R.string.tts_stopped));
}
startingActivity = null;
}
}
public static boolean isRunning() {
return startingActivity != null;
}
}