[ios] refactor TrackRecordingManager to return the proper state

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn 2025-01-09 16:38:13 +04:00
parent 867778c368
commit 106507fef5
5 changed files with 105 additions and 87 deletions

View file

@ -21,12 +21,12 @@ NS_ASSUME_NONNULL_BEGIN
typedef void (^SearchInDownloaderCompletions)(NSArray<MWMMapSearchResult *> *results, BOOL finished);
typedef void (^TrackRecordingUpdatedHandler)(TrackInfo * _Nonnull trackInfo);
@protocol TrackRecorder <NSObject>
@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.

View file

@ -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

View file

@ -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<LocationService>
+ (void)start;
+ (void)stop;
@ -14,10 +21,8 @@ NS_SWIFT_NAME(LocationManager)
+ (void)removeObserver:(id<MWMLocationObserver>)observer NS_SWIFT_NAME(remove(observer:));
+ (void)setMyPositionMode:(MWMMyPositionMode)mode;
+ (void)checkLocationStatus;
+ (nullable CLLocation *)lastLocation;
+ (BOOL)isLocationProhibited;
+ (nullable CLHeading *)lastHeading;
+ (void)applicationDidBecomeActive;

View file

@ -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) }

View file

@ -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()
}