Skip to content

Commit

Permalink
Update iOS project version and add recreation activities menu item (#84)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
RDMurray authored Mar 19, 2024
1 parent c0cd985 commit 23f59a8
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 38 deletions.
22 changes: 11 additions & 11 deletions apps/ios/GuideDogs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
3 changes: 1 addition & 2 deletions apps/ios/GuideDogs/Assets/PropertyLists/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<key>CFBundleSpokenName</key>
<string>${BUNDLE_SPOKEN_NAME}</string>
<key>CFBundleVersion</key>
<string>7</string>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
Expand Down Expand Up @@ -156,7 +156,6 @@
<string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct UniversalLinkComponents {
// Add query items
components.queryItems = queryItems

return components.url
return components.url
}

// MARK: Initialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -206,15 +206,15 @@ 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
switch ext.version ?? "1" {
case "1":
// Version 1 just uses all the top-level waypoints `<wpt></wpt>` 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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 23f59a8

Please sign in to comment.