diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj
index 182f0ea8..b0a8eb6a 100644
--- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj
+++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj
@@ -6472,7 +6472,7 @@
CODE_SIGN_ENTITLEMENTS = GuideDogs/Assets/PropertyLists/SoundscapeDF.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = "";
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES;
ENABLE_BITCODE = NO;
@@ -6503,7 +6503,7 @@
"$(inherited)",
"$(PROJECT_DIR)/GuideDogs",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) -DADHOC";
PRODUCT_BUNDLE_IDENTIFIER = "services.soundscape-adhoc";
@@ -6533,6 +6533,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = X4H33NKGKY;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@@ -6567,6 +6568,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = X4H33NKGKY;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@@ -6598,6 +6600,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = X4H33NKGKY;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@@ -6758,7 +6761,7 @@
CODE_SIGN_ENTITLEMENTS = GuideDogs/Assets/PropertyLists/Soundscape.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 19;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = X4H33NKGKY;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES;
@@ -6790,7 +6793,7 @@
"$(inherited)",
"$(PROJECT_DIR)/GuideDogs",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) -DDEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "services.soundscape-debug";
@@ -6817,11 +6820,9 @@
CLANG_STATIC_ANALYZER_MODE = deep;
CODE_SIGN_ENTITLEMENTS = GuideDogs/Assets/PropertyLists/Soundscape.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
- CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
- DEVELOPMENT_TEAM = "";
- "DEVELOPMENT_TEAM[sdk=iphoneos*]" = X4H33NKGKY;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 19;
+ DEVELOPMENT_TEAM = X4H33NKGKY;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES;
ENABLE_BITCODE = NO;
FILE_SHARING_ENABLED = NO;
@@ -6850,13 +6851,12 @@
"$(inherited)",
"$(PROJECT_DIR)/GuideDogs",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) -DRELEASE";
PRODUCT_BUNDLE_IDENTIFIER = services.soundscape;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore services.soundscape 1691940011";
RUN_CLANG_STATIC_ANALYZER = YES;
SDKROOT = iphoneos;
SWIFT_OBJC_BRIDGING_HEADER = "GuideDogs/Code/App/Soundscape-Bridging-Header.h";
diff --git a/apps/ios/GuideDogs/Assets/PropertyLists/Info.plist b/apps/ios/GuideDogs/Assets/PropertyLists/Info.plist
index 51e2cc03..8541e686 100644
--- a/apps/ios/GuideDogs/Assets/PropertyLists/Info.plist
+++ b/apps/ios/GuideDogs/Assets/PropertyLists/Info.plist
@@ -80,7 +80,7 @@
CFBundleSpokenName
${BUNDLE_SPOKEN_NAME}
CFBundleVersion
- 7
+ ${CURRENT_PROJECT_VERSION}
ITSAppUsesNonExemptEncryption
LSApplicationCategoryType
@@ -156,7 +156,6 @@
UIStatusBarStyleLightContent
UISupportedInterfaceOrientations
- UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationPortrait
UISupportedInterfaceOrientations~ipad
diff --git a/apps/ios/GuideDogs/Code/App/App Delegate/User Activities/Universal Links/Components/UniversalLinkComponents.swift b/apps/ios/GuideDogs/Code/App/App Delegate/User Activities/Universal Links/Components/UniversalLinkComponents.swift
index ae9d79e7..297af555 100644
--- a/apps/ios/GuideDogs/Code/App/App Delegate/User Activities/Universal Links/Components/UniversalLinkComponents.swift
+++ b/apps/ios/GuideDogs/Code/App/App Delegate/User Activities/Universal Links/Components/UniversalLinkComponents.swift
@@ -31,7 +31,7 @@ struct UniversalLinkComponents {
// Add query items
components.queryItems = queryItems
- return components.url
+ return components.url
}
// MARK: Initialization
diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift
index d829cb01..f7a30407 100644
--- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift
+++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift
@@ -179,7 +179,7 @@ extension AuthoredActivityContent {
///
/// - Parameter gpx: A parsed GPX file
/// - Returns: An ``AuthoredActivityContent``, or `nil` if parsing failed or required properties were missing. Currently, waypoints or POIs may be skipped if they lack coordinate data.
- static func parse(gpx: GPXRoot) -> AuthoredActivityContent? {
+ static func parse(gpx: GPXRoot, baseURL: URL) -> AuthoredActivityContent? {
guard let metadata = gpx.metadata else {
return nil
}
@@ -206,7 +206,7 @@ extension AuthoredActivityContent {
var imageURL: URL?
if let image = metadata.links.first, image.mimetype?.hasPrefix("image") != nil, let href = image.href {
- imageURL = URL(string: href)
+ imageURL = URL(string: href, relativeTo: baseURL)
}
// Parse the waypoints and POIs based on the file version
@@ -214,7 +214,7 @@ extension AuthoredActivityContent {
case "1":
// Version 1 just uses all the top-level waypoints `` defined in the GPX, in order
- let wpts: [ActivityWaypoint] = waypoints(from: gpx.waypoints)
+ let wpts: [ActivityWaypoint] = waypoints(from: gpx.waypoints, baseURL: baseURL)
// For waypoints in this experience, require names, descriptions, and street addresses
guard !wpts.isEmpty, !wpts.contains(where: { $0.name == nil }) else {
@@ -242,7 +242,7 @@ extension AuthoredActivityContent {
}
// Waypoints are strict about requiring names and locations
- let wpts: [ActivityWaypoint] = waypoints(from: route.points)
+ let wpts: [ActivityWaypoint] = waypoints(from: route.points, baseURL: baseURL)
// For waypoints in this experience, require names, descriptions, and street addresses
guard !wpts.isEmpty, !wpts.contains(where: { $0.name == nil }) else {
@@ -279,28 +279,33 @@ extension AuthoredActivityContent {
///
/// - Parameter waypoints: an array of ``GPXWaypoint``s
/// - Returns: an array of ``ActivityWaypoint``s including annotation data (if applicable)
- private static func waypoints(from waypoints: [GPXWaypoint]) -> [ActivityWaypoint] {
+ private static func waypoints(from waypoints: [GPXWaypoint], baseURL: URL) -> [ActivityWaypoint] {
let imageMimeTypes = Set(["image/jpeg", "image/jpg", "image/png"])
let audioMimeTypes = Set(["audio/mpeg", "audio/x-m4a"])
return waypoints.compactMap { wpt in
- let links: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({
+ let imageLinks: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({
guard let mimetype = $0.mimetype else { return false }
return imageMimeTypes.contains(mimetype)
}) ?? []
- let parsedImages: [ActivityWaypointImage] = links.compactMap { link in
+ let parsedImages: [ActivityWaypointImage] = imageLinks.compactMap { link in
guard let href = link.href,
- let url = URL(string: href) else {
+ let url = URL(string: href, relativeTo: baseURL) else {
return nil
}
return ActivityWaypointImage(url: url, altText: link.text)
}
- let parsedAudioClips: [ActivityWaypointAudioClip] = links.compactMap { link in
+ let audioLinks: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({
+ guard let mimetype = $0.mimetype else { return false }
+ return audioMimeTypes.contains(mimetype)
+ }) ?? []
+
+ let parsedAudioClips: [ActivityWaypointAudioClip] = audioLinks.compactMap { link in
guard let href = link.href,
- let url = URL(string: href) else {
+ let url = URL(string: href, relativeTo: baseURL) else {
return nil
}
diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift
index b527325a..8b87bd40 100644
--- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift
+++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift
@@ -127,8 +127,15 @@ class AuthoredActivityLoader {
return nil
}
+ guard let index = knownActivities.events.firstIndex(where: { activityID == $0.id }),
+ let baseURL = knownActivities.events[index].downloadPath else {
+ GDLogAppError("Unable to find download path for activity with ID: \(activityID)")
+ return nil
+ }
+
+
// Parse the GPX file and validate its contents
- return AuthoredActivityContent.parse(gpx: gpx)
+ return AuthoredActivityContent.parse(gpx: gpx, baseURL: baseURL)
}
func add(_ activityID: String, linkVersion: UniversalLinkVersion) async throws {
@@ -296,8 +303,8 @@ class AuthoredActivityLoader {
throw ActivityLoaderError.unableToLoadContent
}
- guard let content = AuthoredActivityContent.parse(gpx: gpx) else {
- GDLogWarn(.routeGuidance, "Unable to parse activity content from GPX for \(id)")
+ guard let content = AuthoredActivityContent.parse(gpx: gpx, baseURL: metadata.downloadPath!) else {
+ GDLogWarn(.routeGuidance, "Unable to parse activity content from GPX for \(id), URL = \(metadata.downloadPath)")
NotificationCenter.default.post(name: .didTryActivityUpdate, object: self, userInfo: [
Keys.updateSuccess: false,
diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityMetadata.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityMetadata.swift
index bea9aae3..3d23c4b4 100644
--- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityMetadata.swift
+++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityMetadata.swift
@@ -45,20 +45,18 @@ struct AuthoredActivityMetadata: Codable, CustomStringConvertible {
}
/// Builds the remote server path that the content can be downloaded from
- /// E.G. https://share.openscape.io/experiences/some-activity.gpx
+ /// E.G. https://share.soundscape.services/experiences//activity.gpx
var downloadPath: URL? {
- var components = URLComponents()
+ var components = URLComponents()
switch linkVersion {
- case .v1:
- components.scheme = "https"
- components.host = "share.openscape.io"
- components.path = "/experiences/\(id).gpx"
case .v2, .v3:
- // Version 2 and 3 links also look the same (perk of forking)
components.scheme = "https"
components.host = "share.soundscape.services"
- components.path = "experiences/\(id).gpx"
+ components.path = "/activities/\(id)/activity.gpx"
+ default:
+ // no other versions currently supported
+ break
}
return components.url
diff --git a/apps/ios/GuideDogs/Code/Visual UI/Controls/Main Menu/MenuViewController.swift b/apps/ios/GuideDogs/Code/Visual UI/Controls/Main Menu/MenuViewController.swift
index 368b6585..436512e5 100644
--- a/apps/ios/GuideDogs/Code/Visual UI/Controls/Main Menu/MenuViewController.swift
+++ b/apps/ios/GuideDogs/Code/Visual UI/Controls/Main Menu/MenuViewController.swift
@@ -10,11 +10,12 @@ import UIKit
import SafariServices
enum MenuItem {
- case home, devices, help, settings, status, feedback, rate, share
+ case home, recreation, devices, help, settings, status, feedback, rate, share
var localizedString: String {
switch self {
case .home: return GDLocalizedString("ui.menu.close")
+ case .recreation: return GDLocalizedString("menu.events")
case .devices: return GDLocalizedString("menu.devices")
case .help: return GDLocalizedString("menu.help_and_tutorials")
case .settings: return GDLocalizedString("settings.screen_title")
@@ -28,6 +29,7 @@ enum MenuItem {
var accessibilityString: String {
switch self {
case .home: return GDLocalizedString("ui.menu.close")
+ case .recreation: return GDLocalizedString("menu.events")
case .devices: return GDLocalizedString("menu.devices")
case .help: return GDLocalizedString("menu.help_and_tutorials")
case .settings: return GDLocalizedString("settings.screen_title")
@@ -41,6 +43,7 @@ enum MenuItem {
var icon: UIImage? {
switch self {
case .home: return UIImage(named: "ic_chevron_left_28px")
+ case .recreation: return UIImage(named: "nordic_walking_white_28dp")
case .devices: return UIImage(named: "baseline-headset-28px")
case .help: return UIImage(named: "ic_help_outline_28px")
case .settings: return UIImage(named: "ic_settings_28px")
@@ -61,6 +64,7 @@ class MenuViewController: UIViewController {
override func loadView() {
// Build views for menu items
menuView.addMenuItem(.devices)
+ menuView.addMenuItem(.recreation)
menuView.addMenuItem(.settings)
menuView.addMenuItem(.help)
menuView.addMenuItem(.feedback)
diff --git a/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift b/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift
index ef9a222d..a750177b 100644
--- a/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift
+++ b/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Home/HomeViewController.swift
@@ -38,6 +38,7 @@ class HomeViewController: UIViewController {
/// - Returns: The segue associated with this menu item
static func segue(for menuItem: MenuItem) -> String? {
switch menuItem {
+ case .recreation: return Segue.showRecreationActivities
case .devices: return Segue.showManageDevices
case .help: return Segue.showHelp
case .settings: return Segue.showSettings
diff --git a/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift b/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift
index d5afbb3b..eef29fd4 100644
--- a/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift
+++ b/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift
@@ -15,6 +15,7 @@ final class AuthoredActivityContentTest: XCTestCase {
// MARK: Test GPX Parsing
+ static let baseURL = URL(string: "https://example.com")!
/// Tests parsing from GPX
/// Using `GPXSoundscapeSharedContentExtensions` v1
/// And minimal other details
@@ -63,7 +64,7 @@ final class AuthoredActivityContentTest: XCTestCase {
XCTFail("Failed to get parsedData")
return
}
- guard let activity = AuthoredActivityContent.parse(gpx: root) else {
+ guard let activity = AuthoredActivityContent.parse(gpx: root, baseURL: AuthoredActivityContentTest.baseURL) else {
XCTFail("Failed to create AuthoredActivityContent from GPXRoot")
return
}
@@ -150,7 +151,7 @@ final class AuthoredActivityContentTest: XCTestCase {
XCTFail("Failed to get parsedData")
return
}
- guard let activity = AuthoredActivityContent.parse(gpx: root) else {
+ guard let activity = AuthoredActivityContent.parse(gpx: root, baseURL: AuthoredActivityContentTest.baseURL) else {
XCTFail("Failed to create AuthoredActivityContent from GPXRoot")
return
}