-
Notifications
You must be signed in to change notification settings - Fork 310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose delegate method for failed voice instructions #800
Conversation
|
||
extension NSError { | ||
static func withCustom(message: String) -> NSError { | ||
return NSError(domain: "MBVoiceErrorDomain", code: -1, userInfo: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any improvements that could be made to this object?
@@ -29,7 +29,7 @@ public class PollyVoiceController: RouteVoiceController { | |||
/** | |||
Number of seconds a Polly request can wait before it is canceled and the default speech synthesizer speaks the instruction. | |||
*/ | |||
public var timeoutIntervalForRequest:TimeInterval = 2 | |||
public var timeoutIntervalForRequest:TimeInterval = 5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also going to bump this up. 2 seconds is just too short.
@@ -208,3 +210,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP | |||
speechSynth.speak(utterance) | |||
} | |||
} | |||
|
|||
@objc public protocol VoiceControllerDelegate { | |||
@objc optional func spokenInstructionsDidFail(_ voiceController: RouteVoiceController, error: Error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it failure if we fall back to the system voice, or is it a fallback situation? Perhaps we should scope this protocol more tightly to be just about PollyVoiceController: PollyVoiceController failing is the same thing as PollyVoiceController falling back to VoiceController.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another case to think about though is for overlapping instructions. We're not falling back, but it is an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, i think you're right. Going to move to pollyvoicecontroller
@1ec5 updated to use custom error codes. |
76bece5
to
d78edaf
Compare
@@ -58,14 +58,19 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP | |||
public var bufferBetweenAnnouncements: TimeInterval = 3 | |||
|
|||
/** | |||
Delegate used for getting metadata information about a particular spoken instruction. | |||
*/ | |||
public var voiceControllerDelegate: VoiceControllerDelegate? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a weak var to avoid a retain cycle. We can also rename this property to delegate
.
MapboxNavigation/Error.swift
Outdated
/** | ||
Creates a custom `Error` object. | ||
*/ | ||
public static func withCustom(message: String, errorCode: SpokenVoiceError = .defaultError) -> NSError { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this method an initializer instead: NSError(localizedFailureReason:code:)
MapboxNavigation/Error.swift
Outdated
Creates a custom `Error` object. | ||
*/ | ||
public static func withCustom(message: String, errorCode: SpokenVoiceError = .defaultError) -> NSError { | ||
return NSError(domain: "MBErrorDomain", code: errorCode.rawValue, userInfo: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull this domain out into a global constant, along the lines of the map SDK’s MGLErrorDomain
.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
Enum used for indicating the type of error occured while speaking an instruction. | ||
*/ | ||
public enum SpokenVoiceError: Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a single enumeration with all the error codes we may need to use in this library, similar to the map SDK’s MGLErrorCode
.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
Default error. | ||
*/ | ||
case defaultError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default case should be unknown
, set to −1.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
The audio player failed to play audio data. | ||
*/ | ||
case audiPlayerFailedToPlay |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: audio.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
An intruction interrupted another instruction. | ||
*/ | ||
case overlappingInstruction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really an error? Unlike the other cases, one instruction interrupting another instruction can be completely expected, like when the user triggers a reroute while an instruction is being read aloud.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to know when something like this happens, as it shouldn't. Would you rather have this filed under general?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I’d rather that overlapping instructions not result in an “error” at all, but it would be fine to have a dedicated delegate method indicating that an instruction was interrupted before completion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a dedicated delegate method indicating that an instruction was interrupted before completion
For example, voiceController(_:didInterrupt:with:)
, taking two SpokenInstruction objects.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
The audio player failed to preprare to play audio data. | ||
*/ | ||
case audioPlayerFailedToPrepare |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of these cases are specific to PollyVoiceController (or at least are inapplicable to RouteVoiceController). Perhaps we can generalize these cases so they can be used for more kinds of voice controllers in the future.
// Note why it failed | ||
if let error = error { | ||
print(error) | ||
voiceControllerDelegate?.spokenInstructionsDidFail?(self, error: error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to reapply the refactor that was proposed in #624: the caller should be responsible for handling errors. Making this method the bottleneck for error reporting is a premature optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return | ||
} | ||
|
||
guard let url = awsTask.result else { | ||
callSuperSpeak(fallbackText, error: "No polly response") | ||
callSuperSpeak(fallbackText, error: NSError.withCustom(message: "No AWS Task")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
localizedFailureReason
is meant to be human-readable and understandable by the end user, in case the developer presents the error via an alert box (not required). The best practice would be to make the NSError’s error code something generic, like spokenInstructionFailed
, and its localized failure reason also vague, like “Unable to read aloud the instruction ‘…’.” Then we can add another field to the NSError’s user info dictionary that indicates exactly what went wrong (in this case, an AWS failure).
MapboxNavigation/Error.swift
Outdated
public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) { | ||
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [ | ||
NSLocalizedFailureReasonErrorKey: localizedFailureReason, | ||
"MapboxErrorKey": detailedFailureReason |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@1ec5 What should this key be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSLocalizedDescriptionKey
@@ -189,7 +189,7 @@ public class PollyVoiceController: RouteVoiceController { | |||
} | |||
|
|||
guard let url = awsTask.result else { | |||
callSuperSpeak(fallbackText, error: NSError.withCustom(message: "No AWS Task")) | |||
callSuperSpeak(fallbackText, error: NSError(localizedFailureReason: "Unable to read instruction aloud", detailedFailureReason: "No AWS Task")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant in #800 (comment) is that this error would contain:
NSLocalizedFailureReasonErrorKey
:Unable to say “In 5 miles, turn left onto Main Street”
SpokenInstructionErrorCodeKey
:SpokenInstructionErrorCode.noAwsTask
The application can then do something different by switching on SpokenInstructionErrorCodeKey rather than doing string comparisons.
9f1f165
to
1095b51
Compare
MapboxCoreNavigation/Constants.swift
Outdated
@@ -152,3 +152,6 @@ public var RouteControllerMinimumNumberLocationUpdatesBackwards = 3 | |||
public var RouteControllerNumberOfSecondsForRerouteFeedback: TimeInterval = 10 | |||
|
|||
let FasterRouteFoundEvent = "navigation.fasterRoute" | |||
|
|||
let MGLErrorDomain = "MGLErrorDomain" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constant needs to be public so developers can conditionalize logic in exception handlers based on it. It also needs a little documentation.
MapboxNavigation/Error.swift
Outdated
public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) { | ||
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [ | ||
NSLocalizedFailureReasonErrorKey: localizedFailureReason, | ||
NSLocalizedDescriptionKey: detailedFailureReason |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -207,3 +212,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP | |||
speechSynth.speak(utterance) | |||
} | |||
} | |||
|
|||
@objc public protocol VoiceControllerDelegate { | |||
@objc optional func spokenInstructionsDidFail(_ voiceController: RouteVoiceController, error: Error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delegate methods should always accept the delegator object as the first parameter and be named based on the delegator object. So this method should be voiceController(_:spokenInstructionsDidFailWith:)
. Also, the Objective-C signature needs to include the nouns: -voiceController:spokenInstructionsDidFailWithError:
.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
An intruction interrupted another instruction. | ||
*/ | ||
case overlappingInstruction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a dedicated delegate method indicating that an instruction was interrupted before completion
For example, voiceController(_:didInterrupt:with:)
, taking two SpokenInstruction objects.
print("Voice guidance may not work properly. " + | ||
"Add audio to the UIBackgroundModes key to your app’s Info.plist file") | ||
let errorMessage = "Voice guidance may not work properly. Add audio to the UIBackgroundModes key to your app’s Info.plist file" | ||
voiceControllerDelegate?.spokenInstructionsDidFail?(self, error: NSError(localizedFailureReason: "Unable to read instruction aloud", detailedFailureReason: errorMessage, code: .audioBackgroundModeMissing)) ?? print(errorMessage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should make this flag a hard requirement for the navigation SDK and maybe have it fail earlier, when creating the NavigationViewController. At least make it an assertion, because the user should be able to expect to hear the same spoken instructions while the application is in the background.
938dc54
to
cccbb22
Compare
@1ec5 this is ready for review again. |
MapboxNavigation/Error.swift
Outdated
/** | ||
Custom Error code key used in returned in `voiceController(_:didInterrupt:with:)`. | ||
*/ | ||
public let SpokenInstructionErrorCodeKey = "SpokenInstructionErrorCodeKey" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A global constant that’s accessible to Objective-C needs to use the class prefix MB
in both its name and value. Additionally, the word “Key” should only appear in the name of the constant, not its value.
MapboxNavigation/Error.swift
Outdated
|
||
|
||
/** | ||
Custom Error code key used in returned in `voiceController(_:didInterrupt:with:)`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fully qualify the reference to this method name so that jazzy can autolink it.
MapboxNavigation/Error.swift
Outdated
public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) { | ||
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [ | ||
NSLocalizedFailureReasonErrorKey: localizedFailureReason, | ||
SpokenInstructionErrorCodeKey: detailedFailureReason |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MBSpokenInstructionErrorCodeKey
should be set to an enumeration that a developer can easily switch
on, not a string.
/ref #800 (comment)
MapboxCoreNavigation/Constants.swift
Outdated
/** | ||
Domain which all errors from delegate methods live under. | ||
*/ | ||
public let MGLErrorDomain = "MGLErrorDomain" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MGLErrorDomain
is the name of the constant used by the map SDK. Our non-GL libraries use MB
as the class prefix, not MGL
.
@@ -46,6 +46,8 @@ public class PollyVoiceController: RouteVoiceController { | |||
|
|||
var spokenInstructionsForRoute = NSCache<NSString, NSData>() | |||
|
|||
var previousInstruction: String? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this property to lastSpokenInstruction
and set it to an Instruction, not just its SSML text.
// Note why it failed | ||
if let error = error { | ||
print(error) | ||
voiceControllerDelegate?.voiceController?(self, spokenInstructionsDidFailWith: error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be the responsibility of the caller of this method to deal with any error that is being passed in. This method should only be responsible for handling errors that arise in the execution of this method.
/ref #800 (comment)
@@ -166,7 +172,7 @@ public class PollyVoiceController: RouteVoiceController { | |||
} | |||
} | |||
|
|||
func callSuperSpeak(_ text: String, error: String) { | |||
func callSuperSpeak(_ text: String, error: Error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this method to speakWithoutPolly(_:)
and have it take a single SpokenInstruction object.
(As to why, keep reading: #800 (comment).)
@@ -181,16 +185,16 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP | |||
return true | |||
} | |||
|
|||
func speak(_ text: String, error: String? = nil) { | |||
func speak(_ text: String, error: Error?) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As part of #624, I proposed renaming this method:
open func speak(for routeProgress: RouteProgress, userDistance: CLLocationDistance) { |
and having alertLevelDidChange(_:)
set up the input data for this method instead of storing the input data on instance properties:
mapbox-navigation-ios/MapboxNavigation/RouteVoiceController.swift
Lines 160 to 165 in 2097f6d
func alertLevelDidChange(notification: NSNotification) { | |
guard shouldSpeak(for: notification) else { return } | |
let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress | |
let userDistance = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey] as! CLLocationDistance | |
speak(for: routeProgress, userDistance: userDistance) |
PollyVoiceController overrides speak(for:userDistance:)
instead of alertLevelDidChange(_:)
:
public override func speak(for routeProgress: RouteProgress, userDistance: CLLocationDistance) { |
Of course, this library no longer generates the utterances directly from the route progress and current user distance, so speak(_:)
can take just a SpokenInstruction.
Overall, PollyVoiceController and RouteVoiceController end up being a lot easier to understand to those who haven’t been involved in writing these classes.
MapboxNavigation/ErrorCodes.swift
Outdated
/** | ||
Enum used for indicating the type of error occured while speaking an instruction. | ||
*/ | ||
public enum MapboxNavigationError: Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this enumeration to MBErrorCode
for consistency with MGLErrorCode
in the map SDK.
@@ -479,6 +482,7 @@ | |||
C5ADFBF91DDCC9580011824B /* RouteController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteController.swift; sourceTree = "<group>"; }; | |||
C5ADFBFB1DDCC9AD0011824B /* RouteProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteProgress.swift; sourceTree = "<group>"; }; | |||
C5CFE4871EF2FD4C006F48E8 /* MMEEventsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MMEEventsManager.swift; sourceTree = "<group>"; }; | |||
C5D1C9931FB236900067C619 /* ErrorCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCodes.swift; sourceTree = "<group>"; }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this file to ErrorCode.swift for consistency with the MBErrorCode
enumeration name.
@1ec5 refactored a bit with your feedback. I also removed the overlap timers in https://github.com/mapbox/mapbox-navigation-ios/pull/800/commits/8689cc99856e7c90e16fb2f7abe6b8e9fd85f74e- this is the servers job now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for undertaking the proposed refactor. Some details still need to be refined, particularly around the creation of NSError objects, but this PR is looking much better.
@@ -17,3 +17,5 @@ | |||
NSString *const MBRouteControllerDidReroute = @"RouteControllerDidReroute"; | |||
NSString *const MBRouteControllerDidFailToReroute = @"RouteControllerDidFailToReroute"; | |||
NSString *const MBRouteControllerDidFindFasterRouteKey = @"RouteControllerDidFindFasterRoute"; | |||
NSString *const MBErrorDomain = @"ErrorDomain"; | |||
NSString *const MBSpokenInstructionErrorCode = @"SpokenInstructionErrorCodeKey"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add “Key” to the name of the constant and remove it from the constant’s value.
MapboxNavigation/Error.swift
Outdated
|
||
extension NSError { | ||
/** | ||
Creates a custom `Error` object. | ||
*/ | ||
public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) { | ||
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [ | ||
public convenience init(localizedFailureReason: String, code: MapboxNavigationError = .defaultError) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This initializer is only useful to this SDK’s code, so make it private if possible.
MapboxNavigation/Error.swift
Outdated
NSLocalizedFailureReasonErrorKey: localizedFailureReason, | ||
SpokenInstructionErrorCodeKey: detailedFailureReason | ||
MBSpokenInstructionErrorCode: code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for not being clearer in #800 (comment), #800 (comment), and #800 (comment):
- This method should accept a
spokenInstructionCode
parameter of type SpokenInstructionErrorCode. - SpokenInstructionErrorCode should be an enumeration with the following values:
noAwsResult
,emptyAwsResponse
, andaudioPlayerFailedToPlay
. - MBErrorCode (renamed from MapboxNavigationError) should have the following values:
unknown
andspokenInstructionFailed
. - One of the call sites in PollyVoiceController should call this initializer passing in
spokenInstructionFailed
as thecode
andnoAwsResult
as thespokenInstructionCode
.
@@ -17,3 +17,5 @@ extern NSString *const MBRouteControllerWillReroute; | |||
extern NSString *const MBRouteControllerDidReroute; | |||
extern NSString *const MBRouteControllerDidFailToReroute; | |||
extern NSString *const MBRouteControllerDidFindFasterRouteKey; | |||
extern NSString *const MBErrorDomain; | |||
extern NSString *const MBSpokenInstructionErrorCode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These constants (like all the others here) need documentation comments so that they appear in the jazzy documentation.
@@ -46,7 +47,7 @@ public class PollyVoiceController: RouteVoiceController { | |||
|
|||
var spokenInstructionsForRoute = NSCache<NSString, NSData>() | |||
|
|||
var previousInstruction: String? | |||
var previousInstruction: SpokenInstruction? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this property to lastSpokenInstruction
. Between spoken instructions, it makes more sense to talk about the last instruction that was said than the “previous” one.
return | ||
} | ||
|
||
guard let url = awsTask.result else { | ||
callSuperSpeak(fallbackText, error: NSError(localizedFailureReason: "Unable to read instruction aloud", detailedFailureReason: "No AWS Task")) | ||
speakWithoutPolly(fallbackInstruction, error: NSError(localizedFailureReason: "Unable to read instruction aloud")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Localized failure reasons should be localizable.
@@ -180,24 +167,19 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP | |||
// Set recentlyAnnouncedRouteStep to the current step | |||
recentlyAnnouncedRouteStep = routeProgress.currentLegProgress.currentStep | |||
|
|||
fallbackText = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction?.text | |||
fallbackInstruction = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this line and remove the fallbackInstruction
property. From now on, it should be the responsibility of alertLevelDidChange(_:)
to pass currentSpokenInstruction
directly into speak(_:)
.
@@ -68,14 +69,13 @@ public class PollyVoiceController: RouteVoiceController { | |||
guard shouldSpeak(for: notification) == true else { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move everything below here into speak(_:)
and delete this method implementation, including this guard
statement. It should no longer be necessary for PollyVoiceController to override this method.
@@ -207,3 +211,8 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP | |||
speechSynth.speak(utterance) | |||
} | |||
} | |||
|
|||
@objc public protocol VoiceControllerDelegate { | |||
@objc optional func voiceController(_ voiceController: RouteVoiceController, spokenInstructionsDidFailWith error: Error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method still needs a proper Objective-C signature.
@1ec5 this is ready for review again. |
@@ -17,5 +17,8 @@ extern NSString *const MBRouteControllerWillReroute; | |||
extern NSString *const MBRouteControllerDidReroute; | |||
extern NSString *const MBRouteControllerDidFailToReroute; | |||
extern NSString *const MBRouteControllerDidFindFasterRouteKey; | |||
|
|||
/** | |||
Constant representing the namespace in which errors created in this library will live under. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/namespace/domain/
|
||
/** | ||
Domain for errors created within this framework. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merge this comment with the one in the header. A public Objective-C symbol should be documented only on the declaration (in the header, in this case), not on the definition.
MapboxNavigation/Error.swift
Outdated
self.init(domain: MBErrorDomain, code: code.rawValue, userInfo: [ | ||
NSLocalizedFailureReasonErrorKey: localizedFailureReason, | ||
MBSpokenInstructionErrorCode: code | ||
NSLocalizedFailureReasonErrorKey: localizedFailureReason |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spokenInstructionCode
appears to be unused. Add an entry here that sets MBSpokenInstructionErrorCodeKey
to spokenInstructionCode
, but only if it’s provided.
MapboxNavigation/ErrorCode.swift
Outdated
@@ -19,3 +19,18 @@ public enum MapboxNavigationError: Int { | |||
*/ | |||
case noDataInSpokenInstructionResponse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename noDataInSpokenInstructionResponse
to emptyAwsResponse
, since it’s assumed that any case in this enumeration is about spoken instructions.
Previous iterations of this PR also had error codes for noAwsResult
and audioPlayerFailedToPlay
. Do we still need them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, .spokenInstructionAudioPlayerFailedToPlay
are is used.
var previousInstruction: SpokenInstruction? | ||
var lastSpokenInstruction: SpokenInstruction? | ||
|
||
let localizedErrorMessage = NSLocalizedString("FAILED_INSTRUCTION", bundle: .mapboxNavigation, value: "Unable to read instruction aloud", comment: "Unable to read instruction aloud") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment should explain to translators how this string is used:
Error message when the SDK is unable to read a spoken instruction.
@@ -66,17 +68,15 @@ public class PollyVoiceController: RouteVoiceController { | |||
} | |||
|
|||
public override func didPassSpokenInstructionPoint(notification: NSNotification) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PollyVoiceController should override speak(_:)
, but should have no reason to override didPassSpokenInstructionPoint(_:)
. Move everything from this method into speak(_:)
, except for the statements that are also in RouteVoiceController.didPassSpokenInstructionPoint(_:)
. Then remove this method override. The implementation in RouteVoiceController.didPassSpokenInstructionPoint(_:)
will get called on PollyVoiceController whenever we pass a spoken instruction point.
@@ -177,23 +178,23 @@ public class PollyVoiceController: RouteVoiceController { | |||
voiceControllerDelegate?.voiceController?(self, spokenInstructionsDidFailWith: error) | |||
|
|||
guard let audioPlayer = audioPlayer else { | |||
super.speak(fallbackInstruction) | |||
super.speak(lastSpokenInstruction!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass in instruction
instead of lastSpokenInstuction
. Ditto below.
@@ -1,6 +1,9 @@ | |||
/* Dismiss button title on the steps view */ | |||
"DISMISS_STEPS_TITLE" = "Close"; | |||
|
|||
/* Unable to read instruction aloud */ | |||
"FAILED_INSTRUCTION" = "Unable to read instruction aloud"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A human-readable error message should read like a complete sentence, so it should end with a period.
} | ||
|
||
func shouldSpeak(for notification: NSNotification) -> Bool { | ||
guard isEnabled, volume > 0, !NavigationSettings.shared.muted else { return false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would’ve been fine to keep shouldSpeak(for:)
around, but in any case, make sure these guard
statements are in RouteVoiceController.didPassSpokenInstructionPoint(_:)
, not PollyVoiceController.didPassSpokenInstructionPoint(_:)
– the latter method should go away.
} | ||
|
||
func speak(_ instruction: SpokenInstruction) { | ||
guard isEnabled, volume > 0, !NavigationSettings.shared.muted else { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this statement up to didPassSpokenInstructionPoint(_:)
or keep shouldSpeak(for:)
around.
Following the refactoring, PollyVoiceController should work like this:
At no point does |
I keep forgetting to say how excited I am about this. I'm very excited! :) |
@1ec5 alright, I think we're there. |
MapboxNavigation/ErrorCode.swift
Outdated
/** | ||
Default error. | ||
*/ | ||
case defaultError = -1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unknown
, for consistency with MBErrorCode.unknown
.
MapboxNavigation/ErrorCode.swift
Outdated
/** | ||
The audio player failed to play audio data. | ||
*/ | ||
case spokenInstructionAudioPlayerFailedToPlay |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spokenInstruction
is redundant here, because it’s part of an enumeration that contains SpokenInstruction
in its name. Rename this case to audioPlayerFailedToPlay
.
speak(instruction, error: nil) | ||
guard isEnabled, volume > 0, !NavigationSettings.shared.muted else { return } | ||
|
||
super.speak(instruction) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes RouteVoiceController.speak(_:)
to be called, so PollyVoiceController.speak(_:)
never gets called.
An override should normally call the superclass’s implementation. So call super.didPassSpokenInstructionPoint(_:)
. That will allow RouteVoiceController.didPassSpokenInstructionPoint(_:)
to determine which instruction to pass into PollyVoiceController.speak(_:)
.
If that back-and-forth concerns you, bring back RouteVoiceController.shouldSpeak(for:)
and move this prefecthing logic to an override of that method, PollyVoiceController.shouldSpeak(for:)
. (The override can call the super implementation after performing the prefetching.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in a84a28f.
return | ||
} | ||
|
||
guard !audioPlayer.isPlaying else { return } | ||
|
||
super.speak(fallbackText, error: error) | ||
super.speak(instruction) | ||
} | ||
|
||
func handle(_ awsTask: AWSTask<NSURL>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass instruction
into this method, so that it doesn’t need to use lastSpokenInstruction
. This keeps the class minimally dependent on instance properties.
Also adhere to best practices around overriding methods.
This method is the main customization hook for custom voice controller subclasses.
This exposes a delegate method for any failure related to voice instructions.
Todo:
/cc @mapbox/guidance @1ec5 @frederoni @JThramer @ericrwolfe