Extended voice announcements (spoken updates)

- better scalability
- setting for contents
- inform about duration
- inform about GPS status
This commit is contained in:
jannis 2020-01-07 16:41:02 +01:00
parent f0684647c3
commit 3455702375
19 changed files with 829 additions and 207 deletions

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de> ~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~ ~
~ This file is part of FitoTrack ~ This file is part of FitoTrack
@ -26,7 +27,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
@ -38,7 +38,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning"> tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".activity.VoiceAnnouncementsSettingsActivity"></activity>
<activity <activity
android:name=".activity.ShowWorkoutMapActivity" android:name=".activity.ShowWorkoutMapActivity"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.RingtonePreference;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import androidx.annotation.StringRes;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import de.tadris.fitness.R;
import de.tadris.fitness.util.unit.UnitUtils;
public abstract class FitoTrackSettingsActivity extends PreferenceActivity {
protected void shareFile(Uri uri) {
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setDataAndType(uri, getContentResolver().getType(uri));
intentShareFile.putExtra(Intent.EXTRA_STREAM, uri);
intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intentShareFile, getString(R.string.shareFile)));
Log.d("Export", uri.toString());
Log.d("Export", getContentResolver().getType(uri));
try {
Log.d("Export", new BufferedInputStream(getContentResolver().openInputStream(uri)).toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
protected void showErrorDialog(Exception e, @StringRes int title, @StringRes int message) {
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(getString(message) + "\n\n" + e.getMessage())
.setPositiveButton(R.string.okay, null)
.create().show();
}
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
protected static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
// Set the summary to reflect the new value.
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
} else if (preference instanceof RingtonePreference) {
// For ringtone preferences, look up the correct display value
// using RingtoneManager.
if (TextUtils.isEmpty(stringValue)) {
// Empty values correspond to 'silent' (no ringtone).
preference.setSummary(R.string.pref_ringtone_silent);
} else {
Ringtone ringtone = RingtoneManager.getRingtone(
preference.getContext(), Uri.parse(stringValue));
if (ringtone == null) {
// Clear the summary if there was a lookup error.
preference.setSummary(null);
} else {
// Set the summary to reflect the new ringtone display
// name.
String name = ringtone.getTitle(preference.getContext());
preference.setSummary(name);
}
}
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
};
protected static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
@Override
protected void onDestroy() {
super.onDestroy();
UnitUtils.setUnit(this);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onMenuItemSelected(featureId, item);
}
}

View File

