Check Application state before sending RR (#1960)

* check the state

* better handling

* protocol to mock the application state

* test improvement
This commit is contained in:
Mauro
2023-10-25 19:16:37 +02:00
committed by GitHub
parent af76be2426
commit d457736673
14 changed files with 235 additions and 106 deletions
@@ -0,0 +1,73 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import UIKit
extension ApplicationMock {
static var `default`: ApplicationProtocol {
ApplicationMock(withState: .active,
backgroundTimeRemaining: 10,
allowTasks: true)
}
static var mockBroken: ApplicationProtocol {
ApplicationMock(withState: .inactive,
backgroundTimeRemaining: 0,
allowTasks: false)
}
static var mockAboutToSuspend: ApplicationProtocol {
ApplicationMock(withState: .background,
backgroundTimeRemaining: 2,
allowTasks: false)
}
private static var bgTaskIdentifier = 0
convenience init(withState applicationState: UIApplication.State,
backgroundTimeRemaining: TimeInterval,
allowTasks: Bool) {
self.init()
underlyingApplicationState = applicationState
underlyingBackgroundTimeRemaining = backgroundTimeRemaining
beginBackgroundTaskExpirationHandlerClosure = { [weak self] handler in
guard let self else {
return .invalid
}
guard allowTasks else {
return .invalid
}
return beginBackgroundTask(withName: nil, expirationHandler: handler)
}
beginBackgroundTaskWithNameExpirationHandlerClosure = { _, handler in
guard allowTasks else {
return .invalid
}
Self.bgTaskIdentifier += 1
let identifier = UIBackgroundTaskIdentifier(rawValue: Self.bgTaskIdentifier)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
handler?()
}
return identifier
}
}
}
@@ -279,6 +279,85 @@ class AppLockServiceMock: AppLockServiceProtocol {
}
}
}
class ApplicationMock: ApplicationProtocol {
var backgroundTimeRemaining: TimeInterval {
get { return underlyingBackgroundTimeRemaining }
set(value) { underlyingBackgroundTimeRemaining = value }
}
var underlyingBackgroundTimeRemaining: TimeInterval!
var applicationState: UIApplication.State {
get { return underlyingApplicationState }
set(value) { underlyingApplicationState = value }
}
var underlyingApplicationState: UIApplication.State!
//MARK: - beginBackgroundTask
var beginBackgroundTaskExpirationHandlerCallsCount = 0
var beginBackgroundTaskExpirationHandlerCalled: Bool {
return beginBackgroundTaskExpirationHandlerCallsCount > 0
}
var beginBackgroundTaskExpirationHandlerReturnValue: UIBackgroundTaskIdentifier!
var beginBackgroundTaskExpirationHandlerClosure: (((() -> Void)?) -> UIBackgroundTaskIdentifier)?
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
beginBackgroundTaskExpirationHandlerCallsCount += 1
if let beginBackgroundTaskExpirationHandlerClosure = beginBackgroundTaskExpirationHandlerClosure {
return beginBackgroundTaskExpirationHandlerClosure(handler)
} else {
return beginBackgroundTaskExpirationHandlerReturnValue
}
}
//MARK: - beginBackgroundTask
var beginBackgroundTaskWithNameExpirationHandlerCallsCount = 0
var beginBackgroundTaskWithNameExpirationHandlerCalled: Bool {
return beginBackgroundTaskWithNameExpirationHandlerCallsCount > 0
}
var beginBackgroundTaskWithNameExpirationHandlerReturnValue: UIBackgroundTaskIdentifier!
var beginBackgroundTaskWithNameExpirationHandlerClosure: ((String?, (() -> Void)?) -> UIBackgroundTaskIdentifier)?
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
beginBackgroundTaskWithNameExpirationHandlerCallsCount += 1
if let beginBackgroundTaskWithNameExpirationHandlerClosure = beginBackgroundTaskWithNameExpirationHandlerClosure {
return beginBackgroundTaskWithNameExpirationHandlerClosure(taskName, handler)
} else {
return beginBackgroundTaskWithNameExpirationHandlerReturnValue
}
}
//MARK: - endBackgroundTask
var endBackgroundTaskCallsCount = 0
var endBackgroundTaskCalled: Bool {
return endBackgroundTaskCallsCount > 0
}
var endBackgroundTaskReceivedIdentifier: UIBackgroundTaskIdentifier?
var endBackgroundTaskReceivedInvocations: [UIBackgroundTaskIdentifier] = []
var endBackgroundTaskClosure: ((UIBackgroundTaskIdentifier) -> Void)?
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) {
endBackgroundTaskCallsCount += 1
endBackgroundTaskReceivedIdentifier = identifier
endBackgroundTaskReceivedInvocations.append(identifier)
endBackgroundTaskClosure?(identifier)
}
//MARK: - open
var openCallsCount = 0
var openCalled: Bool {
return openCallsCount > 0
}
var openReceivedUrl: URL?
var openReceivedInvocations: [URL] = []
var openClosure: ((URL) -> Void)?
func open(_ url: URL) {
openCallsCount += 1
openReceivedUrl = url
openReceivedInvocations.append(url)
openClosure?(url)
}
}
class AudioConverterMock: AudioConverterProtocol {
//MARK: - convertToOpusOgg
@@ -65,7 +65,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
roomProxy: parameters.roomProxy,
appSettings: parameters.appSettings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: UIApplication.shared)
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
maxCompressedHeight: ComposerConstant.maxHeight,
@@ -32,6 +32,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private let roomProxy: RoomProxyProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let application: ApplicationProtocol
private unowned let userIndicatorController: UserIndicatorControllerProtocol
private let notificationCenterProtocol: NotificationCenterProtocol
private let voiceMessageRecorder: VoiceMessageRecorderProtocol
@@ -48,6 +49,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol,
application: ApplicationProtocol,
notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) {
self.roomProxy = roomProxy
self.timelineController = timelineController
@@ -56,6 +58,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
self.userIndicatorController = userIndicatorController
self.notificationCenterProtocol = notificationCenterProtocol
self.mediaPlayerProvider = mediaPlayerProvider
self.application = application
voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
super.init(initialViewState: RoomScreenViewState(roomID: timelineController.roomID,
@@ -327,16 +330,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
/// The ID of the newest item in the room that the user has seen.
/// This includes both event based items and virtual items.
private var lastReadItemID: TimelineItemIdentifier?
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError> {
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async {
guard application.applicationState == .active else { return }
guard lastReadItemID != lastVisibleItemID,
let eventItemID = eventBasedItem(nearest: lastVisibleItemID)
else { return .success(()) }
let eventItemID = eventBasedItem(nearest: lastVisibleItemID) else {
return
}
// Make sure the item is newer than the item that was last marked as read.
if let lastReadItemIndex = state.timelineViewState.timelineIDs.firstIndex(of: lastReadItemID?.timelineID ?? ""),
let lastVisibleItemIndex = state.timelineViewState.timelineIDs.firstIndex(of: eventItemID.timelineID),
lastReadItemIndex > lastVisibleItemIndex {
return .success(())
return
}
// Update the last read item ID to avoid attempting duplicate requests.
@@ -348,10 +354,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
switch await timelineController.sendReadReceipt(for: eventItemID) {
case .success:
return .success(())
case .failure:
return .failure(.generic)
case .success():
break
case let .failure(error):
MXLog.error("[TimelineViewController] Failed to send read receipt: \(error)")
}
}
@@ -1028,7 +1034,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func openSystemSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
application.open(url)
}
}
@@ -1061,7 +1067,8 @@ extension RoomScreenViewModel {
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
}
private struct ReplyInfo {
@@ -61,7 +61,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name", avatarURL: URL.picturesDirectory)),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
RoomHeaderView(context: viewModel.context)
.previewLayout(.sizeThatFits)
@@ -76,7 +77,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
RoomHeaderView(context: viewModel.context)
.previewLayout(.sizeThatFits)
@@ -195,7 +195,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room", isCallOngoing: true)),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
static var previews: some View {
NavigationStack {
@@ -65,7 +65,8 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
members: members)),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
@@ -86,7 +86,8 @@ struct UITimelineView_Previews: PreviewProvider, TestablePreview {
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
static var previews: some View {
NavigationStack {
@@ -174,7 +174,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
static var previews: some View {
NavigationStack {
@@ -17,16 +17,23 @@
import Foundation
import UIKit
// sourcery: AutoMockable
protocol ApplicationProtocol {
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
func open(_ url: URL)
var backgroundTimeRemaining: TimeInterval { get }
var applicationState: UIApplication.State { get }
}
extension UIApplication: ApplicationProtocol { }
extension UIApplication: ApplicationProtocol {
func open(_ url: URL) {
open(url, options: [:], completionHandler: nil)
}
}