[ios] implement ActivityWidget support to the TrackRecordingManager

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn 2024-10-08 22:13:20 +04:00 committed by Roman Tsisyk
parent e5395e6f16
commit 5e79069a84
4 changed files with 142 additions and 51 deletions

View file

@ -136,7 +136,7 @@ final class TrackRecordingViewController: MWMViewController {
@objc
private func onTrackRecordingButtonPressed(_ sender: Any) {
switch trackRecordingManager.recordingState {
case .inactive, .error:
case .inactive:
trackRecordingManager.processAction(.start)
case .active:
trackRecordingManager.processAction(.stop)

View file

@ -101,6 +101,7 @@ using namespace osm_auth_ios;
NSTimeInterval const minimumBackgroundFetchIntervalInSeconds = 6 * 60 * 60;
[UIApplication.sharedApplication setMinimumBackgroundFetchInterval:minimumBackgroundFetchIntervalInSeconds];
[self updateApplicationIconBadgeNumber];
[TrackRecordingManager.shared setup];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

View file

@ -1,10 +1,9 @@
enum TrackRecordingState: Equatable {
case inactive
case active
case error(TrackRecordingError)
}
enum TrackRecordingAction {
enum TrackRecordingAction: String, CaseIterable {
case start
case stop
}
@ -13,6 +12,11 @@ enum TrackRecordingError: Error {
case locationIsProhibited
}
protocol TrackRecordingObserver: AnyObject {
func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler)
func removeObserver(_ observer: AnyObject)
}
typealias TrackRecordingStateHandler = (Bool) -> Void
@objcMembers
@ -30,20 +34,48 @@ final class TrackRecordingManager: NSObject {
var recordingStateDidChangeHandler: TrackRecordingStateHandler?
}
static let shared: TrackRecordingManager = TrackRecordingManager(trackRecorder: FrameworkHelper.self)
static let shared: TrackRecordingManager = {
let trackRecorder = FrameworkHelper.self
var activityManager: TrackRecordingActivityManager? = nil
#if canImport(ActivityKit)
if #available(iOS 16.2, *) {
activityManager = TrackRecordingLiveActivityManager.shared
}
#endif
return TrackRecordingManager(trackRecorder: trackRecorder, activityManager: activityManager)
}()
private let trackRecorder: TrackRecorder.Type
private var activityManager: TrackRecordingActivityManager?
private var observations: [Observation] = []
private(set) var recordingState: TrackRecordingState = .inactive {
didSet {
notifyObservers()
}
private var trackRecordingInfo: TrackRecordingInfo?
var recordingState: TrackRecordingState {
trackRecorder.isTrackRecordingEnabled() ? .active : .inactive
}
private init(trackRecorder: TrackRecorder.Type) {
private init(trackRecorder: TrackRecorder.Type, activityManager: TrackRecordingActivityManager?) {
self.trackRecorder = trackRecorder
self.activityManager = activityManager
super.init()
self.recordingState = getCurrentRecordingState()
subscribeOnAppLifecycleEvents()
}
// MARK: - Public methods
@objc
func setup() {
do {
try checkIsLocationEnabled()
switch recordingState {
case .inactive:
break
case .active:
subscribeOnTrackRecordingProgressUpdates()
}
} catch {
handleError(error)
}
}
@objc
@ -60,50 +92,73 @@ final class TrackRecordingManager: NSObject {
}
}
@objc
func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler) {
let observation = Observation(observer: observer, recordingStateDidChangeHandler: recordingIsActiveDidChangeHandler)
observations.append(observation)
recordingIsActiveDidChangeHandler(recordingState == .active)
}
// MARK: - Private methods
@objc
func removeObserver(_ observer: AnyObject) {
observations.removeAll { $0.observer === observer }
}
private func notifyObservers() {
observations = observations.filter { $0.observer != nil }
observations.forEach { $0.recordingStateDidChangeHandler?(recordingState == .active) }
}
private func handleError(_ error: TrackRecordingError, completion: (CompletionHandler)? = nil) {
switch error {
case .locationIsProhibited:
// Show alert to enable location
LocationManager.checkLocationStatus()
}
stopRecording(.withoutSaving, completion: completion)
}
private func getCurrentRecordingState() -> TrackRecordingState {
private func checkIsLocationEnabled() throws {
guard !LocationManager.isLocationProhibited() else {
return .error(.locationIsProhibited)
throw TrackRecordingError.locationIsProhibited
}
return trackRecorder.isTrackRecordingEnabled() ? .active : .inactive
}
// MARK: - Handle lifecycle events
private func subscribeOnAppLifecycleEvents() {
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(prepareForTermination), name: UIApplication.willTerminateNotification, object: nil)
}
@objc
private func willResignActive() {
guard let activityManager, recordingState == .active else { return }
do {
try activityManager.start(with: trackRecordingInfo ?? .empty())
} catch {
handleError(error)
}
}
@objc
private func willEnterForeground() {
activityManager?.stop()
}
@objc
private func prepareForTermination() {
activityManager?.stop()
}
// MARK: - Handle track recording process
private func subscribeOnTrackRecordingProgressUpdates() {
trackRecorder.setTrackRecordingUpdateHandler { [weak self] info in
guard let self else { return }
self.trackRecordingInfo = info
self.activityManager?.update(info)
}
}
private func unsubscribeFromTrackRecordingProgressUpdates() {
trackRecorder.setTrackRecordingUpdateHandler(nil)
trackRecordingInfo = nil
}
// MARK: - Handle Start/Stop event and Errors
private func start(completion: (CompletionHandler)? = nil) {
let state = getCurrentRecordingState()
switch state {
case .inactive:
trackRecorder.startTrackRecording()
recordingState = .active
do {
try checkIsLocationEnabled()
switch recordingState {
case .inactive:
subscribeOnTrackRecordingProgressUpdates()
trackRecorder.startTrackRecording()
notifyObservers()
case .active:
break
}
completion?()
case .active:
completion?()
case .error(let trackRecordingError):
handleError(trackRecordingError, completion: completion)
} catch {
handleError(error, completion: completion)
}
}
@ -128,18 +183,32 @@ final class TrackRecordingManager: NSObject {
}
private func stopRecording(_ savingOption: SavingOption, completion: (CompletionHandler)? = nil) {
unsubscribeFromTrackRecordingProgressUpdates()
trackRecorder.stopTrackRecording()
activityManager?.stop()
notifyObservers()
switch savingOption {
case .withoutSaving:
break
case .saveWithName(let name):
trackRecorder.saveTrackRecording(withName: name)
}
recordingState = .inactive
completion?()
}
private static func showOnFinishRecordingAlert(onSave: @escaping CompletionHandler,
private func handleError(_ error: Error, completion: (CompletionHandler)? = nil) {
LOG(.error, error.localizedDescription)
switch error {
case TrackRecordingError.locationIsProhibited:
// Show alert to enable location
LocationManager.checkLocationStatus()
default:
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)
@ -149,3 +218,24 @@ final class TrackRecordingManager: NSObject {
UIViewController.topViewController().present(alert, animated: true)
}
}
// MARK: - TrackRecordingObserver
extension TrackRecordingManager: TrackRecordingObserver {
@objc
func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler) {
let observation = Observation(observer: observer, recordingStateDidChangeHandler: recordingIsActiveDidChangeHandler)
observations.append(observation)
recordingIsActiveDidChangeHandler(recordingState == .active)
}
@objc
func removeObserver(_ observer: AnyObject) {
observations.removeAll { $0.observer === observer }
}
private func notifyObservers() {
observations = observations.filter { $0.observer != nil }
observations.forEach { $0.recordingStateDidChangeHandler?(recordingState == .active) }
}
}

View file

@ -101,7 +101,7 @@ extension BottomMenuPresenter {
enabled: enabled)
case .recordTrack:
switch trackRecorder.recordingState {
case .inactive, .error:
case .inactive:
cell.configure(imageName: "track_recorder_inactive", title: L("start_track_recording"))
case .active:
cell.configure(imageName: "track_recorder_active", title: L("stop_track_recording"))