@ -31,8 +31,6 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.PowerManager; import android.os.PowerManager;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -52,11 +50,11 @@ import org.mapsforge.map.layer.overlay.Polyline;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import de.tadris.fitness.Instance; import de.tadris.fitness.Instance;
import de.tadris.fitness.R; import de.tadris.fitness.R;
import de.tadris.fitness.data.UserPreferences; import de.tadris.fitness.announcement.AnnouncementGPSStatus;
import de.tadris.fitness.announcement.VoiceAnnouncements;
import de.tadris.fitness.data.Workout; import de.tadris.fitness.data.Workout;
import de.tadris.fitness.map.MapManager; import de.tadris.fitness.map.MapManager;
import de.tadris.fitness.map.tilesource.TileSources; import de.tadris.fitness.map.tilesource.TileSources;
@ -66,7 +64,7 @@ import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.ThemeManager; import de.tadris.fitness.util.ThemeManager;
import de.tadris.fitness.util.unit.UnitUtils; import de.tadris.fitness.util.unit.UnitUtils;
public class RecordWorkoutActivity extends FitoTrackActivity implements LocationListener.LocationChangeListener, WorkoutRecorder.WorkoutRecorderListener { public class RecordWorkoutActivity extends FitoTrackActivity implements LocationListener.LocationChangeListener, WorkoutRecorder.WorkoutRecorderListener, VoiceAnnouncements.VoiceAnnouncementCallback {
public static String ACTIVITY= Workout.WORKOUT_TYPE_RUNNING; public static String ACTIVITY= Workout.WORKOUT_TYPE_RUNNING;
@ -86,8 +84,8 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
private Intent locationListener; private Intent locationListener;
private Intent pressureService; private Intent pressureService;
private boolean saved= false; private boolean saved= false;
private TextToSpeech tts;
private boolean ttsReady = false; private VoiceAnnouncements voiceAnnouncements;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -108,10 +106,8 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
recorder= new WorkoutRecorder(this, ACTIVITY, this); recorder= new WorkoutRecorder(this, ACTIVITY, this);
recorder.start(); recorder.start();
tts = new TextToSpeech(this, (int status) -> { voiceAnnouncements = new VoiceAnnouncements(this, this);
ttsReady = status == TextToSpeech.SUCCESS && tts.setLanguage(Locale.getDefault())>=0;
Log.d("Recorder", "TTS-ready: " + ttsReady);
});
infoViews[0]= new InfoViewHolder(findViewById(R.id.recordInfo1Title), findViewById(R.id.recordInfo1Value)); infoViews[0]= new InfoViewHolder(findViewById(R.id.recordInfo1Title), findViewById(R.id.recordInfo1Value));
infoViews[1]= new InfoViewHolder(findViewById(R.id.recordInfo2Title), findViewById(R.id.recordInfo2Value)); infoViews[1]= new InfoViewHolder(findViewById(R.id.recordInfo2Title), findViewById(R.id.recordInfo2Value));
infoViews[2]= new InfoViewHolder(findViewById(R.id.recordInfo3Title), findViewById(R.id.recordInfo3Value)); infoViews[2]= new InfoViewHolder(findViewById(R.id.recordInfo3Title), findViewById(R.id.recordInfo3Value));
@ -187,40 +183,23 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
}).start(); }).start();
} }
long lastSpokenUpdateTime = 0;
int lastSpokenUpdateDistance = 0;
private void updateDescription() { private void updateDescription() {
long duration = recorder.getDuration(); long duration = recorder.getDuration();
int distanceInMeters = recorder.getDistance(); int distanceInMeters = recorder.getDistanceInMeters();
final String distanceCaption = getString(R.string.workoutDistance); final String distanceCaption = getString(R.string.workoutDistance);
final String distance = UnitUtils.getDistance(distanceInMeters); final String distance = UnitUtils.getDistance(distanceInMeters);
final String avgSpeedCaption = getString(R.string.workoutAvgSpeed);
final String avgSpeed = UnitUtils.getSpeed(Math.min(100d, recorder.getAvgSpeed())); final String avgSpeed = UnitUtils.getSpeed(Math.min(100d, recorder.getAvgSpeed()));
if (isResumed) { if (isResumed) {
timeView.setText(UnitUtils.getHourMinuteSecondTime(duration)); timeView.setText(UnitUtils.getHourMinuteSecondTime(duration));
infoViews[0].setText(distanceCaption, distance); infoViews[0].setText(distanceCaption, distance);
infoViews[1].setText(getString(R.string.workoutBurnedEnergy), recorder.getCalories() + " kcal"); infoViews[1].setText(getString(R.string.workoutBurnedEnergy), recorder.getCalories() + " kcal");
infoViews[2].setText(avgSpeedCaption, avgSpeed); infoViews[2].setText(getString(R.string.workoutAvgSpeedShort), avgSpeed);
infoViews[3].setText(getString(R.string.workoutPauseDuration), UnitUtils.getHourMinuteSecondTime(recorder.getPauseDuration())); infoViews[3].setText(getString(R.string.workoutPauseDuration), UnitUtils.getHourMinuteSecondTime(recorder.getPauseDuration()));
} }
final UserPreferences prefs = Instance.getInstance(this).userPreferences; voiceAnnouncements.check(recorder);
final long intervalT = 60 * 1000 * prefs.getSpokenUpdateTimePeriod();
final int intervalInMeters = (int) (1000.0 / UnitUtils.CHOSEN_SYSTEM.getDistanceFromKilometers(1) * prefs.getSpokenUpdateDistancePeriod());
if (!ttsReady ||
(intervalT == 0 || duration / intervalT == lastSpokenUpdateTime / intervalT)
&& (intervalInMeters == 0 || distanceInMeters / intervalInMeters == lastSpokenUpdateDistance / intervalInMeters)
) return;
final String text = distanceCaption + ": " + distance + ". "
+ avgSpeedCaption + ": " + avgSpeed;
Log.d("Recorder", "TTS speak: " + text);
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, "updateDescription" + duration);
lastSpokenUpdateTime = duration;
lastSpokenUpdateDistance = distanceInMeters;
} }
private void stop(){ private void stop(){
@ -344,9 +323,8 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
mapView.destroyAll(); mapView.destroyAll();
AndroidGraphicFactory.clearResourceMemoryCache(); AndroidGraphicFactory.clearResourceMemoryCache();
// Shutdown TTS engine // Shutdown TTS
ttsReady = false; voiceAnnouncements.destroy();
tts.shutdown();
super.onDestroy(); super.onDestroy();
if(wakeLock.isHeld()){ if(wakeLock.isHeld()){
@ -403,9 +381,22 @@ public class RecordWorkoutActivity extends FitoTrackActivity implements Location
gpsFound= true; gpsFound= true;
hideWaitOverlay(); hideWaitOverlay();
} }
AnnouncementGPSStatus announcement = new AnnouncementGPSStatus(RecordWorkoutActivity.this);
if (announcement.isEnabled()) {
if (oldState == WorkoutRecorder.GpsState.SIGNAL_LOST) { // GPS Signal found
voiceAnnouncements.speak(announcement.getSpokenGPSFound());
} else if (state == WorkoutRecorder.GpsState.SIGNAL_LOST) {
voiceAnnouncements.speak(announcement.getSpokenGPSLost());
}
}
}); });
} }
@Override
public void onVoiceAnnouncementIsReady(boolean available) {
}
static class InfoViewHolder { static class InfoViewHolder {
final TextView titleView; final TextView titleView;
final TextView valueView; final TextView valueView;

View File

@ -25,127 +25,28 @@ import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.preference.RingtonePreference;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.NumberPicker; import android.widget.NumberPicker;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import de.tadris.fitness.R; import de.tadris.fitness.R;
import de.tadris.fitness.announcement.VoiceAnnouncements;
import de.tadris.fitness.export.BackupController; import de.tadris.fitness.export.BackupController;
import de.tadris.fitness.export.RestoreController; import de.tadris.fitness.export.RestoreController;
import de.tadris.fitness.util.unit.UnitUtils; import de.tadris.fitness.util.unit.UnitUtils;
import de.tadris.fitness.view.ProgressDialogController; import de.tadris.fitness.view.ProgressDialogController;
public class SettingsActivity extends PreferenceActivity { public class SettingsActivity extends FitoTrackSettingsActivity {
private void shareFile(Uri uri) {
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setDataAndType(uri, getContentResolver().getType(uri));
intentShareFile.putExtra(Intent.EXTRA_STREAM, uri);
intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intentShareFile, getString(R.string.shareFile)));
Log.d("Export", uri.toString());
Log.d("Export", getContentResolver().getType(uri));
try {
Log.d("Export", new BufferedInputStream(getContentResolver().openInputStream(uri)).toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
protected void showErrorDialog(Exception e, @StringRes int title, @StringRes int message){
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(getString(message) + "\n\n" + e.getMessage())
.setPositiveButton(R.string.okay, null)
.create().show();
}
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
private static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
// Set the summary to reflect the new value.
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
} else if (preference instanceof RingtonePreference) {
// For ringtone preferences, look up the correct display value
// using RingtoneManager.
if (TextUtils.isEmpty(stringValue)) {
// Empty values correspond to 'silent' (no ringtone).
preference.setSummary(R.string.pref_ringtone_silent);
} else {
Ringtone ringtone = RingtoneManager.getRingtone(
preference.getContext(), Uri.parse(stringValue));
if (ringtone == null) {
// Clear the summary if there was a lookup error.
preference.setSummary(null);
} else {
// Set the summary to reflect the new ringtone display
// name.
String name = ringtone.getTitle(preference.getContext());
preference.setSummary(name);
}
}
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
};
private static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
private final Handler mHandler = new Handler(); private final Handler mHandler = new Handler();
@ -166,7 +67,7 @@ public class SettingsActivity extends PreferenceActivity {
return true; return true;
}); });
findPreference("speech").setOnPreferenceClickListener(preference -> { findPreference("speech").setOnPreferenceClickListener(preference -> {
showSpeechConfig(); checkTTSandShowConfig();
return true; return true;
}); });
findPreference("import").setOnPreferenceClickListener(preference -> { findPreference("import").setOnPreferenceClickListener(preference -> {
@ -180,6 +81,24 @@ public class SettingsActivity extends PreferenceActivity {
} }
private VoiceAnnouncements voiceAnnouncements;
private void checkTTSandShowConfig() {
voiceAnnouncements = new VoiceAnnouncements(this, available -> {
if (available) {
showSpeechConfig();
} else {
// TextToSpeech is not available
Toast.makeText(SettingsActivity.this, R.string.ttsNotAvailable, Toast.LENGTH_LONG).show();
}
voiceAnnouncements.destroy();
});
}
private void showSpeechConfig() {
startActivity(new Intent(this, VoiceAnnouncementsSettingsActivity.class));
}
private void showExportDialog() { private void showExportDialog() {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.exportData) .setTitle(R.string.exportData)
@ -305,44 +224,6 @@ public class SettingsActivity extends PreferenceActivity {
d.create().show(); d.create().show();
} }
private void showSpeechConfig() {
UnitUtils.setUnit(this); // Maybe the user changed unit system
final AlertDialog.Builder d = new AlertDialog.Builder(this);
final SharedPreferences preferences= PreferenceManager.getDefaultSharedPreferences(this);
d.setTitle(getString(R.string.pref_spoken_updates_summary));
View v= getLayoutInflater().inflate(R.layout.dialog_spoken_updates_picker, null);
NumberPicker npT = v.findViewById(R.id.spokenUpdatesTimePicker);
npT.setMaxValue(60);
npT.setMinValue(0);
npT.setFormatter(value -> value == 0 ? "No speech" : value + " min");
final String updateTimeVariable = "spokenUpdateTimePeriod";
npT.setValue(preferences.getInt(updateTimeVariable, 0));
npT.setWrapSelectorWheel(false);
final String distanceUnit = " " + UnitUtils.CHOSEN_SYSTEM.getLongDistanceUnit();
NumberPicker npD = v.findViewById(R.id.spokenUpdatesDistancePicker);
npD.setMaxValue(10);
npD.setMinValue(0);
npD.setFormatter(value -> value == 0 ? "No speech" : value + distanceUnit);
final String updateDistanceVariable = "spokenUpdateDistancePeriod";
npD.setValue(preferences.getInt(updateDistanceVariable, 0));
npD.setWrapSelectorWheel(false);
d.setView(v);
d.setNegativeButton(R.string.cancel, null);
d.setPositiveButton(R.string.okay, (dialog, which) -> {
preferences.edit()
.putInt(updateTimeVariable, npT.getValue())
.putInt(updateDistanceVariable, npD.getValue())
.apply();
});
d.create().show();
}
/** /**
* Set up the {@link android.app.ActionBar}, if the API is available. * Set up the {@link android.app.ActionBar}, if the API is available.
*/ */
@ -354,19 +235,4 @@ public class SettingsActivity extends PreferenceActivity {
} }
} }
@Override
protected void onDestroy() {
super.onDestroy();
UnitUtils.setUnit(this);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onMenuItemSelected(featureId, item);
}
} }

View File

@ -91,7 +91,7 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
addTitle(getString(R.string.workoutSpeed)); addTitle(getString(R.string.workoutSpeed));
addKeyValue(getString(R.string.workoutAvgSpeed), UnitUtils.getSpeed(workout.avgSpeed), addKeyValue(getString(R.string.workoutAvgSpeedShort), UnitUtils.getSpeed(workout.avgSpeed),
getString(R.string.workoutTopSpeed), UnitUtils.getSpeed(workout.topSpeed)); getString(R.string.workoutTopSpeed), UnitUtils.getSpeed(workout.topSpeed));
addSpeedDiagram(); addSpeedDiagram();

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.activity;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.NumberPicker;
import de.tadris.fitness.R;
import de.tadris.fitness.util.unit.UnitUtils;
public class VoiceAnnouncementsSettingsActivity extends FitoTrackSettingsActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupActionBar();
setTitle(R.string.voiceAnnouncementsTitle);
addPreferencesFromResource(R.xml.preferences_voice_announcements);
findPreference("speechConfig").setOnPreferenceClickListener(preference -> {
showSpeechConfig();
return true;
});
}
private void showSpeechConfig() {
UnitUtils.setUnit(this); // Maybe the user changed unit system
final AlertDialog.Builder d = new AlertDialog.Builder(this);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
d.setTitle(getString(R.string.pref_voice_announcements_summary));
View v = getLayoutInflater().inflate(R.layout.dialog_spoken_updates_picker, null);
NumberPicker npT = v.findViewById(R.id.spokenUpdatesTimePicker);
npT.setMaxValue(60);
npT.setMinValue(0);
npT.setFormatter(value -> value == 0 ? "No speech" : value + " min");
final String updateTimeVariable = "spokenUpdateTimePeriod";
npT.setValue(preferences.getInt(updateTimeVariable, 0));
npT.setWrapSelectorWheel(false);
final String distanceUnit = " " + UnitUtils.CHOSEN_SYSTEM.getLongDistanceUnit();
NumberPicker npD = v.findViewById(R.id.spokenUpdatesDistancePicker);
npD.setMaxValue(10);
npD.setMinValue(0);
npD.setFormatter(value -> value == 0 ? "No speech" : value + distanceUnit);
final String updateDistanceVariable = "spokenUpdateDistancePeriod";
npD.setValue(preferences.getInt(updateDistanceVariable, 0));
npD.setWrapSelectorWheel(false);
d.setView(v);
d.setNegativeButton(R.string.cancel, null);
d.setPositiveButton(R.string.okay, (dialog, which) ->
preferences.edit()
.putInt(updateTimeVariable, npT.getValue())
.putInt(updateDistanceVariable, npD.getValue())
.apply());
d.create().show();
}
/**
* Set up the {@link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import android.preference.PreferenceManager;
import androidx.annotation.StringRes;
import de.tadris.fitness.recording.WorkoutRecorder;
public abstract class Announcement {
private Context context;
Announcement(Context context) {
this.context = context;
}
public boolean isEnabled() {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("announcement_" + getId(), isEnabledByDefault());
}
protected String getString(@StringRes int resId) {
return context.getString(resId);
}
public abstract String getId();
abstract boolean isEnabledByDefault();
abstract String getSpoken(WorkoutRecorder recorder);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.unit.UnitUtils;
public class AnnouncementAverageSpeed extends Announcement {
public AnnouncementAverageSpeed(Context context) {
super(context);
}
@Override
public String getId() {
return "avgSpeed";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
String avgSpeed = UnitUtils.getSpeed(recorder.getAvgSpeed());
return getString(R.string.workoutAvgSpeedLong) + ": " + avgSpeed + ".";
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.unit.UnitUtils;
public class AnnouncementDistance extends Announcement {
public AnnouncementDistance(Context context) {
super(context);
}
@Override
public String getId() {
return "distance";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
final String distance = UnitUtils.getDistance(recorder.getDistanceInMeters());
return getString(R.string.workoutDistance) + ": " + distance + ".";
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
public class AnnouncementDuration extends Announcement {
public AnnouncementDuration(Context context) {
super(context);
}
@Override
public String getId() {
return "duration";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
return getString(R.string.workoutDuration) + ": " + getSpokenTime(recorder.getDuration()) + ".";
}
private String getSpokenTime(long duration) {
final long minute = 1000L * 60;
final long hour = minute * 60;
StringBuilder spokenTime = new StringBuilder();
if (duration > hour) {
long hours = duration / hour;
duration = duration % hour; // Set duration to the rest
spokenTime.append(hours).append(" ");
spokenTime.append(getString(hours == 1 ? R.string.timeHourSingular : R.string.timeHourPlural)).append(" ")
.append(getString(R.string.and)).append(" ");
}
long minutes = duration / minute;
spokenTime.append(minutes).append(" ");
spokenTime.append(getString(minutes == 1 ? R.string.timeMinuteSingular : R.string.timeMinutePlural));
return spokenTime.toString();
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import de.tadris.fitness.R;
import de.tadris.fitness.recording.WorkoutRecorder;
public class AnnouncementGPSStatus extends Announcement {
public AnnouncementGPSStatus(Context context) {
super(context);
}
@Override
public String getId() {
return "gps-lost";
}
@Override
boolean isEnabledByDefault() {
return true;
}
@Override
String getSpoken(WorkoutRecorder recorder) {
return "";
}
public String getSpokenGPSLost() {
return getString(R.string.gpsLost);
}
public String getSpokenGPSFound() {
return getString(R.string.gpsFound);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
public class AnnouncementManager {
private Context context;
private List<Announcement> announcements = new ArrayList<>();
public AnnouncementManager(Context context) {
this.context = context;
addAnnouncements();
}
private void addAnnouncements() {
announcements.add(new AnnouncementGPSStatus(context));
announcements.add(new AnnouncementDuration(context));
announcements.add(new AnnouncementDistance(context));
announcements.add(new AnnouncementAverageSpeed(context));
}
public List<Announcement> getAnnouncements() {
return announcements;
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
*
* This file is part of FitoTrack
*
* FitoTrack is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FitoTrack is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.tadris.fitness.announcement;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import java.util.Locale;
import de.tadris.fitness.Instance;
import de.tadris.fitness.data.UserPreferences;
import de.tadris.fitness.recording.WorkoutRecorder;
import de.tadris.fitness.util.unit.UnitUtils;
public class VoiceAnnouncements {
private TextToSpeech textToSpeech;
private boolean ttsAvailable;
private VoiceAnnouncementCallback callback;
private final AnnouncementManager manager;
private long lastSpokenUpdateTime = 0;
private int lastSpokenUpdateDistance = 0;
private final long intervalTime;
private final int intervalInMeters;
public VoiceAnnouncements(Context context, VoiceAnnouncementCallback callback) {
this.callback = callback;
UserPreferences prefs = Instance.getInstance(context).userPreferences;
textToSpeech = new TextToSpeech(context, this::ttsReady);
this.intervalTime = 60 * 1000 * prefs.getSpokenUpdateTimePeriod();
this.intervalInMeters = (int) (1000.0 / UnitUtils.CHOSEN_SYSTEM.getDistanceFromKilometers(1) * prefs.getSpokenUpdateDistancePeriod());
this.manager = new AnnouncementManager(context);
}
private void ttsReady(int status) {
ttsAvailable = status == TextToSpeech.SUCCESS && textToSpeech.setLanguage(Locale.getDefault()) >= 0;
callback.onVoiceAnnouncementIsReady(ttsAvailable);
}
public void check(WorkoutRecorder recorder) {
if (!ttsAvailable) {
return;
} // Cannot speak
boolean shouldSpeak = false;
if (intervalTime != 0 && recorder.getDuration() - lastSpokenUpdateTime > intervalTime) {
shouldSpeak = true;
}
if (intervalInMeters != 0 && recorder.getDistanceInMeters() - lastSpokenUpdateDistance > intervalInMeters) {
shouldSpeak = true;
}
if (shouldSpeak) {
speak(recorder);
}
}
private void speak(WorkoutRecorder recorder) {
for (Announcement announcement : manager.getAnnouncements()) {
speak(recorder, announcement);
}
lastSpokenUpdateTime = recorder.getDuration();
lastSpokenUpdateDistance = recorder.getDistanceInMeters();
}
private void speak(WorkoutRecorder recorder, Announcement announcement) {
if (!announcement.isEnabled()) {
return;
}
String text = announcement.getSpoken(recorder);
if (!text.equals("")) {
speak(text);
}
}
private int speakId = 1;
public void speak(String text) {
if (!ttsAvailable) {
return;
} // Cannot speak
Log.d("Recorder", "TTS speaks: " + text);
textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, "announcement" + (++speakId));
}
public void destroy() {
textToSpeech.shutdown();
}
public interface VoiceAnnouncementCallback {
void onVoiceAnnouncementIsReady(boolean available);
}
}

View File

@ -36,11 +36,11 @@ public class UserPreferences {
} }
public int getSpokenUpdateTimePeriod(){ public int getSpokenUpdateTimePeriod(){
return preferences.getInt("spokenUpdateTimePeriod", 5); return preferences.getInt("spokenUpdateTimePeriod", 0);
} }
public int getSpokenUpdateDistancePeriod(){ public int getSpokenUpdateDistancePeriod(){
return preferences.getInt("spokenUpdateDistancePeriod", 1); return preferences.getInt("spokenUpdateDistancePeriod", 0);
} }
public String getMapStyle(){ public String getMapStyle(){

View File

@ -155,7 +155,7 @@
android:id="@+id/recordInfo3Title" android:id="@+id/recordInfo3Title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/workoutAvgSpeed" android:text="@string/workoutAvgSpeedShort"
android:textAlignment="center" android:textAlignment="center"
android:textAllCaps="true" android:textAllCaps="true"
android:textStyle="bold" /> android:textStyle="bold" />

View File

@ -51,10 +51,6 @@
<string name="pref_weight">Dein Gewicht</string> <string name="pref_weight">Dein Gewicht</string>
<string name="pref_weight_summary">Dein Gewicht ist zur Kalorienberechnung wichtig</string> <string name="pref_weight_summary">Dein Gewicht ist zur Kalorienberechnung wichtig</string>
<!-- please translate -->
<string name="pref_spoken_updates">Spoken Updates</string>
<string name="pref_spoken_updates_summary">Choose the time and distance between spoken updates</string>
<string name="preferences">Einstellungen</string> <string name="preferences">Einstellungen</string>
<string name="recordWorkout">Workout aufzeichnen</string> <string name="recordWorkout">Workout aufzeichnen</string>
<string name="restore">Wiederherstellen</string> <string name="restore">Wiederherstellen</string>
@ -69,7 +65,7 @@
<string name="trackerRunningMessage">Dein Workout wird aufgezeichnet</string> <string name="trackerRunningMessage">Dein Workout wird aufgezeichnet</string>
<string name="trackingInfo">Tracking Info</string> <string name="trackingInfo">Tracking Info</string>
<string name="trackingInfoDescription">Info über den laufenden Tracker</string> <string name="trackingInfoDescription">Info über den laufenden Tracker</string>
<string name="workoutAvgSpeed">Durchschnittsgeschw.</string> <string name="workoutAvgSpeedShort">Durchschnittsgeschw.</string>
<string name="workoutBurnedEnergy">Verbrannte Kalorien</string> <string name="workoutBurnedEnergy">Verbrannte Kalorien</string>
<string name="workoutDate">Datum</string> <string name="workoutDate">Datum</string>
<string name="workoutDistance">Distanz</string> <string name="workoutDistance">Distanz</string>
@ -112,4 +108,19 @@
<string name="uploadFailed">Upload fehlgeschlagen</string> <string name="uploadFailed">Upload fehlgeschlagen</string>
<string name="uploadSuccessful">Upload erfolgreich</string> <string name="uploadSuccessful">Upload erfolgreich</string>
<string name="uploadFailedOsmNotAuthorized">Nicht autorisiert, nocheinmal versuchen</string> <string name="uploadFailedOsmNotAuthorized">Nicht autorisiert, nocheinmal versuchen</string>
<string name="ttsNotAvailable">Sprachausgabe ist nicht verfügbar</string>
<string name="workoutAvgSpeedLong">Durchschnittsgeschwindigkeit</string>
<string name="and">und</string>
<string name="announcementGPSStatus">GPS Status</string>
<string name="gpsFound">GPS Signal gefunden</string>
<string name="gpsLost">GPS Signal verloren</string>
<string name="pref_announcements_config_summary">Wähle die Zeit und Distanz zwischen den Sprachansagen</string>
<string name="pref_announcements_config_title">Auslöser der Ansage</string>
<string name="pref_announcements_content">Inhalt der Ansagen</string>
<string name="pref_voice_announcements_summary">Konfiguriere Sprachansagen, die dich während des Trainings informieren</string>
<string name="timeHourPlural">Stunden</string>
<string name="timeHourSingular">Stunde</string>
<string name="timeMinutePlural">Minuten</string>
<string name="timeMinuteSingular">Minute</string>
<string name="voiceAnnouncementsTitle">Sprachansagen</string>
</resources> </resources>

View File

@ -47,6 +47,26 @@
<string name="setPreferencesTitle">Set Preferences</string> <string name="setPreferencesTitle">Set Preferences</string>
<string name="setPreferencesMessage">You can set your preferred unit system and your weight in the settings.</string> <string name="setPreferencesMessage">You can set your preferred unit system and your weight in the settings.</string>
<string name="timeMinuteSingular">Minute</string>
<string name="timeMinutePlural">Minutes</string>
<string name="timeHourSingular">Hour</string>
<string name="timeHourPlural">Hours</string>
<string name="and">and</string>
<string name="announcementGPSStatus">GPS Status</string>
<string name="voiceAnnouncementsTitle">Voice Announcements</string>
<string name="pref_voice_announcements_summary">Configure voice prompts that inform you during the workout</string>
<string name="pref_announcements_config_title">Announcement Trigger</string>
<string name="pref_announcements_config_summary">Choose the time and distance between voice announcements</string>
<string name="pref_announcements_content">Content of the announcements</string>
<string name="gpsLost">GPS signal lost</string>
<string name="gpsFound">GPS signal found</string>
<string name="workoutTime">Time</string> <string name="workoutTime">Time</string>
<string name="workoutDate">Date</string> <string name="workoutDate">Date</string>
<string name="workoutDuration">Duration</string> <string name="workoutDuration">Duration</string>
@ -57,7 +77,8 @@
<string name="workoutPace">Pace</string> <string name="workoutPace">Pace</string>
<string name="workoutRoute">Route</string> <string name="workoutRoute">Route</string>
<string name="workoutSpeed">Speed</string> <string name="workoutSpeed">Speed</string>
<string name="workoutAvgSpeed">Average Speed</string> <string name="workoutAvgSpeedShort">Avg. Speed</string>
<string name="workoutAvgSpeedLong">Average Speed</string>
<string name="workoutTopSpeed">Top Speed</string> <string name="workoutTopSpeed">Top Speed</string>
<string name="workoutBurnedEnergy">Burned Energy</string> <string name="workoutBurnedEnergy">Burned Energy</string>
<string name="workoutTotalEnergy">Total Energy</string> <string name="workoutTotalEnergy">Total Energy</string>
@ -112,8 +133,6 @@
<string name="exportAsGpxFile">Export as GPX-File</string> <string name="exportAsGpxFile">Export as GPX-File</string>
<string name="pref_weight">Your Weight</string> <string name="pref_weight">Your Weight</string>
<string name="pref_weight_summary">Your weight is needed to calculate the burned calories</string> <string name="pref_weight_summary">Your weight is needed to calculate the burned calories</string>
<string name="pref_spoken_updates">Spoken Updates</string>
<string name="pref_spoken_updates_summary">Choose the time and distance between spoken updates</string>
<string name="pref_unit_system">Preferred system of units</string> <string name="pref_unit_system">Preferred system of units</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="exportData">Export Data</string> <string name="exportData">Export Data</string>
@ -128,4 +147,5 @@
<string name="cut">Cut the first/last 300 Meters</string> <string name="cut">Cut the first/last 300 Meters</string>
<string name="trackVisibilityPref">Track Visibility</string> <string name="trackVisibilityPref">Track Visibility</string>
<string name="description">Description</string> <string name="description">Description</string>
<string name="ttsNotAvailable">TextToSpeech is not available</string>
</resources> </resources>

View File

@ -44,8 +44,8 @@
android:key="speech" android:key="speech"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true"
android:summary="@string/pref_spoken_updates_summary" android:summary="@string/pref_voice_announcements_summary"
android:title="@string/pref_spoken_updates" /> android:title="@string/voiceAnnouncementsTitle" />
<PreferenceCategory android:title="@string/data"> <PreferenceCategory android:title="@string/data">

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Jannis Scheibe <jannis@tadris.de>
~
~ This file is part of FitoTrack
~
~ FitoTrack is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ FitoTrack is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="speechConfig"
android:title="@string/pref_announcements_config_title"
android:summary="@string/pref_announcements_config_summary" />
<PreferenceCategory android:title="@string/pref_announcements_content">
<CheckBoxPreference
android:defaultValue="true"
android:key="announcement_gps-lost"
android:title="@string/announcementGPSStatus" />
<CheckBoxPreference
android:defaultValue="true"
android:key="announcement_duration"
android:title="@string/workoutDuration" />
<CheckBoxPreference
android:defaultValue="true"
android:key="announcement_distance"
android:title="@string/workoutDistance" />
<CheckBoxPreference
android:defaultValue="true"
android:key="announcement_avgSpeed"
android:title="@string/workoutAvgSpeedLong" />
</PreferenceCategory>
</PreferenceScreen>