From 23f59a858290b794b94389d385a191f9dd16c0c4 Mon Sep 17 00:00:00 2001 From: RDMurray Date: Tue, 19 Mar 2024 23:49:36 +0000 Subject: [PATCH] Update iOS project version and add recreation activities menu item (#84) * iOS: Re-enable activities and update URLs * Fix the URL for downloading activities * Support relative links for images and audio files in activities. * bump build to 17 * Fix audio clips in activities, remove landscape from orientations. (build 18) * Fix audio and image loading for activities, update UnitTests bumped build number to 19 --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 22 ++++++++-------- .../GuideDogs/Assets/PropertyLists/Info.plist | 3 +-- .../Components/UniversalLinkComponents.swift | 2 +- .../AuthoredActivityContent.swift | 25 +++++++++++-------- .../AuthoredActivityLoader.swift | 13 +++++++--- .../AuthoredActivityMetadata.swift | 14 +++++------ .../Main Menu/MenuViewController.swift | 6 ++++- .../Home/HomeViewController.swift | 1 + .../AuthoredActivityContentTest.swift | 5 ++-- 9 files changed, 53 insertions(+), 38 deletions(-) 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 }