[ios] refactor TrackRecordingManager to return the proper state
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
parent
867778c368
commit
106507fef5
5 changed files with 105 additions and 87 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Reference in a new issue