From bfb2dc0154cb737b3e2c90f34923bee899f949a8 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 16:38:13 +0400 Subject: [PATCH] [ios] refactor TrackRecordingManager to return the proper state Signed-off-by: Kiryl Kaveryn --- .../CoreApi/Framework/MWMFrameworkHelper.h | 4 +- .../CoreApi/Framework/MWMFrameworkHelper.mm | 6 +- .../Maps/Core/Location/MWMLocationManager.h | 11 +- .../TrackRecorder/TrackRecordingManager.swift | 161 +++++++++--------- .../UI/PlacePage/PlacePageInteractor.swift | 10 ++ 5 files changed, 105 insertions(+), 87 deletions(-) diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h index cbadf8945c..0272047ba5 100644 --- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h +++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h @@ -21,12 +21,12 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^SearchInDownloaderCompletions)(NSArray *results, BOOL finished); typedef void (^TrackRecordingUpdatedHandler)(TrackInfo * _Nonnull trackInfo); -@protocol TrackRecorder +@protocol TrackRecorder + (void)startTrackRecording; + (void)setTrackRecordingUpdateHandler:(TrackRecordingUpdatedHandler _Nullable)trackRecordingDidUpdate; + (void)stopTrackRecording; -+ (void)saveTrackRecordingWithName:(nullable NSString *)name; ++ (void)saveTrackRecordingWithName:(nonnull NSString *)name; + (BOOL)isTrackRecordingEnabled; + (BOOL)isTrackRecordingEmpty; /// Returns current track recording elevation info. diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm index 6b8ea0aa9a..47f6fe1461 100644 --- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm +++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm @@ -235,8 +235,8 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore( GetFramework().StopTrackRecording(); } -+ (void)saveTrackRecordingWithName:(nullable NSString *)name { - GetFramework().SaveTrackRecordingWithName(name == nil ? "" : name.UTF8String); ++ (void)saveTrackRecordingWithName:(nonnull NSString *)name { + GetFramework().SaveTrackRecordingWithName(name.UTF8String); } + (BOOL)isTrackRecordingEnabled { @@ -248,7 +248,7 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore( } + (ElevationProfileData * _Nonnull)trackRecordingElevationInfo { - return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingCurrentElevationInfo()]; + return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingElevationInfo()]; } // MARK: - ProductsManager diff --git a/iphone/Maps/Core/Location/MWMLocationManager.h b/iphone/Maps/Core/Location/MWMLocationManager.h index e662c47921..2bb3b033f7 100644 --- a/iphone/Maps/Core/Location/MWMLocationManager.h +++ b/iphone/Maps/Core/Location/MWMLocationManager.h @@ -3,8 +3,15 @@ NS_ASSUME_NONNULL_BEGIN +@protocol LocationService + ++ (BOOL)isLocationProhibited; ++ (void)checkLocationStatus; + +@end + NS_SWIFT_NAME(LocationManager) -@interface MWMLocationManager : NSObject +@interface MWMLocationManager : NSObject + (void)start; + (void)stop; @@ -14,10 +21,8 @@ NS_SWIFT_NAME(LocationManager) + (void)removeObserver:(id)observer NS_SWIFT_NAME(remove(observer:)); + (void)setMyPositionMode:(MWMMyPositionMode)mode; -+ (void)checkLocationStatus; + (nullable CLLocation *)lastLocation; -+ (BOOL)isLocationProhibited; + (nullable CLHeading *)lastHeading; + (void)applicationDidBecomeActive; diff --git a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift index 5012b2ed65..0cef948908 100644 --- a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift +++ b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift @@ -4,31 +4,38 @@ enum TrackRecordingState: Int, Equatable { case active } -enum TrackRecordingAction: String, CaseIterable { +enum TrackRecordingAction { case start - case stop + case stopAndSave(name: String) } enum TrackRecordingError: Error { case locationIsProhibited + case trackIsEmpty + case systemError(Error) } -protocol TrackRecordingObserver: AnyObject { +enum TrackRecordingActionResult { + case success + case error(TrackRecordingError) +} + +@objc +protocol TrackRecordingObservable: AnyObject { + var recordingState: TrackRecordingState { get } + var trackRecordingInfo: TrackInfo { get } + func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler) func removeObserver(_ observer: AnyObject) + func contains(_ observer: AnyObject) -> Bool } -typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo?) -> Void +typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo) -> Void @objcMembers final class TrackRecordingManager: NSObject { - typealias CompletionHandler = () -> Void - - private enum SavingOption { - case withoutSaving - case saveWithName(String? = nil) - } + typealias CompletionHandler = (TrackRecordingActionResult) -> Void fileprivate struct Observation { weak var observer: AnyObject? @@ -37,26 +44,33 @@ final class TrackRecordingManager: NSObject { static let shared: TrackRecordingManager = { let trackRecorder = FrameworkHelper.self + let locationManager = LocationManager.self var activityManager: TrackRecordingActivityManager? = nil #if canImport(ActivityKit) if #available(iOS 16.2, *) { activityManager = TrackRecordingLiveActivityManager.shared } #endif - return TrackRecordingManager(trackRecorder: trackRecorder, activityManager: activityManager) + return TrackRecordingManager(trackRecorder: trackRecorder, + locationService: locationManager, + activityManager: activityManager) }() private let trackRecorder: TrackRecorder.Type + private var locationService: LocationService.Type private var activityManager: TrackRecordingActivityManager? private var observations: [Observation] = [] - private var trackRecordingInfo: TrackInfo? + private(set) var trackRecordingInfo: TrackInfo = .empty() var recordingState: TrackRecordingState { trackRecorder.isTrackRecordingEnabled() ? .active : .inactive } - private init(trackRecorder: TrackRecorder.Type, activityManager: TrackRecordingActivityManager?) { + init(trackRecorder: TrackRecorder.Type, + locationService: LocationService.Type, + activityManager: TrackRecordingActivityManager?) { self.trackRecorder = trackRecorder + self.locationService = locationService self.activityManager = activityManager super.init() } @@ -84,22 +98,35 @@ final class TrackRecordingManager: NSObject { } func processAction(_ action: TrackRecordingAction, completion: (CompletionHandler)? = nil) { - switch action { - case .start: - start(completion: completion) - case .stop: - stop(completion: completion) + do { + switch action { + case .start: + try startRecording() + case .stopAndSave(let name): + stopRecording() + try checkIsTrackNotEmpty() + saveTrackRecording(name: name) + } + completion?(.success) + } catch { + handleError(error, completion: completion) } } // MARK: - Private methods - private func checkIsLocationEnabled() throws { - if LocationManager.isLocationProhibited() { + private func checkIsLocationEnabled() throws(TrackRecordingError) { + if locationService.isLocationProhibited() { throw TrackRecordingError.locationIsProhibited } } + private func checkIsTrackNotEmpty() throws(TrackRecordingError) { + if trackRecorder.isTrackRecordingEmpty() { + throw TrackRecordingError.trackIsEmpty + } + } + // MARK: - Handle track recording process private func subscribeOnTrackRecordingProgressUpdates() { @@ -113,92 +140,63 @@ final class TrackRecordingManager: NSObject { private func unsubscribeFromTrackRecordingProgressUpdates() { trackRecorder.setTrackRecordingUpdateHandler(nil) - trackRecordingInfo = nil } // MARK: - Handle Start/Stop event and Errors - private func start(completion: (CompletionHandler)? = nil) { - do { + private func startRecording() throws(TrackRecordingError) { + switch recordingState { + case .inactive: try checkIsLocationEnabled() - switch recordingState { - case .inactive: - subscribeOnTrackRecordingProgressUpdates() - trackRecorder.startTrackRecording() - notifyObservers() - try? activityManager?.start(with: trackRecordingInfo ?? .empty()) - case .active: - break + subscribeOnTrackRecordingProgressUpdates() + trackRecorder.startTrackRecording() + notifyObservers() + do { + try activityManager?.start(with: trackRecordingInfo) + } catch { + LOG(.warning, "Failed to start activity manager") + handleError(.systemError(error)) } - completion?() - } catch { - handleError(error, completion: completion) + case .active: + break } } - private func stop(completion: (CompletionHandler)? = nil) { - guard !trackRecorder.isTrackRecordingEmpty() else { - Toast.toast(withText: L("track_recording_toast_nothing_to_save")).show() - stopRecording(.withoutSaving, completion: completion) - return - } - Self.showOnFinishRecordingAlert(onSave: { [weak self] in - guard let self else { return } - // TODO: (KK) pass the user provided name from the track saving screen (when it will be implemented) - self.stopRecording(.saveWithName(), completion: completion) - }, - onStop: { [weak self] in - guard let self else { return } - self.stopRecording(.withoutSaving, completion: completion) - }, - onContinue: { - completion?() - }) - } - - private func stopRecording(_ savingOption: SavingOption, completion: (CompletionHandler)? = nil) { + private func stopRecording() { unsubscribeFromTrackRecordingProgressUpdates() trackRecorder.stopTrackRecording() + trackRecordingInfo = .empty() activityManager?.stop() notifyObservers() - - switch savingOption { - case .withoutSaving: - break - case .saveWithName(let name): - trackRecorder.saveTrackRecording(withName: name) - } - completion?() } - private func handleError(_ error: Error, completion: (CompletionHandler)? = nil) { - LOG(.error, error.localizedDescription) + private func saveTrackRecording(name: String) { + trackRecorder.saveTrackRecording(withName: name) + } + + private func handleError(_ error: TrackRecordingError, completion: (CompletionHandler)? = nil) { switch error { case TrackRecordingError.locationIsProhibited: // Show alert to enable location - LocationManager.checkLocationStatus() - default: + locationService.checkLocationStatus() + case TrackRecordingError.trackIsEmpty: + Toast.toast(withText: L("track_recording_toast_nothing_to_save")).show() + case TrackRecordingError.systemError(let error): + LOG(.error, error.localizedDescription) break } - stopRecording(.withoutSaving, completion: completion) - } - - private static func showOnFinishRecordingAlert(onSave: @escaping CompletionHandler, - onStop: @escaping CompletionHandler, - onContinue: @escaping CompletionHandler) { - let alert = UIAlertController(title: L("track_recording_alert_title"), message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: L("continue_recording"), style: .default, handler: { _ in onContinue() })) - alert.addAction(UIAlertAction(title: L("stop_without_saving"), style: .default, handler: { _ in onStop() })) - alert.addAction(UIAlertAction(title: L("save"), style: .cancel, handler: { _ in onSave() })) - UIViewController.topViewController().present(alert, animated: true) + DispatchQueue.main.async { + completion?(.error(error)) + } } } // MARK: - TrackRecordingObserver -extension TrackRecordingManager: TrackRecordingObserver { +extension TrackRecordingManager: TrackRecordingObservable { @objc func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler) { + guard !observations.contains(where: { $0.observer === observer }) else { return } let observation = Observation(observer: observer, recordingStateDidChangeHandler: recordingIsActiveDidChangeHandler) observations.append(observation) recordingIsActiveDidChangeHandler(recordingState, trackRecordingInfo) @@ -209,6 +207,11 @@ extension TrackRecordingManager: TrackRecordingObserver { observations.removeAll { $0.observer === observer } } + @objc + func contains(_ observer: AnyObject) -> Bool { + observations.contains { $0.observer === observer } + } + private func notifyObservers() { observations = observations.filter { $0.observer != nil } observations.forEach { $0.recordingStateDidChangeHandler?(recordingState, trackRecordingInfo) } diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index bb2e8161b3..d316324134 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -242,6 +242,16 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate { // TODO: This is temporary solution. Remove the dialog and use the MWMPlacePageManagerHelper.removeTrack // directly here when the track recovery mechanism will be implemented. showTrackDeletionConfirmationDialog() + case .saveTrackRecording: + // TODO: (KK) pass name + TrackRecordingManager.shared.processAction(.stopAndSave(name: "")) { [weak self] result in + switch result { + case .success: + break + case .error: + self?.presenter?.closeAnimated() + } + } @unknown default: fatalError() }