Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,18 @@
/* Shake callouts info */
"callouts.shake_callouts.info" = "Shake the device to repeat the last callout";

/* Title for a toggle under Manage Callouts. Controls whether callouts include a leading sound effect. */
"callouts.sound_effects" = "Sound Effects";

/* Description for Manage Callouts sound effects toggle. */
"callouts.sound_effects.info" = "Sounds that play before the spoken messages";

/* Title for a toggle under Manage Callouts. Controls whether callouts pause between announcements. */
"callouts.delay_between" = "Delays";

/* Description for Manage Callouts delay toggle. */
"callouts.delay_between.info" = "Short pauses between callouts";

/* Callouts Title, Automatic Callouts */
"callouts.automatic_callouts" = "Automatic Callouts";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

protocol AutoCalloutSettingsProvider: AnyObject {
var automaticCalloutsEnabled: Bool { get set }
var calloutSoundEffectsEnabled: Bool { get set }
var calloutPausesEnabled: Bool { get set }
var placeSenseEnabled: Bool { get set }
var landmarkSenseEnabled: Bool { get set }
var mobilitySenseEnabled: Bool { get set }
Expand Down
23 changes: 23 additions & 0 deletions apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand Down Expand Up @@ -44,6 +45,8 @@ class SettingsContext {
fileprivate static let useOldBeacon = "GDASettingsUseOldBeacon"
fileprivate static let playBeaconStartEndMelody = "GDAPlayBeaconStartEndMelody"
fileprivate static let automaticCalloutsEnabled = "GDASettingsAutomaticCalloutsEnabled"
fileprivate static let calloutSoundEffectsEnabled = "GDASettingsCalloutSoundEffectsEnabled"
fileprivate static let calloutPausesEnabled = "GDASettingsCalloutPausesEnabled"
fileprivate static let shakeCalloutsEnabled = "GDASettingsShakeCalloutsEnabled"
fileprivate static let sensePlace = "GDASettingsPlaceSenseEnabled"
fileprivate static let senseLandmark = "GDASettingsLandmarkSenseEnabled"
Expand Down Expand Up @@ -99,6 +102,8 @@ class SettingsContext {
Keys.useOldBeacon: false,
Keys.playBeaconStartEndMelody: false,
Keys.automaticCalloutsEnabled: true,
Keys.calloutSoundEffectsEnabled: true,
Keys.calloutPausesEnabled: true,
Keys.shakeCalloutsEnabled: false,
Keys.sensePlace: true,
Keys.senseLandmark: true,
Expand Down Expand Up @@ -425,6 +430,24 @@ extension SettingsContext: AutoCalloutSettingsProvider {
NotificationCenter.default.post(name: .automaticCalloutsEnabledChanged, object: self, userInfo: [Keys.enabled: newValue])
}
}

var calloutSoundEffectsEnabled: Bool {
get {
return userDefaults.bool(forKey: Keys.calloutSoundEffectsEnabled)
}
set(newValue) {
userDefaults.set(newValue, forKey: Keys.calloutSoundEffectsEnabled)
}
}

var calloutPausesEnabled: Bool {
get {
return userDefaults.bool(forKey: Keys.calloutPausesEnabled)
}
set(newValue) {
userDefaults.set(newValue, forKey: Keys.calloutPausesEnabled)
}
}

var shakeCalloutsEnabled: Bool {
get {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand Down Expand Up @@ -118,7 +119,6 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator {
// MARK: - Private Constants

private let inVehicleBeaconUpdateDistance: CLLocationDistance = 1000.0 // meters
private let calloutDelay = 0.75

// MARK: - Private Properties

Expand All @@ -141,6 +141,10 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator {
}

// MARK: - Helper Properties

private var calloutDelay: TimeInterval {
return settings.calloutPausesEnabled ? 0.75 : 0.0
}

private var calloutRangeContext: CalloutRangeContext {
return spatialData.motionActivityContext.isInVehicle ? .automotive : .standard
Expand Down Expand Up @@ -435,7 +439,7 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator {

guard let dataView = spatialData.getDataView(for: location, searchDistance: context.searchDistance) else {
let callouts = filterAnnounceablePOIs(prioritizedPOIs, near: location, context: context) {
return POICallout(.auto, poi: $0, location: location)
return POICallout(.auto, poi: $0, location: location, includePrefixSound: settings.calloutSoundEffectsEnabled)
}

if callouts.isEmpty {
Expand All @@ -452,11 +456,11 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator {

// Get the POIs sorted by distance, and filtered for duplicate names
let prioritizedCallouts = filterAnnounceablePOIs(prioritizedPOIs, near: location, context: context) {
return POICallout(.auto, poi: $0, location: location)
return POICallout(.auto, poi: $0, location: location, includePrefixSound: settings.calloutSoundEffectsEnabled)
}

let defaultCallouts = !settings.automaticCalloutsEnabled ? [] : filterAnnounceablePOIs(dataView.pois, near: location, context: context) {
return POICallout(.auto, key: $0.key, location: location)
return POICallout(.auto, key: $0.key, location: location, includePrefixSound: settings.calloutSoundEffectsEnabled)
}

if defaultCallouts.count + prioritizedCallouts.count == 0 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand Down Expand Up @@ -62,7 +63,6 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator {
// MARK: - Private Constants

private let inVehicleBeaconUpdateDistance: CLLocationDistance = 1000.0 // meters
private let calloutDelay: TimeInterval = 0.75

// MARK: - Private Properties

Expand All @@ -73,6 +73,10 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator {
private var userLocation: CLLocation?
private var destinationKey: String?
private var beaconUpdateFilter: MotionActivityUpdateFilter

private var calloutDelay: TimeInterval {
return settings.calloutPausesEnabled ? 0.75 : 0.0
}

// MARK: - Initialization

Expand Down Expand Up @@ -100,7 +104,8 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator {
func handle(event: UserInitiatedEvent, verbosity: Verbosity) -> HandledEventAction? {
switch event {
case let event as BeaconCalloutEvent:
return .playCallouts(CalloutGroup([DestinationCallout(.auto, event.beaconId)], action: .interruptAndClear, logContext: event.logContext))
let callout = DestinationCallout(.auto, event.beaconId, false, settings.calloutSoundEffectsEnabled)
return .playCallouts(CalloutGroup([callout], action: .interruptAndClear, logContext: event.logContext))

case let event as BeaconChangedEvent:
guard settings.automaticCalloutsEnabled else {
Expand Down Expand Up @@ -302,7 +307,7 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator {
configureDestinationUpdates()

// Build the destination callout
return [DestinationCallout(origin, key, causedAudioDisable)]
return [DestinationCallout(origin, key, causedAudioDisable, settings.calloutSoundEffectsEnabled)]
}

// MARK: - Helper Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand All @@ -23,7 +24,7 @@ struct DestinationCallout: POICalloutProtocol {
return "destination"
}

let includePrefixSound = true
let includePrefixSound: Bool

var prefixSound: Sound? {
return GlyphSound(.startJourney)
Expand Down Expand Up @@ -52,10 +53,11 @@ struct DestinationCallout: POICalloutProtocol {
return marker?.getPOI()
}

init(_ calloutOrigin: CalloutOrigin, _ entityKey: String, _ causedAudioDisabled: Bool = false) {
init(_ calloutOrigin: CalloutOrigin, _ entityKey: String, _ causedAudioDisabled: Bool = false, _ includePrefixSound: Bool = true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require explicit includePrefixSound for destination callouts.

Line 56 defaults to true, so callers can omit the new setting. PreviewGenerator.swift (Line 100) currently does that, which keeps preview prefix sounds enabled regardless of the Sound Effects toggle.

Suggested fix
-    init(_ calloutOrigin: CalloutOrigin, _ entityKey: String, _ causedAudioDisabled: Bool = false, _ includePrefixSound: Bool = true) {
+    init(_ calloutOrigin: CalloutOrigin, _ entityKey: String, _ causedAudioDisabled: Bool = false, _ includePrefixSound: Bool) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift`
at line 56, The initializer for DestinationCallout currently has a defaulted
parameter includePrefixSound = true allowing callers to omit it; remove the
default so the init signature becomes init(_ calloutOrigin: CalloutOrigin, _
entityKey: String, _ causedAudioDisabled: Bool = false, _ includePrefixSound:
Bool) to force callers to pass the setting explicitly, then update all call
sites (notably where PreviewGenerator constructs DestinationCallout) to pass the
correct includePrefixSound value based on the Sound Effects toggle or preview
behavior.

self.origin = calloutOrigin
self.entityKey = entityKey
self.causedAudioDisabled = causedAudioDisabled
self.includePrefixSound = includePrefixSound
}

func hasSameEntity(_ rhs: POICallout) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand All @@ -25,7 +26,7 @@ struct IntersectionCallout: CalloutProtocol {
return "intersection"
}

let includePrefixSound = true
let includePrefixSound: Bool

var prefixSound: Sound? {
return GlyphSound(SuperCategory.intersections.glyph)
Expand All @@ -38,11 +39,12 @@ struct IntersectionCallout: CalloutProtocol {
return SpatialDataCache.intersectionByKey(key)
}

init(_ calloutOrigin: CalloutOrigin, _ intersectionKey: String, _ isRoundabout: Bool = false, _ userHeading: CLLocationDirection) {
init(_ calloutOrigin: CalloutOrigin, _ intersectionKey: String, _ isRoundabout: Bool = false, _ userHeading: CLLocationDirection, _ includePrefixSound: Bool = true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make includePrefixSound explicit to prevent settings bypass.

Line 42 sets a default true, which lets call sites skip passing the user preference. RouteGuidanceGenerator.swift (Line 209) and TourGenerator.swift (Line 217) still use the 4-arg initializer, so intersection prefix sounds stay enabled there even when Sound Effects is off.

Suggested fix
-    init(_ calloutOrigin: CalloutOrigin, _ intersectionKey: String, _ isRoundabout: Bool = false, _ userHeading: CLLocationDirection, _ includePrefixSound: Bool = true) {
+    init(_ calloutOrigin: CalloutOrigin, _ intersectionKey: String, _ isRoundabout: Bool = false, _ userHeading: CLLocationDirection, _ includePrefixSound: Bool) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/IntersectionCallout.swift`
at line 42, The initializer for IntersectionCallout currently has a default true
for includePrefixSound which lets callers omit the argument and bypass user
settings; change the init signature in IntersectionCallout (init(_: _:_:_:_:_))
to remove the default so includePrefixSound is a required Bool, then update the
call sites in RouteGuidanceGenerator and TourGenerator (the places using the
4-arg initializer) to pass the explicit user preference (the Sound Effects
setting) when constructing IntersectionCallout so prefix sounds respect the
user's setting.

self.origin = calloutOrigin
self.key = intersectionKey
self.isRoundabout = isRoundabout
self.heading = userHeading
self.includePrefixSound = includePrefixSound
}

func sounds(for location: CLLocation?, isRepeat: Bool, automotive: Bool = false) -> Sounds {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand Down Expand Up @@ -136,7 +137,11 @@ class IntersectionGenerator: AutomaticGenerator {
return .noAction

case let event as IntersectionArrivalEvent:
let callout = IntersectionCallout(.intersection, event.key, event.isRoundabout, event.heading)
let callout = IntersectionCallout(.intersection,
event.key,
event.isRoundabout,
event.heading,
SettingsContext.shared.calloutSoundEffectsEnabled)

GDATelemetry.track("callout", with: ["context": "intersection.arrival",
"type": callout.logCategory,
Expand All @@ -146,7 +151,9 @@ class IntersectionGenerator: AutomaticGenerator {
return .playCallouts(CalloutGroup([callout], action: .interruptAndClear, logContext: "intersections"))

case let event as IntersectionDepartureEvent:
let callout = event.geocodedLocation.buildCallout(origin: .intersection, sound: true, useClosestRoadIfAvailable: true)
let callout = event.geocodedLocation.buildCallout(origin: .intersection,
sound: SettingsContext.shared.calloutSoundEffectsEnabled,
useClosestRoadIfAvailable: true)
return .playCallouts(CalloutGroup([callout], action: .enqueue, logContext: "intersections"))

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Soundscape
//
// Copyright (c) Microsoft Corporation.
// Copyright (c) Soundscape Community Contributors.
// Licensed under the MIT License.
//

Expand All @@ -13,7 +14,7 @@ protocol CalloutSettingsCellViewDelegate: AnyObject {
}

internal enum CalloutSettingCellType {
case all, poi, mobility, beacon, shake
case all, poi, mobility, beacon, shake, soundEffects, delays
}

class CalloutSettingsCellView: UITableViewCell {
Expand Down Expand Up @@ -44,6 +45,12 @@ class CalloutSettingsCellView: UITableViewCell {
return
case .shake:
settingSwitch.isOn = SettingsContext.shared.shakeCalloutsEnabled
return
case .soundEffects:
settingSwitch.isOn = SettingsContext.shared.calloutSoundEffectsEnabled
return
case .delays:
settingSwitch.isOn = SettingsContext.shared.calloutPausesEnabled
}
}
}
Expand Down Expand Up @@ -97,6 +104,14 @@ class CalloutSettingsCellView: UITableViewCell {
case .shake:
SettingsContext.shared.shakeCalloutsEnabled = isOn
GDATelemetry.track("settings.shake_callouts", value: isOn.description)
return
case .soundEffects:
SettingsContext.shared.calloutSoundEffectsEnabled = isOn
GDATelemetry.track("settings.callout_sound_effects", value: isOn.description)
return
case .delays:
SettingsContext.shared.calloutPausesEnabled = isOn
GDATelemetry.track("settings.callout_delay", value: isOn.description)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ class SettingsViewController: BaseTableViewController {

private enum CalloutsRow: Int, CaseIterable {
case all = 0
case poi = 1
case mobility = 2
case beacon = 3
case shake = 4
case soundEffects = 1
case delays = 2
case poi = 3
case mobility = 4
case beacon = 5
case shake = 6
}

private static let cellIdentifiers: [IndexPath: String] = [
Expand All @@ -51,6 +53,8 @@ class SettingsViewController: BaseTableViewController {
IndexPath(row: CalloutsRow.mobility.rawValue, section: Section.callouts.rawValue): "mobilityCallouts",
IndexPath(row: CalloutsRow.beacon.rawValue, section: Section.callouts.rawValue): "beaconCallouts",
IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue): "shakeCallouts",
IndexPath(row: CalloutsRow.soundEffects.rawValue, section: Section.callouts.rawValue): "soundCallouts",
IndexPath(row: CalloutsRow.delays.rawValue, section: Section.callouts.rawValue): "delayCallouts",

IndexPath(row: 0, section: Section.streetPreview.rawValue): "streetPreview",
IndexPath(row: 0, section: Section.troubleshooting.rawValue): "troubleshooting",
Expand All @@ -62,7 +66,9 @@ class SettingsViewController: BaseTableViewController {
IndexPath(row: CalloutsRow.poi.rawValue, section: Section.callouts.rawValue),
IndexPath(row: CalloutsRow.mobility.rawValue, section: Section.callouts.rawValue),
IndexPath(row: CalloutsRow.beacon.rawValue, section: Section.callouts.rawValue),
IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue)
IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue),
IndexPath(row: CalloutsRow.soundEffects.rawValue, section: Section.callouts.rawValue),
IndexPath(row: CalloutsRow.delays.rawValue, section: Section.callouts.rawValue)
]

// MARK: Properties
Expand Down Expand Up @@ -91,7 +97,7 @@ class SettingsViewController: BaseTableViewController {
switch sectionType {
case .general: return 6
case .audio: return 1
case .callouts: return SettingsContext.shared.automaticCalloutsEnabled ? 5 : 1
case .callouts: return SettingsContext.shared.automaticCalloutsEnabled ? 7 : 1
case .streetPreview: return 1
case .troubleshooting: return 1
case .about: return 1
Expand All @@ -118,6 +124,8 @@ class SettingsViewController: BaseTableViewController {
case .mobility: cell.type = .mobility
case .beacon: cell.type = .beacon
case .shake: cell.type = .shake
case .soundEffects: cell.type = .soundEffects
case .delays: cell.type = .delays
}
}

Expand Down
Loading