diff --git a/app/build.gradle b/app/build.gradle index dd94c66..4ba995c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { namespace "org.walkersguide.android" defaultConfig { - versionCode 41 - versionName '3.2.0-b4' + versionCode 42 + versionName '3.2.0-b5' minSdkVersion 21 targetSdkVersion 35 multiDexEnabled true @@ -140,7 +140,7 @@ android.applicationVariants.all { variant -> group = 'Open' description "Installs and opens the APK for ${variant.description}." doFirst { - String activityClass = ".ui.activity.MainActivity" + String activityClass = "org.walkersguide.android.ui.activity.MainActivity" commandLine android.adbExe, 'shell', 'am', 'start', '-n', "${variant.applicationId}/${activityClass}" } diff --git a/app/src/main/java/org/walkersguide/android/data/ObjectWithId.java b/app/src/main/java/org/walkersguide/android/data/ObjectWithId.java index aede0bc..bf47a29 100644 --- a/app/src/main/java/org/walkersguide/android/data/ObjectWithId.java +++ b/app/src/main/java/org/walkersguide/android/data/ObjectWithId.java @@ -325,6 +325,22 @@ public boolean setUserAnnotation(String newAnnotation) { public abstract Coordinates getCoordinates(); + public String formatRelativeBearingFromCurrentLocation(boolean showPreciseBearingValues) { + RelativeBearing relativeBearing = relativeBearingFromCurrentLocation(); + if (relativeBearing != null) { + String output = relativeBearing.getDirection().toString(); + if (showPreciseBearingValues) { + output += " "; + output += String.format( + Locale.ROOT, + GlobalInstance.getStringResource(R.string.preciseBearingValues), + relativeBearing.getDegree()); + } + return output; + } + return ""; + } + public String formatDistanceAndRelativeBearingFromCurrentLocation(int distancePluralResourceId) { return formatDistanceAndRelativeBearingFromCurrentLocation(distancePluralResourceId, false); } @@ -332,24 +348,14 @@ public String formatDistanceAndRelativeBearingFromCurrentLocation(int distancePl public String formatDistanceAndRelativeBearingFromCurrentLocation( int distancePluralResourceId, boolean showPreciseBearingValues) { Integer distance = distanceFromCurrentLocation(); - Bearing bearing = bearingFromCurrentLocation(); - if (distance != null && bearing != null) { - RelativeBearing relativeBearing = bearing.relativeToCurrentBearing(); - if (relativeBearing != null) { - String output = String.format( - Locale.getDefault(), - "%1$s, %2$s", - GlobalInstance.getPluralResource(distancePluralResourceId, distance), - relativeBearing.getDirection()); - if (showPreciseBearingValues) { - output += " "; - output += String.format( - Locale.ROOT, - GlobalInstance.getStringResource(R.string.preciseBearingValues), - relativeBearing.getDegree()); - } - return output; - } + String relativeBearingFromCurrentLocationFormatted = formatRelativeBearingFromCurrentLocation(showPreciseBearingValues); + if (distance != null + && ! TextUtils.isEmpty(relativeBearingFromCurrentLocationFormatted)) { + return String.format( + Locale.getDefault(), + "%1$s, %2$s", + GlobalInstance.getPluralResource(distancePluralResourceId, distance), + relativeBearingFromCurrentLocationFormatted); } return ""; } @@ -377,6 +383,14 @@ public Bearing bearingFromCurrentLocation() { return null; } + public RelativeBearing relativeBearingFromCurrentLocation() { + Bearing bearing = bearingFromCurrentLocation(); + if (bearing != null) { + return bearing.relativeToCurrentBearing(); + } + return null; + } + public Bearing bearingTo(ObjectWithId other) { if (other != null) { return this.getCoordinates().bearingTo(other.getCoordinates()); diff --git a/app/src/main/java/org/walkersguide/android/sensor/DeviceSensorManager.java b/app/src/main/java/org/walkersguide/android/sensor/DeviceSensorManager.java index 6a8f403..78a7649 100644 --- a/app/src/main/java/org/walkersguide/android/sensor/DeviceSensorManager.java +++ b/app/src/main/java/org/walkersguide/android/sensor/DeviceSensorManager.java @@ -2,6 +2,7 @@ import org.walkersguide.android.R; import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; import org.walkersguide.android.util.Helper; import timber.log.Timber; import org.walkersguide.android.data.angle.Bearing; @@ -213,8 +214,8 @@ private void announceBearingSensorChange() { String.format( "%1$s: %2$s", GlobalInstance.getStringResource(R.string.bearingSensor), - getSelectedBearingSensor()) - ); + getSelectedBearingSensor()), + MessageType.DISTANCE_OR_BEARING); } public void deviceOrientationChanged() { @@ -293,7 +294,7 @@ private void broadcastCurrentBearing(boolean isImportant) { public static final String ACTION_NEW_BEARING_VALUE_FROM_COMPASS = "new_bearing_value_from_compass"; // min time difference between compass values - private static final int MIN_COMPASS_VALUE_DELAY = 250; // 250 ms + private static final int MIN_COMPASS_VALUE_DELAY = 250; // in ms private BearingSensorAccuracyRating bearingSensorAccuracyRating = null; // accelerometer diff --git a/app/src/main/java/org/walkersguide/android/sensor/PositionManager.java b/app/src/main/java/org/walkersguide/android/sensor/PositionManager.java index e836226..56baad7 100644 --- a/app/src/main/java/org/walkersguide/android/sensor/PositionManager.java +++ b/app/src/main/java/org/walkersguide/android/sensor/PositionManager.java @@ -427,30 +427,4 @@ public void setSimulatedLocation(Point newPoint) { } } - - /** - * log to text file - */ - private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.ROOT); - - private static void appendToLog(String fileName, String message) { - File file = new File( - GlobalInstance.getContext().getExternalFilesDir(null), - String.format("%1$s.log", fileName)); - try { - FileWriter fw = new FileWriter(file, true); - BufferedWriter bw = new BufferedWriter(fw); - bw.write( - String.format( - "%1$s\t%2$s\n", - message, - sdf.format(new Date(System.currentTimeMillis()))) - ); - bw.close(); - fw.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } diff --git a/app/src/main/java/org/walkersguide/android/sensor/bearing/AcceptNewBearing.java b/app/src/main/java/org/walkersguide/android/sensor/bearing/AcceptNewBearing.java index b302a8d..652cfff 100644 --- a/app/src/main/java/org/walkersguide/android/sensor/bearing/AcceptNewBearing.java +++ b/app/src/main/java/org/walkersguide/android/sensor/bearing/AcceptNewBearing.java @@ -10,22 +10,23 @@ public class AcceptNewBearing implements Serializable { private static final long serialVersionUID = 1l; public static AcceptNewBearing newInstanceForObjectListUpdate() { - return new AcceptNewBearing(45, 1); + return new AcceptNewBearing(45, 1000l); } - public static AcceptNewBearing newInstanceForDistanceLabelUpdate() { - return new AcceptNewBearing(23, 1); + public static AcceptNewBearing newInstanceForBearingLabelUpdate() { + return new AcceptNewBearing(4, 500l); } - private final int angleThreshold, timeThreshold; + private final int angleThreshold; + private final long timeThreshold; private Bearing lastAcceptedBearing; private long lastAcceptedBearingTimestamp; - public AcceptNewBearing(int angleeThresholdInDegree, int timeThresholdInSeconds) { - this.angleThreshold = angleeThresholdInDegree; - this.timeThreshold = timeThresholdInSeconds; + public AcceptNewBearing(int angleThresholdInDegree, long timeThresholdInMs) { + this.angleThreshold = angleThresholdInDegree; + this.timeThreshold = timeThresholdInMs; this.lastAcceptedBearing = DeviceSensorManager.getInstance().getCurrentBearing(); this.lastAcceptedBearingTimestamp = 0l; } @@ -62,7 +63,7 @@ private boolean checkBearing(Bearing newBearing) { return true; } else if (this.lastAcceptedBearing.differenceTo(newBearing) < angleThreshold) { return false; - } else if (System.currentTimeMillis() - this.lastAcceptedBearingTimestamp < this.timeThreshold * 1000l) { + } else if (System.currentTimeMillis() - this.lastAcceptedBearingTimestamp < this.timeThreshold) { return false; } return true; diff --git a/app/src/main/java/org/walkersguide/android/sensor/position/AcceptNewPosition.java b/app/src/main/java/org/walkersguide/android/sensor/position/AcceptNewPosition.java index 204a1bf..c4aff8a 100644 --- a/app/src/main/java/org/walkersguide/android/sensor/position/AcceptNewPosition.java +++ b/app/src/main/java/org/walkersguide/android/sensor/position/AcceptNewPosition.java @@ -12,29 +12,35 @@ public class AcceptNewPosition implements Serializable { public static AcceptNewPosition newInstanceForObjectListUpdate() { return new AcceptNewPosition( - 50, 30, PositionManager.getInstance().getCurrentLocation()); + 50, 30000l, PositionManager.getInstance().getCurrentLocation()); + } + + public static AcceptNewPosition newInstanceForDistanceLabelUpdate() { + return new AcceptNewPosition(2, 1000l, null); } public static AcceptNewPosition newInstanceForTtsAnnouncement() { return new AcceptNewPosition( SettingsManager.getInstance().getTtsSettings().getDistanceAnnouncementInterval(), - 10, + 10000l, PositionManager.getInstance().getCurrentLocation()); } - public static AcceptNewPosition newInstanceForDistanceLabelUpdate() { - return new AcceptNewPosition(3, 3, null); + public static AcceptNewPosition newInstanceForTtsAnnouncementOnFocus() { + return new AcceptNewPosition( + 4, 3000l, PositionManager.getInstance().getCurrentLocation()); } - private final int distanceThreshold, timeThreshold; + private final int distanceThreshold; + private final long timeThreshold; private Point lastAcceptedPoint; private long lastAcceptedPointTimestamp; - public AcceptNewPosition(int distanceThresholdInMeters, int timeThresholdInSeconds, Point initAcceptedPoint) { + public AcceptNewPosition(int distanceThresholdInMeters, long timeThresholdInMs, Point initAcceptedPoint) { this.distanceThreshold = distanceThresholdInMeters; - this.timeThreshold = timeThresholdInSeconds; + this.timeThreshold = timeThresholdInMs; this.lastAcceptedPoint = initAcceptedPoint; this.lastAcceptedPointTimestamp = 0l; } @@ -71,7 +77,7 @@ private boolean checkPoint(Point newPoint) { return true; } else if (this.lastAcceptedPoint.distanceTo(newPoint) < this.distanceThreshold) { return false; - } else if (System.currentTimeMillis() - this.lastAcceptedPointTimestamp < this.timeThreshold * 1000l) { + } else if (System.currentTimeMillis() - this.lastAcceptedPointTimestamp < this.timeThreshold) { return false; } return true; diff --git a/app/src/main/java/org/walkersguide/android/server/address/ResolveCoordinatesTask.java b/app/src/main/java/org/walkersguide/android/server/address/ResolveCoordinatesTask.java index d3a36ce..e3c20ea 100644 --- a/app/src/main/java/org/walkersguide/android/server/address/ResolveCoordinatesTask.java +++ b/app/src/main/java/org/walkersguide/android/server/address/ResolveCoordinatesTask.java @@ -15,17 +15,12 @@ import java.util.Locale; import org.json.JSONObject; import org.walkersguide.android.data.object_with_id.Point; +import org.walkersguide.android.util.Helper; public class ResolveCoordinatesTask extends ServerTask { - private Point point; - - public ResolveCoordinatesTask(Point point) { - this.point = point; - } - - @Override public void execute() throws AddressException { + public static StreetAddress getAddress(Point coordinatesPoint) throws AddressException { final HistoryProfile addressPointProfile = HistoryProfile.addressPoints(); // first look into local database @@ -36,11 +31,11 @@ public ResolveCoordinatesTask(Point point) { StreetAddress address = (StreetAddress) objectWithId; // calculate distance // and select, if within 20m radius - if (this.point.distanceTo(address) < 20) { - ServerTaskExecutor.sendResolveCoordinatesTaskSuccessfulBroadcast(getId(), address); - return; + if (coordinatesPoint.distanceTo(address) < 20) { + return address; } // closest street address from cache is to far away + // that's possible to say because the request above sorts by distance break; } } @@ -51,9 +46,9 @@ public ResolveCoordinatesTask(Point point) { Locale.ROOT, "%1$s/reverse?format=jsonv2&lat=%2$f&lon=%3$f&accept-language=%4$s&addressdetails=1&zoom=18", AddressUtility.ADDRESS_RESOLVER_URL, - this.point.getCoordinates().getLatitude(), - this.point.getCoordinates().getLongitude(), - Locale.getDefault().getLanguage()); + coordinatesPoint.getCoordinates().getLatitude(), + coordinatesPoint.getCoordinates().getLongitude(), + Helper.getAppLocale().getLanguage()); JSONObject jsonStreetAddress = ServerUtility.performRequestAndReturnJsonObject( requestUrl, null, AddressException.class); if (jsonStreetAddress.has("error")) { @@ -68,15 +63,27 @@ public ResolveCoordinatesTask(Point point) { AddressException.RC_BAD_RESPONSE); } - if (! isCancelled()) { - // check for accuracy of address - if (this.point.distanceTo(newAddress) > 100) { - // if the address differs for more than 100 meters, don't take it - throw new AddressException( - AddressException.RC_NO_ADDRESS_FOR_COORDINATES); - } + // check for accuracy of address + if (coordinatesPoint.distanceTo(newAddress) > 100) { + // if the address differs for more than 100 meters, don't take it + throw new AddressException( + AddressException.RC_NO_ADDRESS_FOR_COORDINATES); + } + + addressPointProfile.addObject(newAddress); + return newAddress; + } + + + private Point point; - addressPointProfile.addObject(newAddress); + public ResolveCoordinatesTask(Point point) { + this.point = point; + } + + @Override public void execute() throws AddressException { + StreetAddress newAddress = getAddress(this.point); + if (! isCancelled() && newAddress != null) { ServerTaskExecutor.sendResolveCoordinatesTaskSuccessfulBroadcast(getId(), newAddress); } } diff --git a/app/src/main/java/org/walkersguide/android/server/wg/WgUtility.java b/app/src/main/java/org/walkersguide/android/server/wg/WgUtility.java index 3bb18e8..e8a22d6 100644 --- a/app/src/main/java/org/walkersguide/android/server/wg/WgUtility.java +++ b/app/src/main/java/org/walkersguide/android/server/wg/WgUtility.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; -import java.util.Locale; import org.json.JSONArray; @@ -24,6 +23,7 @@ import org.walkersguide.android.server.wg.poi.PoiCategory; import org.walkersguide.android.util.GlobalInstance; import org.walkersguide.android.util.SettingsManager; +import org.walkersguide.android.util.Helper; public class WgUtility { @@ -184,7 +184,7 @@ public static JSONObject createServerParamList() throws JSONException { requestJson.put( "session_id", GlobalInstance.getInstance().getSessionId()); requestJson.put( - "language", Locale.getDefault().getLanguage()); + "language", Helper.getAppLocale().getLanguage()); requestJson.put( "prefer_translated_strings_in_osm_tags", SettingsManager.getInstance().getPreferTranslatedStrings()); // selected map id diff --git a/app/src/main/java/org/walkersguide/android/server/wg/p2p/P2pRouteTask.java b/app/src/main/java/org/walkersguide/android/server/wg/p2p/P2pRouteTask.java index dfd6027..fd55e05 100644 --- a/app/src/main/java/org/walkersguide/android/server/wg/p2p/P2pRouteTask.java +++ b/app/src/main/java/org/walkersguide/android/server/wg/p2p/P2pRouteTask.java @@ -1,5 +1,6 @@ package org.walkersguide.android.server.wg.p2p; +import org.walkersguide.android.server.address.AddressException; import org.walkersguide.android.R; import org.walkersguide.android.database.profile.StaticProfile; import org.walkersguide.android.server.wg.poi.PoiProfile; @@ -24,6 +25,9 @@ import org.walkersguide.android.util.GlobalInstance; import org.walkersguide.android.data.object_with_id.segment.RouteSegment; import org.walkersguide.android.data.angle.Turn; +import org.walkersguide.android.data.object_with_id.point.GPS; +import org.walkersguide.android.data.object_with_id.point.point_with_address_data.StreetAddress; +import org.walkersguide.android.server.address.ResolveCoordinatesTask; public class P2pRouteTask extends ServerTask { @@ -43,6 +47,24 @@ public P2pRouteTask(P2pRouteRequest request, WayClassWeightSettings wayClassWeig throw new WgException(WgException.RC_START_OR_DESTINATION_MISSING); } + // if the start point is a nameless gps point then try to replace its name with the closest address nearby + if (startPoint instanceof GPS + && ! startPoint.hasCustomName()) { + Timber.d("start point gps: %1$s", startPoint.getName()); + StreetAddress closestAddress = null; + try { + closestAddress = ResolveCoordinatesTask.getAddress(startPoint); + } catch (AddressException e) {} + if (closestAddress != null) { + startPoint.rename( + String.format( + "%1$s %2$s", + GlobalInstance.getStringResource(R.string.labelNearby), + closestAddress.getName())); + Timber.d("renamed to %1$s", startPoint.getName()); + } + } + // create server param list JSONObject jsonServerParams = null; try { diff --git a/app/src/main/java/org/walkersguide/android/server/wg/p2p/WayClassWeightSettings.java b/app/src/main/java/org/walkersguide/android/server/wg/p2p/WayClassWeightSettings.java index 4ef3e78..4e6d96a 100644 --- a/app/src/main/java/org/walkersguide/android/server/wg/p2p/WayClassWeightSettings.java +++ b/app/src/main/java/org/walkersguide/android/server/wg/p2p/WayClassWeightSettings.java @@ -18,16 +18,20 @@ public class WayClassWeightSettings implements Serializable { private static final long serialVersionUID = 1l; public enum Preset { - URBAN_ON_FOOT( + SHORTEST_ROUTE( 1, + GlobalInstance.getStringResource(R.string.wcwsPresetShortestRoute), + presetShortestRoute()), + URBAN_ON_FOOT( + 2, GlobalInstance.getStringResource(R.string.wcwsPresetUrbanOnFoot), presetUrbanOnFoot()), URBAN_BY_CAR( - 2, + 3, GlobalInstance.getStringResource(R.string.wcwsPresetUrbanByCar), presetUrbanByCar()), HIKING( - 3, + 4, GlobalInstance.getStringResource(R.string.wcwsPresetHiking), presetHiking()); @@ -54,6 +58,14 @@ private Preset(int id, String label, WayClassWeightSettings settings) { return this.label.replace(" ", "\u00A0"); } + private static WayClassWeightSettings presetShortestRoute() { + LinkedHashMap map = new LinkedHashMap(); + for (WayClassType type : WayClassType.values()) { + map.put(type, WayClassWeight.NEUTRAL); + } + return new WayClassWeightSettings(map); + } + private static WayClassWeightSettings presetUrbanOnFoot() { LinkedHashMap map = new LinkedHashMap(); map.put(WayClassType.BIG_STREETS, WayClassWeight.SLIGHTLY_PREFER); diff --git a/app/src/main/java/org/walkersguide/android/tts/TTSWrapper.java b/app/src/main/java/org/walkersguide/android/tts/TTSWrapper.java index 66ba549..08f1c36 100644 --- a/app/src/main/java/org/walkersguide/android/tts/TTSWrapper.java +++ b/app/src/main/java/org/walkersguide/android/tts/TTSWrapper.java @@ -1,5 +1,10 @@ package org.walkersguide.android.tts; +import org.walkersguide.android.R; +import android.os.Looper; +import android.os.Handler; +import org.walkersguide.android.util.Helper; + import android.annotation.TargetApi; import android.content.Context; @@ -25,15 +30,22 @@ import android.media.AudioManager; import android.annotation.SuppressLint; import android.media.AudioFocusRequest; +import java.util.LinkedList; +import java.util.ListIterator; +import java.util.Iterator; +import java.util.Collections; public class TTSWrapper extends UtteranceProgressListener { - private static final String UTTERANCE_ID_SPEAK = "utteranceidspeak"; + private static final long SILENCE_DELAY = 400; + private static final String SILENCE_UTTERANCE_ID = "utterance_id_silence"; private static TTSWrapper managerInstance; private AccessibilityManager accessibilityManager; private AudioManager mAudioManager; private TextToSpeech tts; + private LinkedList messageQueue; + private MessageQueueItem lastItem; public static TTSWrapper getInstance() { if (managerInstance == null){ @@ -52,17 +64,22 @@ private static synchronized TTSWrapper getInstanceSynchronized() { private TTSWrapper() { accessibilityManager = (AccessibilityManager) GlobalInstance.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); mAudioManager = (AudioManager) GlobalInstance.getContext().getSystemService(Context.AUDIO_SERVICE); + appendToLog("\nopen session\n"); tts = new TextToSpeech(GlobalInstance.getContext(), new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if (status != TextToSpeech.ERROR) { - tts.setLanguage(Locale.getDefault()); + tts.setLanguage(Helper.getAppLocale()); tts.setOnUtteranceProgressListener(TTSWrapper.this); + //startTestMessages(); } else { tts = null; } } }); + + messageQueue = new LinkedList<>(); + lastItem = null; } public boolean isInitialized() { @@ -80,47 +97,252 @@ public boolean isScreenReaderEnabled() { return ! accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty(); } + + // speak + + public enum MessageType { + TOP_PRIORITY, INSTRUCTION, DISTANCE_OR_BEARING, TRACKED_OBJECT_MODE_DISTANCE, TRACKED_OBJECT_MODE_BEARING; + } + + public void announce(String message, MessageType messageType) { + if (SettingsManager.getInstance().getTtsSettings().getAnnouncementsEnabled()) { + consumeMessage(message, messageType); + } + } + public void screenReader(String message) { if (isScreenReaderEnabled()) { - speak(message); + consumeMessage(message, MessageType.TOP_PRIORITY); } } - public void announce(String message) { - if (SettingsManager.getInstance().getTtsSettings().getAnnouncementsEnabled()) { - speak(message); + public void testInstruction(float speechRate) { + if (! isInitialized()) return; + stopSpeaking(true); + speak( + new MessageQueueItem( + GlobalInstance.getStringResource(R.string.messageTestInstruction), + MessageType.TOP_PRIORITY), + speechRate); + } + + private synchronized void consumeMessage(String message, MessageType messageType) { + if (! isInitialized()) return; + MessageQueueItem newItem = new MessageQueueItem(message, messageType); + appendToLog( + String.format( + "consumeMessage: %1$s\nisSpeaking: %2$s, last: %3$s, Queue: %4$s", + newItem, isSpeaking(), lastItem != null ? lastItem.type : "none", printMessageQueue())); + + if (! isSpeaking()) { + messageQueue.clear(); + tryToGetAudioFocus(); + speak(newItem); + + } else if (lastItem != null) { + switch (newItem.type) { + + case TOP_PRIORITY: + stopSpeaking(true); + speak(newItem); + break; + + case INSTRUCTION: + if (lastItem.type == MessageType.TOP_PRIORITY + || lastItem.type == MessageType.INSTRUCTION) { + // remove all queued messages and add the current one, don't interrupt an ongoing message! + messageQueue.clear(); + messageQueue.addLast(newItem); + } else { + // order is relevant here: first extract the lastItem attributes then the call stop function + // because the latter nulls out the lastItem vairable + String lastItemMessage = lastItem.message; + MessageType lastItemType = lastItem.type; + stopSpeaking(true); + speak(newItem); + // only queue an interrupted message from the distance tracking mode + // messages of type DISTANCE_OR_BEARING are too volatile to requeue them + if (lastItemType == MessageType.TRACKED_OBJECT_MODE_DISTANCE) { + // create a new item to get an up to date timestamp + messageQueue.addLast( + new MessageQueueItem(lastItemMessage, lastItemType)); + } + } + break; + + case DISTANCE_OR_BEARING: + // remove all queued DISTANCE_OR_BEARING messages + cleanUpMessageQueue(newItem.type, 0); + // interrupt and speak directly or add in front of the queue + if (lastItem.type == MessageType.DISTANCE_OR_BEARING) { + stopSpeaking(false); + speak(newItem); + } else { + // important to add it in front of the queue so it it's spoken next + messageQueue.addFirst(newItem); + } + break; + + case TRACKED_OBJECT_MODE_DISTANCE: + // remove all but the last queued TRACKED_OBJECT_MODE_DISTANCE messages + cleanUpMessageQueue(newItem.type, 1); + messageQueue.addLast(newItem); + break; + + case TRACKED_OBJECT_MODE_BEARING: + // interrupt everything but a top priority / instruction message + if (lastItem.type == MessageType.TOP_PRIORITY + || lastItem.type == MessageType.INSTRUCTION) { + // make sure that the new message is spoken next + messageQueue.clear(); + messageQueue.addLast(newItem); + } else { + stopSpeaking(true); + speak(newItem); + } + break; + } } + + appendToLog( + String.format( + "processed: %1$s\nisSpeaking: %2$s, last: %3$s, Queue: %4$s", + newItem, isSpeaking(), lastItem != null ? lastItem.type : "none", printMessageQueue())); } - private void speak(String message) { - if (isInitialized()) { - stop(); + private synchronized void speak(MessageQueueItem item) { + speak(item, SettingsManager.getInstance().getTtsSettings().getSpeechRate()); + } + + private synchronized void speak(MessageQueueItem item, float speechRate) { + if (item == null) return; + appendToLog( + String.format("speak: %1$s", item.toString())); + lastItem = item; + tts.setAudioAttributes(buildAudioAttributes()); + tts.setSpeechRate(speechRate); + tts.speak( + item.message.length() > tts.getMaxSpeechInputLength() + ? item.message.substring(0, tts.getMaxSpeechInputLength()) + : item.message, + TextToSpeech.QUEUE_ADD, null, item.utteranceId); + } + + private synchronized void stopSpeaking(boolean clearMessageQueue) { + appendToLog( + String.format("stopSpeaking: clear queue: %1$s", clearMessageQueue)); + lastItem = null; + if (isSpeaking()) { + tts.stop(); + } + if (clearMessageQueue) { + messageQueue.clear(); + } + } + + + /** + * UtteranceProgressListener interface inplementation + */ + + @Override public void onStart(String utteranceId) { + } + + @Override public void onError(String utteranceId) { + appendToLog( + String.format("onError: utteranceId: %1$s", utteranceId)); + // here "Locale.getDefault() is intended" + tts.setLanguage(Locale.getDefault()); + stopSpeaking(true); + giveUpAudioFocus(); + } + + @Override public void onDone(String utteranceId) { + if (utteranceId.equals(SILENCE_UTTERANCE_ID)) return; + MessageQueueItem nextItem = messageQueue.pollFirst(); + appendToLog( + String.format("onDone: utteranceId: %1$s, next: %1$s", utteranceId, nextItem)); + if (nextItem != null) { + tts.playSilentUtterance( + SILENCE_DELAY, TextToSpeech.QUEUE_ADD, SILENCE_UTTERANCE_ID); + speak(nextItem); + } else { + stopSpeaking(true); + giveUpAudioFocus(); + } + } + + + /** + * message queue + */ - // speak - tts.setAudioAttributes(buildAudioAttributes()); - if (tryToGetAudioFocus()) { - for (String chunk : Splitter.fixedLength(tts.getMaxSpeechInputLength()).splitToList(message)) { - tts.speak(chunk, TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID_SPEAK); + private synchronized void cleanUpMessageQueue(final MessageType typeToRemoveFromQueue, final int numberOfMessagesToKeep) { + int counter = 0; + for (Iterator it = messageQueue.descendingIterator(); it.hasNext();) { + MessageQueueItem item = it.next(); + if (item.type == typeToRemoveFromQueue) { + String action = ""; + if (counter < numberOfMessagesToKeep) { + counter++; + action = "skip queue item"; + } else { + it.remove(); + action = "remove from queue"; } + appendToLog( + String.format( + Locale.ROOT, "%1$s: %2$s; for type: %3$s and counter: %4$d / keep: %5$d", + action, item.toString(), typeToRemoveFromQueue, counter, numberOfMessagesToKeep)); } } } - public void stop() { - if (isInitialized() && isSpeaking()) { - tts.stop(); - giveUpAudioFocus(); + private synchronized String printMessageQueue() { + String output = String.valueOf(messageQueue.size()); + if (! messageQueue.isEmpty()) { + for (MessageQueueItem item : messageQueue) { + output += String.format("\n %1$s", item.toString()); + } } + return output; } - private AudioAttributes buildAudioAttributes() { - return new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .setUsage( - isScreenReaderEnabled() - ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY - : AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) - .build(); + + public class MessageQueueItem { + public final String message; + public final MessageType type; + public final long timestamp; + public final String utteranceId; + + public MessageQueueItem(String message, MessageType type) { + this.message = message; + this.type = type; + this.timestamp = System.currentTimeMillis(); + this.utteranceId = String.format( + Locale.ROOT, "%1$s.%2$d", this.type.name(), this.timestamp); + } + + @Override public String toString() { + return String.format( + Locale.ROOT, "%1$s.%2$d: %3$s", this.type, this.timestamp % 10000, this.message); + } + + @Override public int hashCode() { + return this.utteranceId.hashCode(); + } + + @Override public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (! (obj instanceof MessageQueueItem)) { + return false; + } + MessageQueueItem other = (MessageQueueItem) obj; + return this.utteranceId.equals(other.utteranceId); + } } @@ -151,22 +373,83 @@ private AudioFocusRequest buildAudioFocusRequest() { .build(); } + private AudioAttributes buildAudioAttributes() { + return new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .setUsage( + isScreenReaderEnabled() + ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY + : AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) + .build(); + } + /** - * UtteranceProgressListener interface inplementation + * logs and test */ - @Override public void onStart(String utteranceId) { + private void appendToLog(String message) { + //Helper.appendToLog("tts.log", message); } - @Override public void onError(String utteranceId) { - tts.setLanguage(Locale.getDefault()); - giveUpAudioFocus(); + private void startTestMessages() { + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override public void run() { + testAnnouncements1(); + } + }, 2000); } - @Override public void onDone(String utteranceId) { - Timber.d("onDone"); - giveUpAudioFocus(); + private void testAnnouncements1() { + // start announcing + announce("In 70 metern, links auf 90°", MessageType.DISTANCE_OR_BEARING); + sleep(500); + // interrupt + announce("In 60 metern, links auf 90°", MessageType.DISTANCE_OR_BEARING); + sleep(500); + // interrupt + announce("Test Instruktion 1: dort hinten links abbiegen.", MessageType.INSTRUCTION); + sleep(500); + // queue + announce("In 50 metern, leicht links auf 90°", MessageType.DISTANCE_OR_BEARING); + sleep(500); + // kill that one above and queue + announce("Test Instruktion 2: Nun geradeaus überqueren.", MessageType.INSTRUCTION); + sleep(500); + // queue + announce("Bäckerei, 30 meter, geradeaus", MessageType.TRACKED_OBJECT_MODE_DISTANCE); + sleep(500); + // queue but prioritise so that it is spoken before the message above + announce("In 30 metern, leicht links auf 90°", MessageType.DISTANCE_OR_BEARING); + sleep(500); + // do it again and replace 30m message above + announce("In 25 metern, leicht links auf 90°", MessageType.DISTANCE_OR_BEARING); + } + + private void testAnnouncements2() { + // announce + announce("In 102 metern, leicht links auf 90°", MessageType.DISTANCE_OR_BEARING); + sleep(100); + // queue + announce("Fleischerei, 43 meter, links", MessageType.TRACKED_OBJECT_MODE_DISTANCE); + sleep(100); + // queue + announce("Blumenladen, 49 meter, rechts", MessageType.TRACKED_OBJECT_MODE_DISTANCE); + sleep(100); + // queue but cleanup queue before so that the queue still only contains two message of type distance tracking mode + announce("Bäckerei, 51 meter, rechts", MessageType.TRACKED_OBJECT_MODE_DISTANCE); + sleep(5000); + // queue is empty by no + announce("Döner, 66 meter, leicht links", MessageType.TRACKED_OBJECT_MODE_DISTANCE); + sleep(800); + // interrupt and speak message above afterwards again + announce("Test Instruktion 3: dort hinten links abbiegen.", MessageType.INSTRUCTION); + } + + private void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) {} } } diff --git a/app/src/main/java/org/walkersguide/android/tts/TtsSettings.java b/app/src/main/java/org/walkersguide/android/tts/TtsSettings.java index dde7d12..94726e0 100644 --- a/app/src/main/java/org/walkersguide/android/tts/TtsSettings.java +++ b/app/src/main/java/org/walkersguide/android/tts/TtsSettings.java @@ -9,16 +9,18 @@ public class TtsSettings implements Serializable { private static final long serialVersionUID = 1l; public static TtsSettings getDefault() { - return new TtsSettings(true, 30); + return new TtsSettings(true, 30, 1.0f); } private boolean announcementsEnabled; private int distanceAnnouncementInterval; + private float speechRate; - public TtsSettings(boolean announcementsEnabled, int distanceAnnouncementInterval) { + public TtsSettings(boolean announcementsEnabled, int distanceAnnouncementInterval, float speechRate) { this.announcementsEnabled = announcementsEnabled; this.distanceAnnouncementInterval = distanceAnnouncementInterval; + this.speechRate = speechRate; } public boolean getAnnouncementsEnabled() { @@ -33,10 +35,24 @@ public int getDistanceAnnouncementInterval() { return this.distanceAnnouncementInterval; } - public void setDistanceAnnouncementInterval(int newInterval) { + public boolean setDistanceAnnouncementInterval(int newInterval) { if (newInterval > 0) { this.distanceAnnouncementInterval = newInterval; + return true; } + return false; + } + + public float getSpeechRate() { + return this.speechRate; + } + + public boolean setSpeechRate(float newSpeechRate) { + if (newSpeechRate > 0) { + this.speechRate = newSpeechRate; + return true; + } + return false; } } diff --git a/app/src/main/java/org/walkersguide/android/ui/UiHelper.java b/app/src/main/java/org/walkersguide/android/ui/UiHelper.java index efab8ac..471975f 100644 --- a/app/src/main/java/org/walkersguide/android/ui/UiHelper.java +++ b/app/src/main/java/org/walkersguide/android/ui/UiHelper.java @@ -12,7 +12,6 @@ import org.walkersguide.android.R; import org.walkersguide.android.util.GlobalInstance; -import timber.log.Timber; import androidx.fragment.app.DialogFragment; import android.view.Window; import androidx.core.view.WindowInsetsControllerCompat; @@ -20,9 +19,6 @@ import androidx.fragment.app.Fragment; import android.app.Dialog; import android.app.Activity; -import androidx.fragment.app.FragmentActivity; -import java.util.List; -import org.walkersguide.android.ui.dialog.toolbar.BearingDetailsDialog; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.view.View; @@ -33,9 +29,7 @@ import android.os.Build; import android.text.Html; import android.text.style.URLSpan; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabItem; -import android.widget.Spinner; +import android.view.accessibility.AccessibilityEvent; public class UiHelper { @@ -46,9 +40,21 @@ public static int convertDpToPx(int dp) { /** - * keyboard + * TextView and EditText */ + public static View.AccessibilityDelegate getAccessibilityDelegateToMuteContentChangedEventsWhileFocussed() { + return new View.AccessibilityDelegate() { + @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + if (host.isAccessibilityFocused() + && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + return; + } + super.onInitializeAccessibilityEvent(host, event); + } + }; + } + public static boolean isDoSomeThingEditorAction(int givenActionId, int wantedActionId, KeyEvent event) { if (givenActionId == wantedActionId) { return true; @@ -60,6 +66,11 @@ public static boolean isDoSomeThingEditorAction(int givenActionId, int wantedAct } } + + /** + * hide keyboard + */ + public static void hideKeyboard(Activity activity) { if (activity != null) { hideKeyboard(activity.getWindow()); diff --git a/app/src/main/java/org/walkersguide/android/ui/activity/MainActivity.java b/app/src/main/java/org/walkersguide/android/ui/activity/MainActivity.java index 6b702e8..3e53172 100644 --- a/app/src/main/java/org/walkersguide/android/ui/activity/MainActivity.java +++ b/app/src/main/java/org/walkersguide/android/ui/activity/MainActivity.java @@ -1,5 +1,8 @@ package org.walkersguide.android.ui.activity; +import org.walkersguide.android.server.ServerTaskExecutor; +import org.walkersguide.android.sensor.bearing.AcceptNewBearing; +import java.util.Locale; import org.walkersguide.android.ui.dialog.WhereAmIDialog; import org.walkersguide.android.ui.dialog.create.EnterAddressDialog; import org.walkersguide.android.BuildConfig; @@ -106,6 +109,12 @@ import java.util.ArrayList; import android.hardware.Sensor; import android.widget.Toast; +import android.view.accessibility.AccessibilityEvent; +import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; +import org.walkersguide.android.server.address.ResolveCoordinatesTask; +import org.walkersguide.android.ui.dialog.toolbar.LocationSensorDetailsDialog; +import org.walkersguide.android.server.address.AddressException; public class MainActivity extends AppCompatActivity @@ -174,7 +183,7 @@ public static void startRouteRecording(Context context) { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Timber.d("onCreate"); + Timber.d("onCreate, locale: %1$s / %2$s", Locale.getDefault(), Helper.getAppLocale()); setContentView(R.layout.activity_main); registerFlingGestureDetector(); AccessDatabase.getInstance(); // important for app-started-for-the-first-time detection @@ -232,6 +241,7 @@ public void onClick(View view) { }); buttonBearingDetails = (ImageButton) findViewById(R.id.buttonBearingDetails); + buttonBearingDetails.setAccessibilityDelegate(new BearingDetailsTtsAccessibilityDelegate()); buttonBearingDetails.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { BearingDetailsDialog.newInstance() @@ -240,6 +250,7 @@ public void onClick(View view) { }); buttonLocationDetails = (ImageButton) findViewById(R.id.buttonLocationDetails); + buttonLocationDetails.setAccessibilityDelegate(new LocationDetailsTtsAccessibilityDelegate()); buttonLocationDetails.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { LocationDetailsDialog.newInstance() @@ -247,6 +258,28 @@ public void onClick(View view) { } }); + // these accessibility actions for the buttonLocationDetails are always there + // for the rest see updateAccessibilityActionsOnLocationDetailsButton() + ViewCompat.addAccessibilityAction( + this.buttonLocationDetails, + GlobalInstance.getStringResource(R.string.labelPrefixClosestAddress), + (actionView, arguments) -> { + Helper.vibrateOnce( + Helper.VIBRATION_DURATION_SHORT, Helper.VIBRATION_INTENSITY_WEAK); + resolveCurrentCoordinatesForTalkbackAction.execute(); + return true; + }); + ViewCompat.addAccessibilityAction( + this.buttonLocationDetails, + GlobalInstance.getStringResource(R.string.labelPointGPSDetailsHeading), + (actionView, arguments) -> { + Helper.vibrateOnce( + Helper.VIBRATION_DURATION_SHORT, Helper.VIBRATION_INTENSITY_WEAK); + LocationSensorDetailsDialog.newInstance() + .show(getSupportFragmentManager(), "LocationSensorDetailsDialog"); + return true; + }); + // navigation drawer drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout); navigationView = (NavigationView) findViewById(R.id.navigationView); @@ -258,9 +291,6 @@ public void onClick(View view) { } else if (menuItem.getItemId() == R.id.menuItemSaveCurrentLocation) { SaveCurrentLocationDialog.addToDatabaseProfile() .show(getSupportFragmentManager(), "SaveCurrentLocationDialog"); - } else if (menuItem.getItemId() == R.id.menuItemOpenWhereAmIDialog) { - WhereAmIDialog.newInstance() - .show(getSupportFragmentManager(), "WhereAmIDialog"); } else if (menuItem.getItemId() == R.id.menuItemCollections) { CollectionListFragment.newInstance() .show(getSupportFragmentManager(), "CollectionListFragment"); @@ -398,7 +428,7 @@ private void openTab(Intent intent) { Uri uri = intent.getData(); if (uri != null && ! uri.equals(lastUri)) { - Timber.d("uri from intent filter: %1$s", uri.toString()); + Timber.d("uri from intent filter: %1$s; last uri: %2$s", uri.toString(), lastUri != null ? lastUri.toString() : ""); if ("geo".equals(uri.getScheme())) { String[] parts = uri.toString().split("\\?q="); if (parts.length > 1 @@ -482,7 +512,9 @@ private void updateToolbarNavigateUpButtonVisibility() { ? View.VISIBLE : View.GONE); } - private void updateBearingDetailsButton() { + // bearing details + + private String createBearingDetailsButtonContentDescription() { Bearing currentBearing = deviceSensorManagerInstance.getCurrentBearing(); StringBuilder bearingDescriptionBuilder = new StringBuilder(); @@ -514,7 +546,7 @@ private void updateBearingDetailsButton() { } } - buttonBearingDetails.setContentDescription(bearingDescriptionBuilder.toString()); + return bearingDescriptionBuilder.toString(); } private void updateAccessibilityActionsOnBearingDetailsButton() { @@ -526,7 +558,8 @@ private void updateAccessibilityActionsOnBearingDetailsButton() { } bearingSourceAccessibilityActionIdList.clear(); - if (! settingsManagerInstance.getAutoSwitchBearingSourceEnabled()) { + if (! settingsManagerInstance.getAutoSwitchBearingSourceEnabled() + && ! deviceSensorManagerInstance.getSimulationEnabled()) { for (final BearingSensor sensor : BearingSensor.values()) { if (sensor.equals(settingsManagerInstance.getSelectedBearingSensor())) { continue; @@ -546,8 +579,6 @@ private void updateAccessibilityActionsOnBearingDetailsButton() { getResources().getString(R.string.errorNoBearingFound), Toast.LENGTH_LONG).show(); } else { - ViewCompat.setAccessibilityLiveRegion( - buttonBearingDetails, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); Helper.vibrateOnce( Helper.VIBRATION_DURATION_SHORT, Helper.VIBRATION_INTENSITY_WEAK); deviceSensorManagerInstance.setSelectedBearingSensor(sensor); @@ -580,8 +611,6 @@ private void updateAccessibilityActionsOnBearingDetailsButton() { simulatedBearing.toString()), (actionView, arguments) -> { - ViewCompat.setAccessibilityLiveRegion( - buttonBearingDetails, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); Helper.vibrateOnce( Helper.VIBRATION_DURATION_SHORT, Helper.VIBRATION_INTENSITY_WEAK); deviceSensorManagerInstance.setSimulationEnabled( @@ -591,7 +620,40 @@ private void updateAccessibilityActionsOnBearingDetailsButton() { } } - private void updateLocationDetailsButton() { + private class BearingDetailsTtsAccessibilityDelegate extends View.AccessibilityDelegate { + private AcceptNewBearing acceptNewBearing = new AcceptNewBearing(2, 1000l); + private BearingSensor lastBearingSensor = DeviceSensorManager.getInstance().getSelectedBearingSensor(); + private boolean lastSimulationEnabled = DeviceSensorManager.getInstance().getSimulationEnabled(); + + @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + if (host.isAccessibilityFocused() + && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + + BearingSensor currentBearingSensor = DeviceSensorManager.getInstance().getSelectedBearingSensor(); + boolean currentSimulationEnabled = DeviceSensorManager.getInstance().getSimulationEnabled(); + Bearing currentBearing = DeviceSensorManager.getInstance().getCurrentBearing(); + + if (currentBearingSensor != lastBearingSensor + || lastSimulationEnabled != currentSimulationEnabled) { + TTSWrapper.getInstance().announce( + createBearingDetailsButtonContentDescription(), MessageType.TOP_PRIORITY); + lastBearingSensor = currentBearingSensor; + lastSimulationEnabled = currentSimulationEnabled; + } else if (acceptNewBearing.updateBearing(currentBearing, false, false)) { + TTSWrapper.getInstance().announce( + currentBearing.toString(), MessageType.DISTANCE_OR_BEARING); + } + + return; + } + super.onInitializeAccessibilityEvent(host, event); + } + }; + + // location details + private ResolveCurrentCoordinatesForTalkbackAction resolveCurrentCoordinatesForTalkbackAction = new ResolveCurrentCoordinatesForTalkbackAction(); + + private String createLocationDetailsButtonContentDescription() { Point currentLocation = positionManagerInstance.getCurrentLocation(); StringBuilder locationDescriptionBuilder = new StringBuilder(); @@ -621,7 +683,7 @@ private void updateLocationDetailsButton() { currentLocation.getName()); } - buttonLocationDetails.setContentDescription(locationDescriptionBuilder.toString()); + return locationDescriptionBuilder.toString(); } private void updateAccessibilityActionsOnLocationDetailsButton() { @@ -634,7 +696,6 @@ private void updateAccessibilityActionsOnLocationDetailsButton() { // and create action to control simulation status Point simulatedPoint = positionManagerInstance.getSimulatedLocation(); if (simulatedPoint != null) { - Timber.d("updateAccessibilityActionsOnLocationDetailsButton: set location action, sim enabled: %1$s", positionManagerInstance.getSimulationEnabled()); controlSimulationAccessibilityActionIdForLocation = ViewCompat.addAccessibilityAction( this.buttonLocationDetails, String.format( @@ -644,8 +705,6 @@ private void updateAccessibilityActionsOnLocationDetailsButton() { simulatedPoint.getName()), (actionView, arguments) -> { - ViewCompat.setAccessibilityLiveRegion( - buttonLocationDetails, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); Helper.vibrateOnce( Helper.VIBRATION_DURATION_SHORT, Helper.VIBRATION_INTENSITY_WEAK); positionManagerInstance.setSimulationEnabled( @@ -656,6 +715,78 @@ private void updateAccessibilityActionsOnLocationDetailsButton() { } + private class ResolveCurrentCoordinatesForTalkbackAction { + private ServerTaskExecutor serverTaskExecutorInstance; + private long taskId; + + public ResolveCurrentCoordinatesForTalkbackAction() { + taskId = ServerTaskExecutor.NO_TASK_ID; + serverTaskExecutorInstance = ServerTaskExecutor.getInstance(); + } + + public void execute() { + if (serverTaskExecutorInstance.taskInProgress(taskId)) return; + Point currentLocation = PositionManager.getInstance().getCurrentLocation(); + if (currentLocation == null) return; + + IntentFilter localIntentFilter = new IntentFilter(); + localIntentFilter.addAction(ServerTaskExecutor.ACTION_RESOLVE_COORDINATES_TASK_SUCCESSFUL); + localIntentFilter.addAction(ServerTaskExecutor.ACTION_SERVER_TASK_CANCELLED); + localIntentFilter.addAction(ServerTaskExecutor.ACTION_SERVER_TASK_FAILED); + LocalBroadcastManager.getInstance(GlobalInstance.getContext()).registerReceiver(localIntentReceiver, localIntentFilter); + + taskId = serverTaskExecutorInstance.executeTask( + new ResolveCoordinatesTask(currentLocation)); + } + + private BroadcastReceiver localIntentReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + if (taskId != intent.getLongExtra(ServerTaskExecutor.EXTRA_TASK_ID, ServerTaskExecutor.INVALID_TASK_ID)) return; + + if (intent.getAction().equals(ServerTaskExecutor.ACTION_RESOLVE_COORDINATES_TASK_SUCCESSFUL)) { + StreetAddress addressPoint = (StreetAddress) intent.getSerializableExtra(ServerTaskExecutor.EXTRA_STREET_ADDRESS); + if (addressPoint != null) { + TTSWrapper.getInstance().screenReader(addressPoint.toString()); + } + } else if (intent.getAction().equals(ServerTaskExecutor.ACTION_SERVER_TASK_CANCELLED)) { + } else if (intent.getAction().equals(ServerTaskExecutor.ACTION_SERVER_TASK_FAILED)) { + AddressException addressException = (AddressException) intent.getSerializableExtra(ServerTaskExecutor.EXTRA_EXCEPTION); + if (addressException != null) { + TTSWrapper.getInstance().screenReader(addressException.getMessage()); + } + } + + LocalBroadcastManager.getInstance(GlobalInstance.getContext()).unregisterReceiver(localIntentReceiver); + taskId = ServerTaskExecutor.NO_TASK_ID; + } + }; + } + + + private class LocationDetailsTtsAccessibilityDelegate extends View.AccessibilityDelegate { + private boolean lastSimulationEnabled = PositionManager.getInstance().getSimulationEnabled(); + + @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + if (host.isAccessibilityFocused() + && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + + boolean currentSimulationEnabled = PositionManager.getInstance().getSimulationEnabled(); + if (lastSimulationEnabled != currentSimulationEnabled) { + TTSWrapper.getInstance().announce( + createLocationDetailsButtonContentDescription(), MessageType.TOP_PRIORITY); + lastSimulationEnabled = currentSimulationEnabled; + } else { + TTSWrapper.getInstance().announce( + createLocationDetailsButtonContentDescription(), MessageType.DISTANCE_OR_BEARING); + } + + return; + } + super.onInitializeAccessibilityEvent(host, event); + } + }; + + /** * pause and resume */ @@ -676,10 +807,10 @@ private void updateAccessibilityActionsOnLocationDetailsButton() { registerBroadcastReceiver(); displayRemainsActiveSettingChanged( settingsManagerInstance.getDisplayRemainsActive()); + buttonBearingDetails.setContentDescription(createBearingDetailsButtonContentDescription()); updateAccessibilityActionsOnBearingDetailsButton(); - updateBearingDetailsButton(); + buttonLocationDetails.setContentDescription(createLocationDetailsButtonContentDescription()); updateAccessibilityActionsOnLocationDetailsButton(); - updateLocationDetailsButton(); WalkersGuideService.requestServiceState(); if (globalInstance.applicationWasInBackground()) { @@ -824,13 +955,9 @@ private void registerBroadcastReceiver() { } else if (drawerLayout != null && ! drawerLayout.isOpen()) { if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING)) { - updateBearingDetailsButton(); - ViewCompat.setAccessibilityLiveRegion( - buttonBearingDetails, ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE); + buttonBearingDetails.setContentDescription(createBearingDetailsButtonContentDescription()); } else if (intent.getAction().equals(PositionManager.ACTION_NEW_LOCATION)) { - updateLocationDetailsButton(); - ViewCompat.setAccessibilityLiveRegion( - buttonLocationDetails, ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE); + buttonLocationDetails.setContentDescription(createLocationDetailsButtonContentDescription()); } } } diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/WhereAmIDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/WhereAmIDialog.java index caa44f0..5b2f0e0 100644 --- a/app/src/main/java/org/walkersguide/android/ui/dialog/WhereAmIDialog.java +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/WhereAmIDialog.java @@ -42,28 +42,17 @@ public class WhereAmIDialog extends DialogFragment implements OnCurrentAddressRe // instance constructors public static WhereAmIDialog newInstance() { - return WhereAmIDialog.newInstance(false); - } - - public static WhereAmIDialog newInstance(boolean onlyResolveAddressAndCloseDialogImmediately) { WhereAmIDialog dialog = new WhereAmIDialog(); - Bundle args = new Bundle(); - args.putBoolean(KEY_ONLY_RESOLVE_ADDRESS, onlyResolveAddressAndCloseDialogImmediately); - dialog.setArguments(args); return dialog; } // dialog private static final String KEY_TASK_ID = "taskId"; - private static final String KEY_ONLY_RESOLVE_ADDRESS = "onlyResolveAddressAndCloseDialogImmediately"; - private boolean onlyResolveAddressAndCloseDialogImmediately; private ResolveCurrentAddressView layoutClosestAddress; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - onlyResolveAddressAndCloseDialogImmediately = getArguments().getBoolean(KEY_ONLY_RESOLVE_ADDRESS); - layoutClosestAddress = new ResolveCurrentAddressView(WhereAmIDialog.this.getContext()); layoutClosestAddress.setLayoutParams( new LinearLayout.LayoutParams( @@ -75,27 +64,21 @@ public static WhereAmIDialog newInstance(boolean onlyResolveAddressAndCloseDialo : ServerTaskExecutor.NO_TASK_ID); // create dialog - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()) + return new AlertDialog.Builder(getActivity()) .setView(layoutClosestAddress) .setNegativeButton( - onlyResolveAddressAndCloseDialogImmediately - ? getResources().getString(R.string.dialogCancel) - : getResources().getString(R.string.dialogClose), + getResources().getString(R.string.dialogClose), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } - }); - - if (! onlyResolveAddressAndCloseDialogImmediately) { - dialogBuilder.setNeutralButton( - getResources().getString(R.string.dialogRefresh), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - } - }); - } - - return dialogBuilder.create(); + }) + .setNeutralButton( + getResources().getString(R.string.dialogRefresh), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .create(); } @Override public void onStart() { @@ -138,15 +121,7 @@ public void onClick(DialogInterface dialog, int which) { } @Override public void onCurrentAddressResolved(StreetAddress addressPoint) { - if (onlyResolveAddressAndCloseDialogImmediately) { - Bundle result = new Bundle(); - result.putSerializable(EXTRA_STREET_ADDRESS, addressPoint); - getParentFragmentManager().setFragmentResult(REQUEST_RESOLVE_COORDINATES, result); - dismiss(); - } else { - // announce - TTSWrapper.getInstance().screenReader(addressPoint.toString()); - } + TTSWrapper.getInstance().screenReader(addressPoint.toString()); } @Override public void onDestroy() { diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/create/ImportGpxFileDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/create/ImportGpxFileDialog.java index f365f28..d0c16f5 100644 --- a/app/src/main/java/org/walkersguide/android/ui/dialog/create/ImportGpxFileDialog.java +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/create/ImportGpxFileDialog.java @@ -388,10 +388,10 @@ private void execute() { Toast.LENGTH_LONG) .show(); + dismiss(); Bundle result = new Bundle(); result.putSerializable(EXTRA_GPX_FILE_PROFILE, profileToAddObjectsTo); getParentFragmentManager().setFragmentResult(REQUEST_IMPORT_OF_GPX_FILE_WAS_SUCCESSFUL, result); - dismiss(); } diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/edit/SpeechRateDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/edit/SpeechRateDialog.java new file mode 100644 index 0000000..2e8658a --- /dev/null +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/edit/SpeechRateDialog.java @@ -0,0 +1,182 @@ +package org.walkersguide.android.ui.dialog.edit; + +import org.walkersguide.android.tts.TTSWrapper; +import android.os.Handler; +import android.os.Looper; +import android.view.accessibility.AccessibilityEvent; + +import androidx.appcompat.app.AlertDialog; +import android.app.Dialog; + +import android.content.DialogInterface; + +import android.os.Bundle; + +import androidx.fragment.app.DialogFragment; + +import android.view.View; + + +import org.walkersguide.android.R; + +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.view.LayoutInflater; +import java.util.Locale; +import android.widget.Button; +import android.widget.ImageButton; + + +public class SpeechRateDialog extends DialogFragment { + public static final String REQUEST_CHANGE_SPEECH_RATE = "changeSpeechRate"; + public static final String EXTRA_SPEECH_RATE = "speechRate"; + + // instance constructors + + public static SpeechRateDialog newInstance(float selectedSpeechRate) { + SpeechRateDialog dialog = new SpeechRateDialog(); + Bundle args = new Bundle(); + args.putSerializable(KEY_SELECTED_SPEECH_RATE, selectedSpeechRate); + dialog.setArguments(args); + return dialog; + } + + + // dialog + private static final float[] SPEECH_RATE_PRESETS = new float[]{ 0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f }; + private static final float SPEECH_RATE_INTERVAL = 0.05f; + private static final String KEY_SELECTED_SPEECH_RATE = "selectedSpeechRate"; + + private float selectedSpeechRate; + private TextView labelSpeechRate; + + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + selectedSpeechRate = savedInstanceState != null + ? savedInstanceState.getFloat(KEY_SELECTED_SPEECH_RATE) + : getArguments().getFloat(KEY_SELECTED_SPEECH_RATE); + + // custom view + final ViewGroup nullParent = null; + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_speech_rate, nullParent); + + ImageButton buttonDecreaseSpeechRate = (ImageButton) view.findViewById(R.id.buttonDecreaseSpeechRate); + buttonDecreaseSpeechRate.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + if (selectedSpeechRate - SPEECH_RATE_INTERVAL > 0.01f) { + selectedSpeechRate -= SPEECH_RATE_INTERVAL; + updateSpeechRateLabel(); + } + } + }); + + labelSpeechRate = (TextView) view.findViewById(R.id.labelSpeechRate); + updateSpeechRateLabel(); + + ImageButton buttonIncreaseSpeechRate = (ImageButton) view.findViewById(R.id.buttonIncreaseSpeechRate); + buttonIncreaseSpeechRate.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + selectedSpeechRate += SPEECH_RATE_INTERVAL; + updateSpeechRateLabel(); + } + }); + + LinearLayout layoutSpeechRatePresets = (LinearLayout) view.findViewById(R.id.layoutSpeechRatePresets); + layoutSpeechRatePresets.removeAllViews(); + + for (final float speechRatePreset : SPEECH_RATE_PRESETS) { + Button buttonSpeechRatePreset = new Button(getActivity()); + buttonSpeechRatePreset.setText( + String.format( + Locale.getDefault(), "%1$.2fx", speechRatePreset)); + + // Create LayoutParams with width=0dp, height=wrap_content, weight=1.0 + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + 0, // width: 0dp + ViewGroup.LayoutParams.WRAP_CONTENT, // height: wrap_content + 1.0f); // weight: 1.0 + buttonSpeechRatePreset.setLayoutParams(params); + + // listener + buttonSpeechRatePreset.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + selectedSpeechRate = speechRatePreset; + updateSpeechRateLabel(); + } + }); + + // Add the Button to the LinearLayout + layoutSpeechRatePresets.addView(buttonSpeechRatePreset); + } + + return new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.buttonSpeechRate)) + .setView(view) + .setPositiveButton( + getResources().getString(R.string.dialogOK), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .setNeutralButton( + getResources().getString(R.string.dialogTest), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .setNegativeButton( + getResources().getString(R.string.dialogCancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .create(); + } + + @Override public void onStart() { + super.onStart(); + AlertDialog dialog = (AlertDialog) getDialog(); + if (dialog != null) { + + // positive button + Button buttonPositive = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + buttonPositive.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + dismiss(); + Bundle result = new Bundle(); + result.putSerializable(EXTRA_SPEECH_RATE, selectedSpeechRate); + getParentFragmentManager().setFragmentResult(REQUEST_CHANGE_SPEECH_RATE, result); + } + }); + + // neutral button + Button buttonNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + buttonNeutral.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + TTSWrapper.getInstance().testInstruction(selectedSpeechRate); + } + }); + + // negative button + Button buttonNegative = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); + buttonNegative.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View view) { + dismiss(); + } + }); + } + } + + @Override public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + savedInstanceState.putFloat(KEY_SELECTED_SPEECH_RATE, selectedSpeechRate); + } + + private void updateSpeechRateLabel() { + labelSpeechRate.setText( + String.format( + Locale.getDefault(), "%1$.2fx", selectedSpeechRate)); + } + +} diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectObjectWithIdFromMultipleSourcesDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectObjectWithIdFromMultipleSourcesDialog.java index e162afc..5b0d53e 100644 --- a/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectObjectWithIdFromMultipleSourcesDialog.java +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectObjectWithIdFromMultipleSourcesDialog.java @@ -42,7 +42,6 @@ import androidx.annotation.NonNull; import androidx.fragment.app.FragmentResultListener; import org.walkersguide.android.ui.fragment.ObjectListFragment; -import org.walkersguide.android.ui.dialog.WhereAmIDialog; import org.walkersguide.android.ui.dialog.SimpleMessageDialog; import timber.log.Timber; import org.walkersguide.android.database.SortMethod; @@ -84,9 +83,6 @@ public enum Target { getChildFragmentManager() .setFragmentResultListener( SaveCurrentLocationDialog.REQUEST_SAVE_CURRENT_LOCATION, this, this); - getChildFragmentManager() - .setFragmentResultListener( - WhereAmIDialog.REQUEST_RESOLVE_COORDINATES, this, this); getChildFragmentManager() .setFragmentResultListener( EnterAddressDialog.REQUEST_ENTER_ADDRESS, this, this); @@ -110,10 +106,6 @@ public enum Target { objectWithIdSelected( (GPS) bundle.getSerializable(SaveCurrentLocationDialog.EXTRA_CURRENT_LOCATION)); - } else if (requestKey.equals(WhereAmIDialog.REQUEST_RESOLVE_COORDINATES)) { - objectWithIdSelected( - (StreetAddress) bundle.getSerializable(WhereAmIDialog.EXTRA_STREET_ADDRESS)); - } else if (requestKey.equals(EnterAddressDialog.REQUEST_ENTER_ADDRESS)) { objectWithIdSelected( (StreetAddress) bundle.getSerializable(EnterAddressDialog.EXTRA_STREET_ADDRESS)); @@ -214,7 +206,7 @@ public void onClick(DialogInterface dialog, int which) { ArrayList sourceActionList = new ArrayList( Arrays.asList(SourceAction.values())); - // remove actions "CURRENT_LOCATION" and "CLOSEST_ADDRESS" + // remove action "CURRENT_LOCATION" switch (target) { case ROUTE_VIA_POINT_1: case ROUTE_VIA_POINT_2: @@ -222,7 +214,6 @@ public void onClick(DialogInterface dialog, int which) { case ROUTE_DESTINATION_POINT: case SIMULATE_LOCATION: sourceActionList.remove(SourceAction.CURRENT_LOCATION); - sourceActionList.remove(SourceAction.CLOSEST_ADDRESS); break; } @@ -267,11 +258,10 @@ public void onClick(DialogInterface dialog, int which) { private enum SourceAction { CURRENT_LOCATION(GlobalInstance.getStringResource(R.string.pointSelectFromCurrentLocation)), - CLOSEST_ADDRESS(GlobalInstance.getStringResource(R.string.pointSelectFromClosestAddress)), - HOME_ADDRESS(GlobalInstance.getStringResource(R.string.pointSelectFromHomeAddress)), ENTER_ADDRESS(GlobalInstance.getStringResource(R.string.pointSelectFromEnterAddress)), - COLLECTIONS(GlobalInstance.getStringResource(R.string.pointSelectFromCollections)), POI(GlobalInstance.getStringResource(R.string.pointSelectFromPOI)), + HOME_ADDRESS(GlobalInstance.getStringResource(R.string.pointSelectFromHomeAddress)), + COLLECTIONS(GlobalInstance.getStringResource(R.string.pointSelectFromCollections)), HISTORY(GlobalInstance.getStringResource(R.string.pointSelectFromHistory)), FROM_COORDINATES_LINK(GlobalInstance.getStringResource(R.string.pointSelectFromCoordinatesLink)), ENTER_COORDINATES(GlobalInstance.getStringResource(R.string.pointSelectFromEnterCoordinates)); @@ -326,11 +316,6 @@ private void executeAction(SourceAction action) { } break; - case CLOSEST_ADDRESS: - WhereAmIDialog.newInstance(true) - .show(getChildFragmentManager(), "WhereAmIDialog"); - break; - case HOME_ADDRESS: Point homeAddress = SettingsManager.getInstance().getHomeAddress(); if (homeAddress == null) { diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectProfileFromMultipleSourcesDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectProfileFromMultipleSourcesDialog.java index d474300..f044b74 100644 --- a/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectProfileFromMultipleSourcesDialog.java +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/select/SelectProfileFromMultipleSourcesDialog.java @@ -308,7 +308,7 @@ private void profileSelected(Profile profile) { public static class CreateEmptyCollectionDialog extends EnterStringDialog { public static final String REQUEST_CREATE_EMPTY_COLLECTION_WAS_SUCCESSFUL = "requestCreateEmptyCollectionWasSuccessful"; - public static final String EXTRA_EMPTY_COLLECTION = "emptyCollection"; + public static final String EXTRA_EMPTY_COLLECTION = "emptyCollection"; public static CreateEmptyCollectionDialog newInstance() { diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/BearingDetailsDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/BearingDetailsDialog.java index c11f0ce..4d616b4 100644 --- a/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/BearingDetailsDialog.java +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/BearingDetailsDialog.java @@ -1,5 +1,7 @@ package org.walkersguide.android.ui.dialog.toolbar; +import org.walkersguide.android.sensor.bearing.AcceptNewBearing; +import android.view.accessibility.AccessibilityEvent; import android.widget.Toast; import org.walkersguide.android.sensor.bearing.BearingSensor; import org.walkersguide.android.data.angle.Bearing; @@ -9,6 +11,7 @@ import android.text.Spanned; import java.lang.NumberFormatException; import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; import org.walkersguide.android.ui.interfaces.TextChangedListener; import org.walkersguide.android.ui.UiHelper; @@ -100,9 +103,14 @@ public void onCheckedChanged(CompoundButton view, boolean isChecked) { // compass labelCompassDetails = (TextView) view.findViewById(R.id.labelCompassDetails); radioCompass = (RadioButton) view.findViewById(R.id.radioCompass); + radioCompass.setAccessibilityDelegate( + new TtsAccessibilityDelegate(BearingSensor.COMPASS)); + // gps labelSatelliteDetails = (TextView) view.findViewById(R.id.labelSatelliteDetails); radioSatellite = (RadioButton) view.findViewById(R.id.radioSatellite); + radioSatellite.setAccessibilityDelegate( + new TtsAccessibilityDelegate(BearingSensor.SATELLITE)); // simulated direction @@ -304,6 +312,40 @@ private void checkBearingSensorRadioButtons() { } + private class TtsAccessibilityDelegate extends View.AccessibilityDelegate { + private AcceptNewBearing acceptNewBearing = new AcceptNewBearing(2, 1000l); + private BearingSensor sensor; + + public TtsAccessibilityDelegate(BearingSensor sensor) { + super(); + this.sensor = sensor; + } + + @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + if (host.isAccessibilityFocused() + && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + + BearingSensorValue bearingSensorValue; + if (this.sensor ==BearingSensor.COMPASS) { + bearingSensorValue = DeviceSensorManager.getInstance().getBearingValueFromCompass(); + } else if (this.sensor ==BearingSensor.SATELLITE) { + bearingSensorValue = DeviceSensorManager.getInstance().getBearingValueFromSatellite(); + } else { + return; + } + + if (acceptNewBearing.updateBearing(bearingSensorValue, false, false)) { + TTSWrapper.getInstance().announce( + bearingSensorValue.toString(), MessageType.DISTANCE_OR_BEARING); + } + + return; + } + super.onInitializeAccessibilityEvent(host, event); + } + } + + private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(DeviceSensorManager.ACTION_BEARING_SENSOR_CHANGED)) { diff --git a/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/LocationDetailsDialog.java b/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/LocationDetailsDialog.java index 49ed5aa..932dd07 100644 --- a/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/LocationDetailsDialog.java +++ b/app/src/main/java/org/walkersguide/android/ui/dialog/toolbar/LocationDetailsDialog.java @@ -30,7 +30,6 @@ -import org.walkersguide.android.database.util.AccessDatabase; import org.walkersguide.android.data.object_with_id.point.GPS; import org.walkersguide.android.R; import org.walkersguide.android.sensor.PositionManager; @@ -48,7 +47,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; -import timber.log.Timber; import org.walkersguide.android.data.ObjectWithId; @@ -121,7 +119,7 @@ public static LocationDetailsDialog newInstance() { public void onCheckedChanged(CompoundButton view, boolean isChecked) { if (positionManagerInstance.getSimulationEnabled() != isChecked) { // check or uncheck simulation - if (isChecked && positionManagerInstance.getCurrentLocation() == null) { + if (isChecked && positionManagerInstance.getSimulatedLocation() == null) { // no simulated point selected Toast.makeText( getActivity(), diff --git a/app/src/main/java/org/walkersguide/android/ui/fragment/SettingsFragment.java b/app/src/main/java/org/walkersguide/android/ui/fragment/SettingsFragment.java index 1210af3..433cb0e 100644 --- a/app/src/main/java/org/walkersguide/android/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/org/walkersguide/android/ui/fragment/SettingsFragment.java @@ -1,13 +1,13 @@ package org.walkersguide.android.ui.fragment; import android.view.accessibility.AccessibilityEvent; -import org.walkersguide.android.tts.TTSWrapper; import org.walkersguide.android.data.object_with_id.point.point_with_address_data.StreetAddress; import java.util.concurrent.Executors; import android.os.Handler; import android.os.Looper; import timber.log.Timber; +import org.walkersguide.android.ui.dialog.edit.SpeechRateDialog; import org.walkersguide.android.ui.dialog.edit.ChangeServerUrlDialog; import org.walkersguide.android.ui.dialog.select.SelectPublicTransportProviderDialog; import org.walkersguide.android.ui.dialog.select.SelectShakeIntensityDialog; @@ -62,7 +62,6 @@ import android.view.ViewGroup; -import androidx.fragment.app.Fragment; import org.walkersguide.android.util.SettingsManager; @@ -86,9 +85,6 @@ import android.view.KeyEvent; import org.walkersguide.android.ui.UiHelper; import android.view.inputmethod.EditorInfo; -import org.walkersguide.android.ui.activity.MainActivity; -import android.app.Activity; -import org.walkersguide.android.ui.activity.MainActivityController; import org.walkersguide.android.ui.dialog.select.SelectObjectWithIdFromMultipleSourcesDialog; import org.walkersguide.android.ui.view.ObjectWithIdView; import org.walkersguide.android.data.object_with_id.Point; @@ -99,7 +95,6 @@ import androidx.fragment.app.DialogFragment; import org.walkersguide.android.sensor.PositionManager; import org.walkersguide.android.ui.dialog.edit.ConfigureWayClassWeightsDialog; -import org.walkersguide.android.server.wg.p2p.WayClassWeightSettings; import org.walkersguide.android.server.wg.p2p.WayClassWeightSettings.Preset; import org.walkersguide.android.ui.fragment.object_list.extended.ObjectListFromDatabaseFragment; import org.walkersguide.android.database.profile.StaticProfile; @@ -127,6 +122,7 @@ public static SettingsFragment newInstance() { private Button buttonShakeIntensity; private SwitchCompat switchShowActionButton, switchDisplayRemainsActive, switchPreferFusedLocationProviderInsteadOfNetworkProvider; private SwitchCompat switchAnnouncementsEnabled, switchKeepBluetoothHeadsetConnectionAlive; + private Button buttonSpeechRate; private EditText editDistanceAnnouncementInterval; @Override public void onCreate(Bundle savedInstanceState) { @@ -160,6 +156,9 @@ public static SettingsFragment newInstance() { getChildFragmentManager() .setFragmentResultListener( SelectShakeIntensityDialog.REQUEST_SELECT_SHAKE_INTENSITY, this, this); + getChildFragmentManager() + .setFragmentResultListener( + SpeechRateDialog.REQUEST_CHANGE_SPEECH_RATE, this, this); getChildFragmentManager() .setFragmentResultListener( ImportSettingsInBackgroundDialog.REQUEST_IMPORT_OF_SETTINGS_IN_BACKGROUND_WAS_SUCCESSFUL, this, this); @@ -214,6 +213,13 @@ public static SettingsFragment newInstance() { (ShakeIntensity) bundle.getSerializable(SelectShakeIntensityDialog.EXTRA_SHAKE_INTENSITY)); updateUI(); + } else if (requestKey.equals(SpeechRateDialog.REQUEST_CHANGE_SPEECH_RATE)) { + TtsSettings ttsSettings = settingsManagerInstance.getTtsSettings(); + ttsSettings.setSpeechRate( + bundle.getFloat(SpeechRateDialog.EXTRA_SPEECH_RATE)); + settingsManagerInstance.setTtsSettings(ttsSettings); + updateUI(); + } else if (requestKey.equals(ImportSettingsInBackgroundDialog.REQUEST_IMPORT_OF_SETTINGS_IN_BACKGROUND_WAS_SUCCESSFUL)) { settingsImportSuccessful = bundle.getBoolean(ImportSettingsInBackgroundDialog.EXTRA_SETTINGS_IMPORT_SUCCESSFUL); cleanupCache(); @@ -362,6 +368,8 @@ public void onClick(View view) { } }); + // tts + switchAnnouncementsEnabled = (SwitchCompat) view.findViewById(R.id.switchAnnouncementsEnabled); switchAnnouncementsEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton view, boolean isChecked) { @@ -373,6 +381,15 @@ public void onCheckedChanged(CompoundButton view, boolean isChecked) { } }); + buttonSpeechRate = (Button) view.findViewById(R.id.buttonSpeechRate); + buttonSpeechRate.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + SpeechRateDialog.newInstance( + settingsManagerInstance.getTtsSettings().getSpeechRate()) + .show(getChildFragmentManager(), "SpeechRateDialog"); + } + }); + editDistanceAnnouncementInterval = (EditText) view.findViewById(R.id.editDistanceAnnouncementInterval); editDistanceAnnouncementInterval.addTextChangedListener(new TextChangedListener(editDistanceAnnouncementInterval) { @Override public void onTextChanged(EditText view, Editable s) { @@ -522,6 +539,13 @@ private void updateUI() { ); TtsSettings ttsSettings = settingsManagerInstance.getTtsSettings(); switchAnnouncementsEnabled.setChecked(ttsSettings.getAnnouncementsEnabled()); + buttonSpeechRate.setText( + String.format( + Locale.getDefault(), + "%1$s: %2$.2fx", + getResources().getString(R.string.buttonSpeechRate), + ttsSettings.getSpeechRate()) + ); editDistanceAnnouncementInterval.setText( String.valueOf(ttsSettings.getDistanceAnnouncementInterval())); editDistanceAnnouncementInterval.selectAll(); diff --git a/app/src/main/java/org/walkersguide/android/ui/fragment/TabLayoutFragment.java b/app/src/main/java/org/walkersguide/android/ui/fragment/TabLayoutFragment.java index 1350904..10a79be 100644 --- a/app/src/main/java/org/walkersguide/android/ui/fragment/TabLayoutFragment.java +++ b/app/src/main/java/org/walkersguide/android/ui/fragment/TabLayoutFragment.java @@ -104,9 +104,12 @@ public void initializeViewPagerAndTabLayout(AbstractTabAdapter adapter) { } // prepare tab layout + tabLayout.clearOnTabSelectedListeners(); + tabLayout.removeAllTabs(); for (int i=0; i tab) { } return 0; } + + public String getFragmentDescription(int position) { + return getFragmentName(position); + } } } diff --git a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/ObjectDetailsTabLayoutFragment.java b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/ObjectDetailsTabLayoutFragment.java index aa9688e..b0cdda3 100644 --- a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/ObjectDetailsTabLayoutFragment.java +++ b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/ObjectDetailsTabLayoutFragment.java @@ -1,32 +1,19 @@ package org.walkersguide.android.ui.fragment.tabs; +import org.walkersguide.android.ui.view.DistanceAndBearingView; import androidx.core.view.MenuProvider; -import org.walkersguide.android.tts.TTSWrapper; import org.walkersguide.android.database.profile.static_profile.HistoryProfile; -import org.walkersguide.android.sensor.DeviceSensorManager; -import org.walkersguide.android.data.angle.Bearing; -import org.walkersguide.android.sensor.bearing.AcceptNewBearing; -import org.walkersguide.android.sensor.position.AcceptNewPosition; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.os.Bundle; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.widget.Switch; -import android.widget.TextView; import java.util.ArrayList; -import org.walkersguide.android.database.util.AccessDatabase; import org.walkersguide.android.data.object_with_id.point.Intersection; import org.walkersguide.android.data.object_with_id.point.point_with_address_data.POI; import org.walkersguide.android.data.object_with_id.point.point_with_address_data.poi.Station; @@ -39,16 +26,10 @@ import org.walkersguide.android.ui.fragment.object_list.simple.PedestrianCrossingListFragment; import org.walkersguide.android.data.object_with_id.Point; import org.walkersguide.android.ui.view.ObjectWithIdView; -import org.walkersguide.android.util.GlobalInstance; import org.walkersguide.android.sensor.PositionManager; import timber.log.Timber; import org.walkersguide.android.data.ObjectWithId; -import android.os.Handler; -import android.os.Looper; -import androidx.core.app.NavUtils; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import org.walkersguide.android.data.object_with_id.segment.IntersectionSegment; import org.walkersguide.android.data.object_with_id.Segment; import org.walkersguide.android.data.object_with_id.Route; @@ -58,10 +39,6 @@ import org.walkersguide.android.server.wg.street_course.StreetCourseRequest; import org.walkersguide.android.ui.fragment.TabLayoutFragment.AbstractTabAdapter; import org.walkersguide.android.data.object_with_id.point.point_with_address_data.StreetAddress; -import androidx.fragment.app.DialogFragment; -import android.text.TextUtils; -import org.walkersguide.android.ui.UiHelper; -import androidx.annotation.Nullable; import androidx.annotation.NonNull; import android.view.Menu; import android.view.MenuInflater; @@ -131,7 +108,7 @@ private static ObjectDetailsTabLayoutFragment newInstance(ObjectWithId object, T private ObjectWithId object; - private TextView labelDistanceAndBearing; + private DistanceAndBearingView labelDistanceAndBearing; @Override public int getLayoutResourceId() { return R.layout.fragment_object_details; @@ -139,7 +116,6 @@ private static ObjectDetailsTabLayoutFragment newInstance(ObjectWithId object, T @Override public View configureView(View view, Bundle savedInstanceState) { view = super.configureView(view, savedInstanceState); - requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); // load object object = (ObjectWithId) getArguments().getSerializable(KEY_OBJECT); @@ -156,13 +132,18 @@ private static ObjectDetailsTabLayoutFragment newInstance(ObjectWithId object, T }, false); layoutObject.configureAsSingleObject(object); - labelDistanceAndBearing = (TextView) view.findViewById(R.id.labelDistanceAndBearing); + labelDistanceAndBearing = (DistanceAndBearingView) view.findViewById(R.id.labelDistanceAndBearing); labelDistanceAndBearing.setVisibility(View.GONE); } return view; } + @Override public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED); + } + @Override public void onStart() { super.onStart(); @@ -229,133 +210,20 @@ private static ObjectDetailsTabLayoutFragment newInstance(ObjectWithId object, T /** * pause and resume */ - private AcceptNewPosition acceptNewPositionForTtsAnnouncement; @Override public void onPause() { super.onPause(); - if (object instanceof Point) { - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(newLocationAndDirectionReceiverForPoints); - } else if (object instanceof Segment) { - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(newDirectionReceiverForSegments); - } } @Override public void onResume() { super.onResume(); - Timber.d("onResume: object=%1$s", object); - if (object instanceof Point) { - IntentFilter filter = new IntentFilter(); - filter.addAction(PositionManager.ACTION_NEW_LOCATION); - filter.addAction(DeviceSensorManager.ACTION_NEW_BEARING); - LocalBroadcastManager.getInstance(getActivity()).registerReceiver(newLocationAndDirectionReceiverForPoints, filter); - // request current location to update the ui - labelDistanceAndBearing.setVisibility(View.VISIBLE); - acceptNewPositionForTtsAnnouncement = AcceptNewPosition.newInstanceForTtsAnnouncement(); - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override public void run() { - Timber.d("runnable get current location"); - // wait, until onResume is finished and the ui has focus - PositionManager.getInstance().requestCurrentLocation(); - } - }, 200); - - } else if (object instanceof Segment) { - IntentFilter filter = new IntentFilter(); - filter.addAction(DeviceSensorManager.ACTION_NEW_BEARING); - LocalBroadcastManager.getInstance(getActivity()).registerReceiver(newDirectionReceiverForSegments, filter); - // request current direction to update the ui + labelDistanceAndBearing.setObjectWithId(object); labelDistanceAndBearing.setVisibility(View.VISIBLE); - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override public void run() { - // wait, until onResume is finished and the ui has focus - DeviceSensorManager.getInstance().requestCurrentBearing(); - } - }, 200); } } - /** - * broadcast receiver - */ - - // point: distance and bearing - - private BroadcastReceiver newLocationAndDirectionReceiverForPoints = new BroadcastReceiver() { - // distance label - private AcceptNewPosition acceptNewPositionForDistanceLabel = AcceptNewPosition.newInstanceForDistanceLabelUpdate(); - private AcceptNewBearing acceptNewBearing = AcceptNewBearing.newInstanceForDistanceLabelUpdate(); - - private TTSWrapper ttsWrapperInstance = TTSWrapper.getInstance(); - - @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(PositionManager.ACTION_NEW_LOCATION)) { - boolean announceViaTts = false; - if (acceptNewPositionForDistanceLabel.updatePoint( - (Point) intent.getSerializableExtra(PositionManager.EXTRA_NEW_LOCATION), - UiHelper.isInBackground(ObjectDetailsTabLayoutFragment.this), - intent.getBooleanExtra(PositionManager.EXTRA_IS_IMPORTANT, false))) { - updateDistanceAndBearingLabel(); - announceViaTts = labelDistanceAndBearing.isAccessibilityFocused(); - } - if (acceptNewPositionForTtsAnnouncement.updatePoint( - (Point) intent.getSerializableExtra(PositionManager.EXTRA_NEW_LOCATION), false, false)) { - if (! UiHelper.isInBackground(ObjectDetailsTabLayoutFragment.this)) { - announceViaTts = true; - } - } - if (announceViaTts) { - ttsWrapperInstance.announce( - ((Point) object).formatDistanceAndRelativeBearingFromCurrentLocation(R.plurals.meter)); - } - - } else if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING)) { - if (acceptNewBearing.updateBearing( - (Bearing) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING), - UiHelper.isInBackground(ObjectDetailsTabLayoutFragment.this), - intent.getBooleanExtra(DeviceSensorManager.EXTRA_IS_IMPORTANT, false))) { - updateDistanceAndBearingLabel(); - } - } - } - - private void updateDistanceAndBearingLabel() { - labelDistanceAndBearing.setText( - String.format( - GlobalInstance.getStringResource(R.string.labelPointDistanceAndBearing), - ((Point) object).formatDistanceAndRelativeBearingFromCurrentLocation( - R.plurals.meter, settingsManagerInstance.getShowPreciseBearingValues())) - ); - } - }; - - // segments: bearing - - private BroadcastReceiver newDirectionReceiverForSegments = new BroadcastReceiver() { - private AcceptNewBearing acceptNewBearing = AcceptNewBearing.newInstanceForDistanceLabelUpdate(); - - @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING)) { - if (acceptNewBearing.updateBearing( - (Bearing) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING), - UiHelper.isInBackground(ObjectDetailsTabLayoutFragment.this), - intent.getBooleanExtra(DeviceSensorManager.EXTRA_IS_IMPORTANT, false))) { - updateDirectionLabel(); - } - } - } - - private void updateDirectionLabel() { - labelDistanceAndBearing.setText( - String.format( - GlobalInstance.getStringResource(R.string.labelSegmentDirection), - ((Segment) object).getBearing().relativeToCurrentBearing().getDirection()) - ); - } - }; - - /** * fragment management */ @@ -450,13 +318,24 @@ public TabAdapter(ArrayList tabList) { case PEDESTRIAN_CROSSINGS: return getResources().getString(R.string.fragmentPedestrianCrossingsName); case STREET_COURSE: - return object instanceof IntersectionSegment - ? (new StreetCourseRequest((IntersectionSegment) object)).getStreetCourseName() - : getResources().getString(R.string.fragmentStreetCourseName); + return getResources().getString(R.string.fragmentStreetCourseName); } } return null; } + + @Override public String getFragmentDescription(int position) { + Tab tab = getTab(position); + if (tab != null) { + switch (tab) { + case STREET_COURSE: + if (object instanceof IntersectionSegment) { + return (new StreetCourseRequest((IntersectionSegment) object)).getStreetCourseName(); + } + } + } + return super.getFragmentDescription(position); + } } } diff --git a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/object_details/SegmentDetailsFragment.java b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/object_details/SegmentDetailsFragment.java index 204e098..57ccb4c 100644 --- a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/object_details/SegmentDetailsFragment.java +++ b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/object_details/SegmentDetailsFragment.java @@ -29,6 +29,19 @@ import androidx.core.view.MenuProvider; import androidx.lifecycle.Lifecycle; import androidx.annotation.NonNull; +import org.walkersguide.android.sensor.DeviceSensorManager; +import org.walkersguide.android.ui.UiHelper; +import android.content.BroadcastReceiver; +import org.walkersguide.android.sensor.bearing.AcceptNewBearing; +import android.content.Context; +import android.content.IntentFilter; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.view.accessibility.AccessibilityEvent; +import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; +import android.widget.TextView; +import org.walkersguide.android.data.angle.Bearing; +import org.walkersguide.android.data.angle.RelativeBearing; public class SegmentDetailsFragment extends Fragment implements MenuProvider { @@ -41,6 +54,7 @@ public class SegmentDetailsFragment extends Fragment implements MenuProvider { // ui components private LinearLayout layoutAttributes; + private TextView labelBearing; // newInstance constructor for creating fragment with arguments public static SegmentDetailsFragment newInstance(Segment segment) { @@ -56,7 +70,6 @@ public static SegmentDetailsFragment newInstance(Segment segment) { * create view */ - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.layout_single_linear_layout, container, false); } @@ -105,6 +118,10 @@ public static SegmentDetailsFragment newInstance(Segment segment) { @Override public void onPause() { super.onPause(); + if (segment != null) { + labelBearing = null; + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(newBearingValueReceiver); + } } @Override public void onResume() { @@ -114,6 +131,11 @@ public static SegmentDetailsFragment newInstance(Segment segment) { return; } + // register for bearing updates + IntentFilter filter = new IntentFilter(); + filter.addAction(DeviceSensorManager.ACTION_NEW_BEARING); + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(newBearingValueReceiver, filter); + UserAnnotationView layoutUserAnnotation = new UserAnnotationView(getActivity()); layoutUserAnnotation.setObjectWithId(segment); layoutAttributes.addView(layoutUserAnnotation); @@ -174,15 +196,13 @@ public static SegmentDetailsFragment newInstance(Segment segment) { ); // bearing - layoutAttributes.addView( - new TextViewBuilder( - SegmentDetailsFragment.this.getContext(), - String.format( - "%1$s: %2$s", - getResources().getString(R.string.labelSegmentBearing), - segment.getBearing())) - .create() - ); + labelBearing = new TextViewBuilder( + SegmentDetailsFragment.this.getContext(), + getResources().getString(R.string.labelSegmentBearing)) + .setAccessibilityDelegate( + UiHelper.getAccessibilityDelegateToMuteContentChangedEventsWhileFocussed()) + .create(); + layoutAttributes.addView(labelBearing); // description if (segment.getDescription() != null) { @@ -349,6 +369,43 @@ public static SegmentDetailsFragment newInstance(Segment segment) { ); } } + + // request current bearing to update the ui + DeviceSensorManager.getInstance().requestCurrentBearing(); } + + private BroadcastReceiver newBearingValueReceiver = new BroadcastReceiver() { + private AcceptNewBearing acceptNewBearing = AcceptNewBearing.newInstanceForBearingLabelUpdate(); + private RelativeBearing.Direction lastDirection = null; + + @Override public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING) + && labelBearing != null + && acceptNewBearing.updateBearing( + (Bearing) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING), + UiHelper.isInBackground(SegmentDetailsFragment.this), + intent.getBooleanExtra(DeviceSensorManager.EXTRA_IS_IMPORTANT, false))) { + + RelativeBearing.Direction currentDirection = segment + .getBearing().relativeToCurrentBearing().getDirection(); + labelBearing.setText( + String.format( + "%1$s: %2$s, %3$s", + context.getResources().getString(R.string.labelSegmentBearing), + segment.getBearing(), + currentDirection) + ); + + if (currentDirection != lastDirection) { + if (labelBearing.isAccessibilityFocused()) { + TTSWrapper.getInstance().announce( + currentDirection.toString(), MessageType.DISTANCE_OR_BEARING); + } + lastDirection = currentDirection; + } + } + } + }; + } diff --git a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/overview/StartFragment.java b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/overview/StartFragment.java index d43b4f6..a7d6abf 100644 --- a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/overview/StartFragment.java +++ b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/overview/StartFragment.java @@ -1,5 +1,6 @@ package org.walkersguide.android.ui.fragment.tabs.overview; +import org.walkersguide.android.ui.dialog.create.ImportGpxFileDialog; import org.walkersguide.android.ui.dialog.create.PointFromCoordinatesLinkDialog; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -61,6 +62,8 @@ import org.walkersguide.android.util.SettingsManager; import org.walkersguide.android.ui.activity.MainActivity; import org.walkersguide.android.ui.fragment.tabs.ObjectDetailsTabLayoutFragment; +import org.walkersguide.android.database.DatabaseProfile; +import org.walkersguide.android.ui.fragment.object_list.extended.ObjectListFromDatabaseFragment; public class StartFragment extends BaseOverviewFragment @@ -80,6 +83,9 @@ public static StartFragment newInstance() { getChildFragmentManager() .setFragmentResultListener( PointFromCoordinatesLinkDialog.REQUEST_FROM_COORDINATES_LINK, this, this); + getChildFragmentManager() + .setFragmentResultListener( + ImportGpxFileDialog.REQUEST_IMPORT_OF_GPX_FILE_WAS_SUCCESSFUL, this, this); } @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { @@ -92,10 +98,18 @@ public static StartFragment newInstance() { mainActivityController.addFragment( ObjectDetailsTabLayoutFragment.details(sharedLocation)); } + } else if (requestKey.equals(ImportGpxFileDialog.REQUEST_IMPORT_OF_GPX_FILE_WAS_SUCCESSFUL)) { + DatabaseProfile profileFromGpxFileImport = (DatabaseProfile) bundle.getSerializable(ImportGpxFileDialog.EXTRA_GPX_FILE_PROFILE); + if (profileFromGpxFileImport != null) { + mainActivityController.addFragment( + CollectionListFragment.newInstance()); + mainActivityController.addFragment( + ObjectListFromDatabaseFragment.newInstance(profileFromGpxFileImport)); + } } } - private void prepareRequestAndCalculateRoute(StreetAddress destination) { + private void prepareRequestAndCalculateRoute(Point destination) { P2pRouteRequest p2pRouteRequest = P2pRouteRequest.getDefault(); // start point @@ -147,6 +161,22 @@ public void onClick(View view) { } }); + Button buttonNavigateHome = (Button) view.findViewById(R.id.buttonNavigateHome); + buttonNavigateHome.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + Point homeAddress = SettingsManager.getInstance().getHomeAddress(); + if (homeAddress == null) { + Toast.makeText( + getActivity(), + GlobalInstance.getStringResource(R.string.errorNoHomeAddressSet), + Toast.LENGTH_LONG) + .show(); + } else { + prepareRequestAndCalculateRoute(homeAddress); + } + } + }); + Button buttonSaveCurrentLocation = (Button) view.findViewById(R.id.buttonSaveCurrentLocation); buttonSaveCurrentLocation.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { @@ -176,6 +206,14 @@ public void onClick(View view) { } }); + Button buttonOpenGpxFile = (Button) view.findViewById(R.id.buttonOpenGpxFile); + buttonOpenGpxFile.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + ImportGpxFileDialog.newInstance(null, false) + .show(getChildFragmentManager(), "ImportGpxFileDialog"); + } + }); + Button buttonRecordRoute = (Button) view.findViewById(R.id.buttonRecordRoute); buttonRecordRoute.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/routes/NavigateFragment.java b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/routes/NavigateFragment.java index bbf5721..6defe35 100644 --- a/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/routes/NavigateFragment.java +++ b/app/src/main/java/org/walkersguide/android/ui/fragment/tabs/routes/NavigateFragment.java @@ -1,15 +1,13 @@ package org.walkersguide.android.ui.fragment.tabs.routes; +import org.walkersguide.android.ui.view.DistanceAndBearingView; import org.walkersguide.android.data.object_with_id.Segment.SortByBearingRelativeTo; import androidx.appcompat.app.AppCompatActivity; import android.os.Build; -import android.annotation.SuppressLint; import org.walkersguide.android.ui.view.IntersectionScheme; import android.widget.ImageButton; import org.walkersguide.android.ui.activity.MainActivity; import org.walkersguide.android.ui.activity.MainActivityController; -import androidx.fragment.app.FragmentResultListener; -import org.walkersguide.android.sensor.bearing.AcceptNewBearing; import org.walkersguide.android.data.angle.RelativeBearing; import org.walkersguide.android.data.angle.Bearing; import org.walkersguide.android.data.angle.bearing.BearingSensorValue; @@ -17,7 +15,6 @@ import org.walkersguide.android.ui.view.RouteObjectView; import org.walkersguide.android.ui.view.ObjectWithIdView; import android.widget.Toast; -import timber.log.Timber; import android.content.BroadcastReceiver; import android.content.Context; @@ -25,7 +22,6 @@ import android.content.IntentFilter; import android.os.Bundle; -import android.os.Vibrator; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -37,52 +33,31 @@ import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.TextView; import org.walkersguide.android.data.object_with_id.route.RouteObject; -import org.walkersguide.android.sensor.position.AcceptNewPosition; import org.walkersguide.android.R; import org.walkersguide.android.sensor.DeviceSensorManager; import org.walkersguide.android.sensor.PositionManager; -import org.walkersguide.android.ui.dialog.PlanRouteDialog; import org.walkersguide.android.util.SettingsManager; import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; import androidx.fragment.app.Fragment; import org.walkersguide.android.util.GlobalInstance; import org.walkersguide.android.data.object_with_id.Point; import org.walkersguide.android.data.object_with_id.Route; -import androidx.fragment.app.FragmentResultListener; import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import java.util.Locale; -import org.walkersguide.android.database.DatabaseProfile; -import org.walkersguide.android.ui.fragment.object_list.extended.ObjectListFromDatabaseFragment; -import org.walkersguide.android.data.ObjectWithId; import org.walkersguide.android.util.Helper; import android.os.Handler; import android.os.Looper; -import java.util.ArrayList; -import androidx.fragment.app.DialogFragment; -import android.app.Dialog; -import android.content.DialogInterface; -import androidx.appcompat.app.AlertDialog; -import android.widget.ListView; -import androidx.core.util.Pair; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.AbsListView; import org.walkersguide.android.data.object_with_id.point.Intersection; import org.walkersguide.android.data.object_with_id.Segment; import org.walkersguide.android.data.object_with_id.segment.IntersectionSegment; import org.walkersguide.android.data.Angle; -import java.util.Collections; import android.text.TextUtils; -import androidx.constraintlayout.widget.ConstraintLayout; import android.widget.LinearLayout; -import java.util.List; import java.util.LinkedHashMap; import java.util.stream.Collectors; import org.walkersguide.android.ui.UiHelper; @@ -125,7 +100,7 @@ public static NavigateFragment newInstance(Route route, boolean showObjectWithId private TextView labelIntersectionStructure; private IntersectionScheme intersectionScheme; // bottom - private TextView labelDistanceAndBearing; + private DistanceAndBearingView labelDistanceAndBearing; private ImageButton buttonPreviousRouteObject, buttonNextRouteObject; @Override public void onCreate(Bundle savedInstanceState) { @@ -300,7 +275,7 @@ public static NavigateFragment newInstance(Route route, boolean showObjectWithId intersectionScheme = (IntersectionScheme) view.findViewById(R.id.intersectionScheme); // bottom layout - labelDistanceAndBearing = (TextView) view.findViewById(R.id.labelDistanceAndBearing); + labelDistanceAndBearing = (DistanceAndBearingView) view.findViewById(R.id.labelDistanceAndBearing); buttonPreviousRouteObject = (ImageButton) view.findViewById(R.id.buttonPreviousRouteObject); buttonPreviousRouteObject.setOnClickListener(new View.OnClickListener() { @@ -337,7 +312,6 @@ public void loadNewRouteFromSettings() { /** * pause and resume */ - private AcceptNewPosition acceptNewPositionForTtsAnnouncement; @Override public void onPause() { super.onPause(); @@ -347,13 +321,11 @@ public void loadNewRouteFromSettings() { @Override public void onResume() { super.onResume(); - IntentFilter locationAndBearingUpdateFilter = new IntentFilter(); - locationAndBearingUpdateFilter.addAction(PositionManager.ACTION_NEW_LOCATION); - locationAndBearingUpdateFilter.addAction(DeviceSensorManager.ACTION_NEW_BEARING); - locationAndBearingUpdateFilter.addAction(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE); + IntentFilter bearingUpdateFilter = new IntentFilter(); + bearingUpdateFilter.addAction(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE); LocalBroadcastManager .getInstance(getActivity()) - .registerReceiver(routeBroadcastReceiver, locationAndBearingUpdateFilter); + .registerReceiver(routeBroadcastReceiver, bearingUpdateFilter); requestUiUpdate(); } @@ -376,7 +348,6 @@ private void requestUiUpdate() { updateUi(); // request current location for labelDistanceAndBearing field - acceptNewPositionForTtsAnnouncement = AcceptNewPosition.newInstanceForTtsAnnouncement(); new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { // wait, until onResume is finished and the ui has focus @@ -403,7 +374,7 @@ private void onPostProcessSkipRouteObjectManually(boolean skipWasSuccessful) { } else { nextInstruction = GlobalInstance.getStringResource(R.string.messageFirstSegment); } - ttsWrapperInstance.announce(nextInstruction); + ttsWrapperInstance.announce(nextInstruction, MessageType.TOP_PRIORITY); } updateUi(); } @@ -491,27 +462,14 @@ private void updateUi() { } } - updateDistanceAndBearingLabel(currentRouteObject); - } - - private void updateDistanceAndBearingLabel(RouteObject currentRouteObject) { - labelDistanceAndBearing.setText( - currentRouteObject - .getPoint() - .formatDistanceAndRelativeBearingFromCurrentLocation( - R.plurals.meter, settingsManagerInstance.getShowPreciseBearingValues())); + labelDistanceAndBearing.setObjectWithId(currentRouteObject.getPoint()); } private class RouteBroadcastReceiver extends BroadcastReceiver { private static final int SHORTLY_BEFORE_ARRIVAL_THRESHOLD_IN_METERS = 30; - // distance label - private AcceptNewPosition acceptNewPositionForDistanceLabel = AcceptNewPosition.newInstanceForDistanceLabelUpdate(); - private AcceptNewBearing acceptNewBearing = AcceptNewBearing.newInstanceForDistanceLabelUpdate(); - private RouteObject lastRouteObject = null; - private boolean shortlyBeforeArrivalAnnounced, arrivalAnnounced; private long arrivalTime; @@ -527,53 +485,26 @@ private class RouteBroadcastReceiver extends BroadcastReceiver { this.arrivalTime = System.currentTimeMillis(); } - if (intent.getAction().equals(PositionManager.ACTION_NEW_LOCATION)) { - boolean announceViaTts = false; - if (acceptNewPositionForDistanceLabel.updatePoint( - (Point) intent.getSerializableExtra(PositionManager.EXTRA_NEW_LOCATION), - UiHelper.isInBackground(NavigateFragment.this), - intent.getBooleanExtra(PositionManager.EXTRA_IS_IMPORTANT, false))) { - updateDistanceAndBearingLabel(currentRouteObject); - announceViaTts = labelDistanceAndBearing.isAccessibilityFocused(); - } - if (acceptNewPositionForTtsAnnouncement.updatePoint( - (Point) intent.getSerializableExtra(PositionManager.EXTRA_NEW_LOCATION), false, false)) { - if (! UiHelper.isInBackground(NavigateFragment.this)) { - announceViaTts = true; - } - } - if (announceViaTts) { - ttsWrapperInstance.announce( - currentRouteObject.getPoint().formatDistanceAndRelativeBearingFromCurrentLocation(R.plurals.meter)); - } - - } else if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING)) { - if (acceptNewBearing.updateBearing( - (Bearing) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING), - UiHelper.isInBackground(NavigateFragment.this), - intent.getBooleanExtra(DeviceSensorManager.EXTRA_IS_IMPORTANT, false))) { - updateDistanceAndBearingLabel(currentRouteObject); + if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE)) { + BearingSensorValue bearingValueFromSatellite = (BearingSensorValue) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING); + if (bearingValueFromSatellite == null) return; + + // announce shortly before arrival + if ( + ! shortlyBeforeArrivalAnnounced + && ! currentRouteObject.getIsFirstRouteObject() + && currentRouteObject.getPoint().distanceFromCurrentLocation() < SHORTLY_BEFORE_ARRIVAL_THRESHOLD_IN_METERS + && currentRouteObject.getSegment().getDistance() > SHORTLY_BEFORE_ARRIVAL_THRESHOLD_IN_METERS) { + announceShortlyBeforeArrival(currentRouteObject); } - } else if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE)) { - BearingSensorValue bearingValueFromSatellite = (BearingSensorValue) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING); - if (bearingValueFromSatellite != null) { - // announce shortly before arrival - if ( - ! shortlyBeforeArrivalAnnounced - && ! currentRouteObject.getIsFirstRouteObject() - && currentRouteObject.getPoint().distanceFromCurrentLocation() < SHORTLY_BEFORE_ARRIVAL_THRESHOLD_IN_METERS - && currentRouteObject.getSegment().getDistance() > SHORTLY_BEFORE_ARRIVAL_THRESHOLD_IN_METERS) { - announceShortlyBeforeArrival(currentRouteObject); - } - // announce arrival - if ( - ! arrivalAnnounced - && nextRouteObjectWithinRange(currentRouteObject, bearingValueFromSatellite) - && ! walkingReverseDetected(currentRouteObject, bearingValueFromSatellite) - && (System.currentTimeMillis() - arrivalTime) > 5000) { - announceArrival(currentRouteObject); - } + // announce arrival + if ( + ! arrivalAnnounced + && nextRouteObjectWithinRange(currentRouteObject, bearingValueFromSatellite) + && ! walkingReverseDetected(currentRouteObject, bearingValueFromSatellite) + && (System.currentTimeMillis() - arrivalTime) > 5000) { + announceArrival(currentRouteObject); } } } @@ -625,14 +556,15 @@ private boolean walkingReverseDetected(RouteObject currentRouteObject, BearingSe private void announceShortlyBeforeArrival(RouteObject currentRouteObject) { shortlyBeforeArrivalAnnounced = true; ttsWrapperInstance.announce( - route.formatShortlyBeforeArrivalAtPointMessage()); + route.formatShortlyBeforeArrivalAtPointMessage(), + MessageType.INSTRUCTION); } private void announceArrival(RouteObject currentRouteObject) { shortlyBeforeArrivalAnnounced = true; arrivalAnnounced = true; ttsWrapperInstance.announce( - route.formatArrivalAtPointMessage()); + route.formatArrivalAtPointMessage(), MessageType.INSTRUCTION); Helper.vibrateOnce(Helper.VIBRATION_DURATION_LONG); // auto jump to next route point diff --git a/app/src/main/java/org/walkersguide/android/ui/view/DistanceAndBearingView.java b/app/src/main/java/org/walkersguide/android/ui/view/DistanceAndBearingView.java new file mode 100644 index 0000000..a0493a2 --- /dev/null +++ b/app/src/main/java/org/walkersguide/android/ui/view/DistanceAndBearingView.java @@ -0,0 +1,249 @@ +package org.walkersguide.android.ui.view; + +import org.walkersguide.android.util.Helper; +import android.os.Handler; +import android.os.Looper; +import org.walkersguide.android.sensor.bearing.AcceptNewBearing; +import org.walkersguide.android.sensor.position.AcceptNewPosition; +import android.view.accessibility.AccessibilityEvent; +import org.walkersguide.android.ui.dialog.edit.UserAnnotationForObjectWithIdDialog; +import org.walkersguide.android.ui.interfaces.ViewChangedListener; +import org.walkersguide.android.ui.fragment.tabs.ObjectDetailsTabLayoutFragment; +import org.walkersguide.android.ui.UiHelper; +import androidx.core.view.ViewCompat; + +import org.walkersguide.android.sensor.bearing.AcceptNewBearing; +import org.walkersguide.android.sensor.position.AcceptNewPosition; +import org.walkersguide.android.ui.activity.MainActivity; +import org.walkersguide.android.ui.activity.MainActivityController; +import org.walkersguide.android.server.wg.p2p.P2pRouteRequest; +import androidx.appcompat.app.AppCompatActivity; +import org.walkersguide.android.data.ObjectWithId; +import android.view.MenuItem; +import timber.log.Timber; + + + +import android.view.View; +import android.view.ViewGroup; + +import android.widget.TextView; + +import android.widget.ImageButton; +import org.walkersguide.android.util.GlobalInstance; +import org.walkersguide.android.R; +import android.text.TextUtils; +import org.walkersguide.android.util.SettingsManager; +import org.walkersguide.android.data.object_with_id.Point; +import android.content.Context; +import android.widget.ImageView; +import androidx.appcompat.widget.PopupMenu; +import androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener; +import android.view.Menu; +import android.view.SubMenu; +import android.content.Intent; +import android.widget.LinearLayout; +import android.util.AttributeSet; +import android.view.Gravity; +import org.walkersguide.android.sensor.DeviceSensorManager; +import org.walkersguide.android.sensor.PositionManager; +import org.walkersguide.android.data.object_with_id.Segment; +import androidx.core.view.MenuCompat; +import android.content.BroadcastReceiver; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.content.IntentFilter; +import org.walkersguide.android.data.object_with_id.Route; +import android.widget.Toast; +import org.walkersguide.android.ui.dialog.PlanRouteDialog; +import org.walkersguide.android.data.object_with_id.point.Intersection; +import android.content.res.TypedArray; +import org.walkersguide.android.data.object_with_id.segment.IntersectionSegment; +import org.walkersguide.android.data.object_with_id.point.point_with_address_data.poi.Station; +import org.walkersguide.android.data.object_with_id.point.point_with_address_data.POI; +import org.walkersguide.android.data.angle.Bearing; +import org.walkersguide.android.database.profile.StaticProfile; +import org.walkersguide.android.ui.dialog.template.EnterStringDialog; +import android.os.Bundle; +import android.app.Dialog; +import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; +import android.graphics.Rect; +import android.view.TouchDelegate; +import java.lang.Runnable; +import org.json.JSONException; +import org.walkersguide.android.ui.dialog.select.SelectCollectionsDialog; +import java.util.ArrayList; +import org.walkersguide.android.database.profile.Collection; +import org.walkersguide.android.database.util.AccessDatabase; +import java.util.LinkedHashMap; +import java.util.Map; +import android.view.accessibility.AccessibilityNodeInfo; +import org.walkersguide.android.data.angle.RelativeBearing; +import org.walkersguide.android.data.angle.bearing.BearingSensorValue; +import androidx.appcompat.widget.AppCompatTextView; + + +public class DistanceAndBearingView extends AppCompatTextView { + + private DeviceSensorManager deviceSensorManagerInstance; + private PositionManager positionManagerInstance; + private SettingsManager settingsManagerInstance; + private TTSWrapper ttsWrapperInstance; + + private String prefix = null; + private ObjectWithId objectWithId = null; + + public DistanceAndBearingView(Context context) { + super(context); + this.initUi(context); + } + + public DistanceAndBearingView(Context context, String prefix) { + super(context); + this.prefix = prefix; + this.initUi(context); + } + + public DistanceAndBearingView(Context context, AttributeSet attrs) { + super(context, attrs); + + // parse xml layout attributes + TypedArray attributeArray = context.obtainStyledAttributes( + attrs, R.styleable.ObjectWithIdAndProfileView); + if (attributeArray != null) { + this.prefix = attributeArray.getString( + R.styleable.ObjectWithIdAndProfileView_prefix); + attributeArray.recycle(); + } + + this.initUi(context); + } + + public void setObjectWithId(ObjectWithId object) { + this.objectWithId = object; + updateDistanceAndBearingLabel(); + } + + private void initUi(Context context) { + deviceSensorManagerInstance = DeviceSensorManager.getInstance(); + positionManagerInstance = PositionManager.getInstance(); + settingsManagerInstance = SettingsManager.getInstance(); + ttsWrapperInstance = TTSWrapper.getInstance(); + + setAccessibilityDelegate( + UiHelper.getAccessibilityDelegateToMuteContentChangedEventsWhileFocussed()); + } + + + /** + * broadcasts + */ + + @Override public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + Timber.d("onDetachedFromWindow: %1$s", objectWithId); + LocalBroadcastManager.getInstance(GlobalInstance.getContext()) + .unregisterReceiver(newLocationAndDirectionReceiver); + } + + @Override public void onAttachedToWindow() { + super.onAttachedToWindow(); + Timber.d("onAttachedToWindow: %1$s", objectWithId); + + IntentFilter filter = new IntentFilter(); + filter.addAction(PositionManager.ACTION_NEW_LOCATION); + filter.addAction(DeviceSensorManager.ACTION_NEW_BEARING); + filter.addAction(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE); + LocalBroadcastManager.getInstance(GlobalInstance.getContext()) + .registerReceiver(newLocationAndDirectionReceiver, filter); + + // request current location to update the ui + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override public void run() { + positionManagerInstance.requestCurrentLocation(); + } + }, 200); + } + + + private BroadcastReceiver newLocationAndDirectionReceiver = new BroadcastReceiver() { + private AcceptNewPosition acceptNewPosition = AcceptNewPosition.newInstanceForDistanceLabelUpdate(); + private AcceptNewPosition acceptNewPositionTts = AcceptNewPosition.newInstanceForTtsAnnouncement(); + private AcceptNewPosition acceptNewPositionTtsFocus = AcceptNewPosition.newInstanceForTtsAnnouncementOnFocus(); + + private AcceptNewBearing acceptNewBearing = AcceptNewBearing.newInstanceForBearingLabelUpdate(); + private AcceptNewBearing acceptNewBearingSatellite = AcceptNewBearing.newInstanceForBearingLabelUpdate(); + private RelativeBearing.Direction lastDirection = null; + + @Override public void onReceive(Context context, Intent intent) { + if (objectWithId == null) return; + + if (intent.getAction().equals(PositionManager.ACTION_NEW_LOCATION)) { + Point currentLocation = (Point) intent.getSerializableExtra(PositionManager.EXTRA_NEW_LOCATION); + + if (acceptNewPosition.updatePoint( + currentLocation, false, intent.getBooleanExtra(PositionManager.EXTRA_IS_IMPORTANT, false))) { + updateDistanceAndBearingLabel(); + } + + boolean announce = false; + // no if/else, it's important, that both updatePoint functions are called to keep them up to date + if (acceptNewPositionTts.updatePoint(currentLocation, false, false)) { + announce = true; + } + if (acceptNewPositionTtsFocus.updatePoint(currentLocation, false, false) + && isAccessibilityFocused()) { + announce = true; + } + if (announce) { + ttsWrapperInstance.announce( + objectWithId.formatDistanceAndRelativeBearingFromCurrentLocation( + R.plurals.meter, settingsManagerInstance.getShowPreciseBearingValues()), + MessageType.DISTANCE_OR_BEARING); + } + + } else if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING) + || intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE)) { + boolean announce = false; + + if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING) + && acceptNewBearing.updateBearing( + (Bearing) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING), + false, intent.getBooleanExtra(DeviceSensorManager.EXTRA_IS_IMPORTANT, false))) { + announce = isAccessibilityFocused(); + updateDistanceAndBearingLabel(); + } + if (intent.getAction().equals(DeviceSensorManager.ACTION_NEW_BEARING_VALUE_FROM_SATELLITE) + && acceptNewBearingSatellite.updateBearing( + (BearingSensorValue) intent.getSerializableExtra(DeviceSensorManager.EXTRA_BEARING), + false, intent.getBooleanExtra(DeviceSensorManager.EXTRA_IS_IMPORTANT, false))) { + announce = true; + } + + RelativeBearing.Direction currentDirection = objectWithId + .relativeBearingFromCurrentLocation().getDirection(); + if (announce && currentDirection != lastDirection) { + ttsWrapperInstance.announce( + objectWithId.formatRelativeBearingFromCurrentLocation( + settingsManagerInstance.getShowPreciseBearingValues()), + MessageType.DISTANCE_OR_BEARING); + lastDirection = currentDirection; + } + } + } + }; + + + private void updateDistanceAndBearingLabel() { + String text = ""; + if (prefix != null) { + text += String.format("%1$s: ", prefix); + } + if (objectWithId != null) { + text += objectWithId.formatDistanceAndRelativeBearingFromCurrentLocation( + R.plurals.meter, settingsManagerInstance.getShowPreciseBearingValues()); + } + setText(text); + } + +} diff --git a/app/src/main/java/org/walkersguide/android/ui/view/IntersectionScheme.java b/app/src/main/java/org/walkersguide/android/ui/view/IntersectionScheme.java index c45803d..59fa354 100644 --- a/app/src/main/java/org/walkersguide/android/ui/view/IntersectionScheme.java +++ b/app/src/main/java/org/walkersguide/android/ui/view/IntersectionScheme.java @@ -7,6 +7,7 @@ import org.walkersguide.android.util.Helper; import android.widget.Toast; import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; import android.content.Context; import android.graphics.Canvas; @@ -224,8 +225,8 @@ public SelfVoicingTouchHelper(View forView) { GlobalInstance.getPluralResource( R.plurals.turning, intersectionSegmentRelativeToInstructionMap.size()) - ) - ); + ), + MessageType.TOP_PRIORITY); Helper.vibrateOnce( 100, Helper.VIBRATION_INTENSITY_WEAK); this.lastAnnouncedSegment = null; @@ -250,7 +251,7 @@ public SelfVoicingTouchHelper(View forView) { announcement += String.format( ", %1$s", GlobalInstance.getStringResource(R.string.labelPartOfNextRouteSegment)); } - ttsWrapperInstance.announce(announcement); + ttsWrapperInstance.announce(announcement, MessageType.TOP_PRIORITY); this.lastAnnouncedSegment = segment; this.atSegment = true; } diff --git a/app/src/main/java/org/walkersguide/android/ui/view/ObjectWithIdView.java b/app/src/main/java/org/walkersguide/android/ui/view/ObjectWithIdView.java index 9916d08..e3d7c40 100644 --- a/app/src/main/java/org/walkersguide/android/ui/view/ObjectWithIdView.java +++ b/app/src/main/java/org/walkersguide/android/ui/view/ObjectWithIdView.java @@ -439,8 +439,8 @@ private boolean executeAccessibilityMenuAction(int menuItemId) { } private BroadcastReceiver newLocationReceiver = new BroadcastReceiver() { - private AcceptNewPosition acceptNewPosition = new AcceptNewPosition(6, 4, null); - private AcceptNewBearing acceptNewBearing = new AcceptNewBearing(30, 2); + private AcceptNewPosition acceptNewPosition = new AcceptNewPosition(6, 4000l, null); + private AcceptNewBearing acceptNewBearing = new AcceptNewBearing(30, 2000l); @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(RenameObjectWithIdDialog.ACTION_RENAME_OBJECT_WITH_ID_WAS_SUCCESSFUL)) { diff --git a/app/src/main/java/org/walkersguide/android/ui/view/ResolveCurrentAddressView.java b/app/src/main/java/org/walkersguide/android/ui/view/ResolveCurrentAddressView.java index db8ed49..7d57c6e 100644 --- a/app/src/main/java/org/walkersguide/android/ui/view/ResolveCurrentAddressView.java +++ b/app/src/main/java/org/walkersguide/android/ui/view/ResolveCurrentAddressView.java @@ -72,7 +72,7 @@ private void initUi(Context context) { // current address layoutCurrentAddress = new ObjectWithIdView( - context, getResources().getString(R.string.pointSelectFromClosestAddress)); + context, getResources().getString(R.string.labelPrefixClosestAddress)); layoutCurrentAddress.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); @@ -121,7 +121,7 @@ public void requestAddressForCurrentLocation() { // background task results private BroadcastReceiver localIntentReceiver = new BroadcastReceiver() { - private AcceptNewPosition acceptNewPosition = new AcceptNewPosition(50, 60, null); + private AcceptNewPosition acceptNewPosition = new AcceptNewPosition(50, 60000l, null); @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ServerTaskExecutor.ACTION_RESOLVE_COORDINATES_TASK_SUCCESSFUL) diff --git a/app/src/main/java/org/walkersguide/android/ui/view/UserAnnotationView.java b/app/src/main/java/org/walkersguide/android/ui/view/UserAnnotationView.java index 630e4a0..982f030 100644 --- a/app/src/main/java/org/walkersguide/android/ui/view/UserAnnotationView.java +++ b/app/src/main/java/org/walkersguide/android/ui/view/UserAnnotationView.java @@ -2,25 +2,15 @@ import androidx.core.view.ViewCompat; import org.walkersguide.android.ui.dialog.edit.UserAnnotationForObjectWithIdDialog; -import timber.log.Timber; import org.walkersguide.android.R; -import org.walkersguide.android.ui.view.builder.TextViewBuilder; import android.widget.LinearLayout; import android.content.Context; import android.util.AttributeSet; -import android.os.Bundle; import android.content.IntentFilter; -import org.walkersguide.android.server.ServerTaskExecutor; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.walkersguide.android.util.GlobalInstance; -import org.walkersguide.android.sensor.PositionManager; -import org.walkersguide.android.server.address.ResolveCoordinatesTask; import android.content.BroadcastReceiver; import android.content.Intent; -import org.walkersguide.android.data.object_with_id.point.point_with_address_data.StreetAddress; -import org.walkersguide.android.data.object_with_id.Point; -import org.walkersguide.android.server.address.AddressException; -import org.walkersguide.android.sensor.position.AcceptNewPosition; import org.walkersguide.android.data.ObjectWithId; import android.widget.TextView; import android.view.View; diff --git a/app/src/main/java/org/walkersguide/android/ui/view/builder/TextViewBuilder.java b/app/src/main/java/org/walkersguide/android/ui/view/builder/TextViewBuilder.java index bcaa954..8bd5a64 100644 --- a/app/src/main/java/org/walkersguide/android/ui/view/builder/TextViewBuilder.java +++ b/app/src/main/java/org/walkersguide/android/ui/view/builder/TextViewBuilder.java @@ -54,6 +54,11 @@ public TextViewBuilder setId(int id) { return this; } + public TextViewBuilder setAccessibilityDelegate(View.AccessibilityDelegate accessibilityDelegate) { + this.label.setAccessibilityDelegate(accessibilityDelegate); + return this; + } + public TextViewBuilder setContentDescription(String description) { this.label.setContentDescription(description); return this; diff --git a/app/src/main/java/org/walkersguide/android/util/Helper.java b/app/src/main/java/org/walkersguide/android/util/Helper.java index ef0361a..58e41f2 100644 --- a/app/src/main/java/org/walkersguide/android/util/Helper.java +++ b/app/src/main/java/org/walkersguide/android/util/Helper.java @@ -1,5 +1,6 @@ package org.walkersguide.android.util; +import org.walkersguide.android.BuildConfig; import org.walkersguide.android.R; import org.walkersguide.android.data.object_with_id.common.Coordinates; import timber.log.Timber; @@ -29,6 +30,16 @@ import android.speech.RecognizerIntent; import android.content.ActivityNotFoundException; import org.walkersguide.android.ui.dialog.SimpleMessageDialog; +import android.app.LocaleManager; +import android.os.LocaleList; +import java.io.FileWriter; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import androidx.annotation.RawRes; +import android.media.MediaPlayer; +import android.content.res.AssetFileDescriptor; +import android.content.res.Resources; public class Helper { @@ -132,7 +143,7 @@ public static String formatYesOrNo(boolean value) { /** - * date and time + * date, time and locale */ private static final String ISO_8601_FORMAT1 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final String ISO_8601_FORMAT2 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; @@ -147,6 +158,18 @@ public static Date parseTimestamp(String timestamp) { return null; } + @TargetApi(Build.VERSION_CODES.TIRAMISU) + public static Locale getAppLocale() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + LocaleManager localeManager = (LocaleManager) GlobalInstance.getContext().getSystemService(Context.LOCALE_SERVICE); + LocaleList appLocales = localeManager.getApplicationLocales(); + if (! appLocales.isEmpty()) { + return appLocales.get(0); + } + } + return Locale.getDefault(); + } + /** * geometry functions @@ -386,9 +409,29 @@ public static String extractSpeechRecognitionResult(int resultCode, Intent data) /** - * vibration + * sound and vibration */ - // duration constants + + public static void playSound(@RawRes int rawResId) { + MediaPlayer mediaPlayer = new MediaPlayer(); + mediaPlayer.setOnPreparedListener(MediaPlayer::start); + mediaPlayer.setOnCompletionListener(MediaPlayer::reset); + mediaPlayer.setVolume(0.3f, 0.3f); + + Resources resources = GlobalInstance.getContext().getResources(); + try (AssetFileDescriptor afd = resources.openRawResourceFd(rawResId)) { + if (afd == null) return; + mediaPlayer.setDataSource( + afd.getFileDescriptor(), + afd.getStartOffset(), + afd.getDeclaredLength()); + mediaPlayer.prepareAsync(); + } catch (IOException ignored) { + Timber.d("Could not play sound"); + } + } + + // vibration duration constants public static final long VIBRATION_DURATION_SHORT = 50; public static final long VIBRATION_DURATION_LONG = 250; // intensity constants @@ -431,4 +474,33 @@ public static void vibratePattern(long[] timings) { } } + + /** + * log to text file + */ + private static SimpleDateFormat logFileDateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.ROOT); + + public static void appendToLog(String fileName, String message) { + File file = new File( + GlobalInstance.getContext().getExternalFilesDir(null), + fileName); + if (! BuildConfig.DEBUG) return; + + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.write( + String.format( + "%1$s\n%2$s\n", + logFileDateFormat.format(new Date(System.currentTimeMillis())), + message) + ); + bw.close(); + fw.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } diff --git a/app/src/main/java/org/walkersguide/android/util/WalkersGuideService.java b/app/src/main/java/org/walkersguide/android/util/WalkersGuideService.java index ee6be42..fc65f46 100644 --- a/app/src/main/java/org/walkersguide/android/util/WalkersGuideService.java +++ b/app/src/main/java/org/walkersguide/android/util/WalkersGuideService.java @@ -712,12 +712,11 @@ private static int getPendingIntentFlags() { // location private AcceptNewPosition acceptNewValueForTrackedObjectListUpdate = AcceptNewPosition.newInstanceForObjectListUpdate(); - private AcceptNewPosition acceptNewValueForDistanceTrackingMode = new AcceptNewPosition(5, 3, null); + private AcceptNewPosition acceptNewValueForDistanceTrackingMode = new AcceptNewPosition(5, 3000l, null); @Override public void newLocation(Point point, boolean isImportant) { if (trackingMode != TrackingMode.OFF && acceptNewValueForTrackedObjectListUpdate.updatePoint(point, false, isImportant)) { - Helper.vibrateOnce(100, Helper.VIBRATION_INTENSITY_WEAK); updateTrackedObjectList(); } @@ -741,7 +740,7 @@ && shouldPointBeAddedToTheRecordedRoute(gps)) { } // device sensor data - private AcceptNewBearing acceptNewValueForBearingTrackingMode = new AcceptNewBearing(10, 0); + private AcceptNewBearing acceptNewValueForBearingTrackingMode = new AcceptNewBearing(10, 500l); @Override public void newBearing(Bearing bearing, boolean isImportant) { if (this.trackedObjectCache != null diff --git a/app/src/main/java/org/walkersguide/android/util/service/BearingTrackingMode.java b/app/src/main/java/org/walkersguide/android/util/service/BearingTrackingMode.java index ae5c7cf..7d92bf3 100644 --- a/app/src/main/java/org/walkersguide/android/util/service/BearingTrackingMode.java +++ b/app/src/main/java/org/walkersguide/android/util/service/BearingTrackingMode.java @@ -9,6 +9,7 @@ import org.walkersguide.android.data.ObjectWithId; import org.walkersguide.android.data.profile.AnnouncementRadius; import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; import org.walkersguide.android.util.GlobalInstance; import org.walkersguide.android.util.Helper; import org.walkersguide.android.util.SettingsManager; @@ -94,7 +95,8 @@ public synchronized void lookForObjectsWithinViewingDirection(TrackedObjectCache } if (messageList.isEmpty()) { - ttsWrapperInstance.stop(); + // empty string to stop the tts without interrupting more important utterances + ttsWrapperInstance.announce("", MessageType.TRACKED_OBJECT_MODE_BEARING); } else { if (! filteredObjectList.isEmpty()) { @@ -107,7 +109,7 @@ public synchronized void lookForObjectsWithinViewingDirection(TrackedObjectCache } ttsWrapperInstance.announce( - TextUtils.join(", ", messageList)); + TextUtils.join(", ", messageList), MessageType.TRACKED_OBJECT_MODE_BEARING); } this.running = false; diff --git a/app/src/main/java/org/walkersguide/android/util/service/DistanceTrackingMode.java b/app/src/main/java/org/walkersguide/android/util/service/DistanceTrackingMode.java index 1468ea6..0358548 100644 --- a/app/src/main/java/org/walkersguide/android/util/service/DistanceTrackingMode.java +++ b/app/src/main/java/org/walkersguide/android/util/service/DistanceTrackingMode.java @@ -9,6 +9,7 @@ import org.walkersguide.android.data.ObjectWithId; import org.walkersguide.android.data.profile.AnnouncementRadius; import org.walkersguide.android.tts.TTSWrapper; +import org.walkersguide.android.tts.TTSWrapper.MessageType; import org.walkersguide.android.util.GlobalInstance; import org.walkersguide.android.util.Helper; import org.walkersguide.android.util.SettingsManager; @@ -61,8 +62,8 @@ private boolean announce(ObjectWithId object, Point currentLocation) { "%1$s %2$s", object.formatNameAndSubType(), object.formatDistanceAndRelativeBearingFromCurrentLocation( - R.plurals.inMeters)) - ); + R.plurals.inMeters)), + MessageType.TRACKED_OBJECT_MODE_DISTANCE); announcedObjectBlacklist.put( object, Pair.create(currentLocation, System.currentTimeMillis())); return true; diff --git a/app/src/main/res/layout/dialog_bearing_details.xml b/app/src/main/res/layout/dialog_bearing_details.xml index 9dae99a..3936106 100644 --- a/app/src/main/res/layout/dialog_bearing_details.xml +++ b/app/src/main/res/layout/dialog_bearing_details.xml @@ -60,8 +60,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="@dimen/minHeight" - android:text="@string/buttonEnableBearingSimulation" - android:theme="@style/HeadingTextView" /> + android:text="@string/buttonEnableBearingSimulation" /> + android:text="@string/buttonEnableLocationSimulation" /> + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_navigate.xml b/app/src/main/res/layout/fragment_navigate.xml index de68b99..012e0d5 100644 --- a/app/src/main/res/layout/fragment_navigate.xml +++ b/app/src/main/res/layout/fragment_navigate.xml @@ -17,7 +17,7 @@ - - + android:focusable="true" + app:prefix="@string/labelPointDistanceAndBearing" + android:visibility="gone" /> diff --git a/app/src/main/res/layout/fragment_route_details.xml b/app/src/main/res/layout/fragment_route_details.xml index 82b1495..a26f0e0 100644 --- a/app/src/main/res/layout/fragment_route_details.xml +++ b/app/src/main/res/layout/fragment_route_details.xml @@ -10,7 +10,8 @@ android:id="@+id/labelDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:focusable="true" /> + android:focusable="true" + android:textSize="@dimen/smallTextSize" /> @@ -133,6 +134,12 @@ android:layout_height="wrap_content" android:text="@string/switchAnnouncementsEnabled" /> +