Skip to content
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

Merged
merged 18 commits into from
Nov 17, 2017

Conversation

bsudekum
Copy link
Contributor

@bsudekum bsudekum commented Nov 6, 2017

This exposes a delegate method for any failure related to voice instructions.

Todo:

  • Document.
  • Detect when the audioController is interrupted, send error.

/cc @mapbox/guidance @1ec5 @frederoni @JThramer @ericrwolfe


extension NSError {
static func withCustom(message: String) -> NSError {
return NSError(domain: "MBVoiceErrorDomain", code: -1, userInfo: [
Copy link
Contributor Author

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
Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

@bsudekum
Copy link
Contributor Author

bsudekum commented Nov 7, 2017

@1ec5 updated to use custom error codes.

@@ -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?
Copy link
Contributor

@frederoni frederoni Nov 7, 2017

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.

/**
Creates a custom `Error` object.
*/
public static func withCustom(message: String, errorCode: SpokenVoiceError = .defaultError) -> NSError {
Copy link
Contributor

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:)

Creates a custom `Error` object.
*/
public static func withCustom(message: String, errorCode: SpokenVoiceError = .defaultError) -> NSError {
return NSError(domain: "MBErrorDomain", code: errorCode.rawValue, userInfo: [
Copy link
Contributor

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.

/**
Enum used for indicating the type of error occured while speaking an instruction.
*/
public enum SpokenVoiceError: Int {
Copy link
Contributor

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.

/**
Default error.
*/
case defaultError
Copy link
Contributor

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.

/**
The audio player failed to play audio data.
*/
case audiPlayerFailedToPlay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: audio.

/**
An intruction interrupted another instruction.
*/
case overlappingInstruction
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

/**
The audio player failed to preprare to play audio data.
*/
case audioPlayerFailedToPrepare
Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1ec5 I'm not seeing a proposal for fixing this line in #624.

Copy link
Contributor

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"))
Copy link
Contributor

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).

public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) {
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [
NSLocalizedFailureReasonErrorKey: localizedFailureReason,
"MapboxErrorKey": detailedFailureReason
Copy link
Contributor Author

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?

Copy link
Contributor

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"))
Copy link
Contributor

@1ec5 1ec5 Nov 8, 2017

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.

@@ -152,3 +152,6 @@ public var RouteControllerMinimumNumberLocationUpdatesBackwards = 3
public var RouteControllerNumberOfSecondsForRerouteFeedback: TimeInterval = 10

let FasterRouteFoundEvent = "navigation.fasterRoute"

let MGLErrorDomain = "MGLErrorDomain"
Copy link
Contributor

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.

public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) {
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [
NSLocalizedFailureReasonErrorKey: localizedFailureReason,
NSLocalizedDescriptionKey: detailedFailureReason
Copy link
Contributor

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)
Copy link
Contributor

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:.

/**
An intruction interrupted another instruction.
*/
case overlappingInstruction
Copy link
Contributor

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)
Copy link
Contributor

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.

@bsudekum
Copy link
Contributor Author

@1ec5 this is ready for review again.

/**
Custom Error code key used in returned in `voiceController(_:didInterrupt:with:)`.
*/
public let SpokenInstructionErrorCodeKey = "SpokenInstructionErrorCodeKey"
Copy link
Contributor

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.



/**
Custom Error code key used in returned in `voiceController(_:didInterrupt:with:)`.
Copy link
Contributor

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.

public convenience init(localizedFailureReason: String, detailedFailureReason: String, code: MapboxNavigationError = .defaultError) {
self.init(domain: MGLErrorDomain, code: code.rawValue, userInfo: [
NSLocalizedFailureReasonErrorKey: localizedFailureReason,
SpokenInstructionErrorCodeKey: detailedFailureReason
Copy link
Contributor

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)

/**
Domain which all errors from delegate methods live under.
*/
public let MGLErrorDomain = "MGLErrorDomain"
Copy link
Contributor

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?
Copy link
Contributor

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)
Copy link
Contributor

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) {
Copy link
Contributor

@1ec5 1ec5 Nov 14, 2017

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?) {
Copy link
Contributor

@1ec5 1ec5 Nov 14, 2017

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:

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.

/**
Enum used for indicating the type of error occured while speaking an instruction.
*/
public enum MapboxNavigationError: Int {
Copy link
Contributor

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>"; };
Copy link
Contributor

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.

@bsudekum
Copy link
Contributor Author

@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.

Copy link
Contributor

@1ec5 1ec5 left a 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";
Copy link
Contributor

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.


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) {
Copy link
Contributor

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.

NSLocalizedFailureReasonErrorKey: localizedFailureReason,
SpokenInstructionErrorCodeKey: detailedFailureReason
MBSpokenInstructionErrorCode: code
Copy link
Contributor

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, and audioPlayerFailedToPlay.
  • MBErrorCode (renamed from MapboxNavigationError) should have the following values: unknown and spokenInstructionFailed.
  • One of the call sites in PollyVoiceController should call this initializer passing in spokenInstructionFailed as the code and noAwsResult as the spokenInstructionCode.

@@ -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;
Copy link
Contributor

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?
Copy link
Contributor

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"))
Copy link
Contributor

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
Copy link
Contributor

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 }
Copy link
Contributor

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)
Copy link
Contributor

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.

@bsudekum
Copy link
Contributor Author

@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.
Copy link
Contributor

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.
*/
Copy link
Contributor

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.

self.init(domain: MBErrorDomain, code: code.rawValue, userInfo: [
NSLocalizedFailureReasonErrorKey: localizedFailureReason,
MBSpokenInstructionErrorCode: code
NSLocalizedFailureReasonErrorKey: localizedFailureReason
Copy link
Contributor

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.

@@ -19,3 +19,18 @@ public enum MapboxNavigationError: Int {
*/
case noDataInSpokenInstructionResponse
Copy link
Contributor

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?

Copy link
Contributor Author

@bsudekum bsudekum Nov 16, 2017

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")
Copy link
Contributor

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) {
Copy link
Contributor

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!)
Copy link
Contributor

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";
Copy link
Contributor

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 }
Copy link
Contributor

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 }
Copy link
Contributor

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.

@1ec5
Copy link
Contributor

1ec5 commented Nov 16, 2017

Following the refactoring, PollyVoiceController should work like this:

  • RouteVoiceController.didPassSpokenInstructionPoint(_:) gets called. It consults RouteVoiceController.shouldSpeak(for:) as to whether it should continue or bail (or that logic can be inlined in didPassSpokenInstructionPoint(_:) – either way’s fine.
  • RouteVoiceController.didPassSpokenInstructionPoint(_:) calls speak(_:). Since PollyVoiceController has overridden speak(_:), PollyVoiceController.speak(_:) gets called.
  • Various code paths in PollyVoiceController.speak(_:) may call RouteVoiceController.speak(_:) via PollyVoiceController.speakWithoutPolly(_:error:).

At no point does PollyVoiceController.didPassSpokenInstructionPoint(_:) ever get called, even for PollyVoiceController.

@mcwhittemore
Copy link
Contributor

I keep forgetting to say how excited I am about this. I'm very excited! :)

@bsudekum
Copy link
Contributor Author

@1ec5 alright, I think we're there.

/**
Default error.
*/
case defaultError = -1
Copy link
Contributor

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.

/**
The audio player failed to play audio data.
*/
case spokenInstructionAudioPlayerFailedToPlay
Copy link
Contributor

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)
Copy link
Contributor

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.)

Copy link
Contributor

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>) {
Copy link
Contributor

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.

Bobby Sudekum and others added 4 commits November 16, 2017 15:51
Also adhere to best practices around overriding methods.
This method is the main customization hook for custom voice controller subclasses.
@bsudekum bsudekum merged commit a66e5aa into master Nov 17, 2017
@bsudekum bsudekum deleted the voice-fail-delegate branch November 17, 2017 00:49
@bsudekum bsudekum mentioned this pull request Nov 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants