From 5e476516cbee07e73e16c7e97cec44903b7b5010 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 28 May 2022 14:58:52 +0400 Subject: [PATCH] ios: lock toggle; android: fix lock timer (#702) --- .../.idea/codeStyles/codeStyleConfig.xml | 1 + .../java/chat/simplex/app/MainActivity.kt | 62 +++++++-------- .../app/src/main/res/values-ru/strings.xml | 2 +- .../app/src/main/res/values/strings.xml | 2 +- apps/ios/Shared/ContentView.swift | 48 +++++------ apps/ios/Shared/Model/ChatModel.swift | 1 + apps/ios/Shared/SimpleXApp.swift | 56 +++++++------ .../Helpers/LocalAuthenticationUtils.swift | 27 +++++-- .../Views/UserSettings/SettingsView.swift | 79 +++++++++++++++++++ 9 files changed, 190 insertions(+), 88 deletions(-) diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..6e6eec1148 100644 --- a/apps/android/.idea/codeStyles/codeStyleConfig.xml +++ b/apps/android/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 4646474f0a..f8a21c2cd3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.* import android.net.Uri import android.os.Bundle +import android.os.SystemClock.elapsedRealtime import android.util.Log import androidx.activity.compose.setContent import androidx.activity.viewModels @@ -37,12 +38,12 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { private val vm by viewModels() private val chatController by lazy { (application as SimplexApp).chatController } private val userAuthorized = mutableStateOf(null) - private val lastLA = mutableStateOf(null) + private val enteredBackground = mutableStateOf(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ProcessLifecycleOwner.get().lifecycle.addObserver(this) -// testJson() + // testJson() val m = vm.chatModel processNotificationIntent(intent, m) setContent { @@ -67,38 +68,37 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { withApi { when (event) { + Lifecycle.Event.ON_STOP -> { + enteredBackground.value = elapsedRealtime() + } Lifecycle.Event.ON_START -> { // perform local authentication if needed val m = vm.chatModel - val lastLAVal = lastLA.value - if ( - m.controller.appPrefs.performLA.get() - && (lastLAVal == null || (System.nanoTime() - lastLAVal >= 30 * 1e+9)) - ) { - userAuthorized.value = false - authenticate( - generalGetString(R.string.auth_access_chats), - generalGetString(R.string.auth_log_in_using_credential), - this@MainActivity, - completed = { laResult -> - when (laResult) { - LAResult.Success -> { - userAuthorized.value = true - lastLA.value = System.nanoTime() - } - is LAResult.Error -> laErrorToast(applicationContext, laResult.errString) - LAResult.Failed -> laFailedToast(applicationContext) - LAResult.Unavailable -> { - userAuthorized.value = true - m.performLA.value = false - m.controller.appPrefs.performLA.set(false) - laUnavailableTurningOffAlert() + val enteredBackgroundVal = enteredBackground.value + if (!m.controller.appPrefs.performLA.get()) { + userAuthorized.value = true + } else { + if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) { + userAuthorized.value = false + authenticate( + generalGetString(R.string.auth_unlock), + generalGetString(R.string.auth_log_in_using_credential), + this@MainActivity, + completed = { laResult -> + when (laResult) { + LAResult.Success -> userAuthorized.value = true + is LAResult.Error -> laErrorToast(applicationContext, laResult.errString) + LAResult.Failed -> laFailedToast(applicationContext) + LAResult.Unavailable -> { + userAuthorized.value = true + m.performLA.value = false + m.controller.appPrefs.performLA.set(false) + laUnavailableTurningOffAlert() + } } } - } - ) - } else { - userAuthorized.value = true + ) + } } } } @@ -137,8 +137,6 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { LAResult.Success -> { m.performLA.value = true prefPerformLA.set(true) - userAuthorized.value = true - lastLA.value = System.nanoTime() laTurnedOnAlert() } is LAResult.Error -> { @@ -206,7 +204,7 @@ fun MainPage( showLANotice: () -> Unit ) { // this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication - var chatsAccessAuthorized by remember { mutableStateOf(false) } + var chatsAccessAuthorized by remember { mutableStateOf(false) } LaunchedEffect(userAuthorized.value) { delay(500L) chatsAccessAuthorized = userAuthorized.value == true diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index f8b1242652..6382394249 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -72,7 +72,7 @@ Блокировка SimpleX включена Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме. - Разблокировать SimpleX + Разблокировать Пройдите аутентификацию Включить блокировку SimpleX Отключить блокировку SimpleX diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index cb1d5cc80e..cad76c4ab8 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -72,7 +72,7 @@ SimpleX Lock turned on You will be required to authenticate when you start or resume the app after 30 seconds in background. - Access chats + Unlock Log in using your credential Enable SimpleX Lock Disable SimpleX Lock diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 158392ec23..408edf0911 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -11,29 +11,29 @@ struct ContentView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var callController = CallController.shared - @State private var showNotificationAlert = false @Binding var userAuthorized: Bool? var body: some View { ZStack { - if let step = chatModel.onboardingStage { - if userAuthorized == true, - case .onboardingComplete = step, - let user = chatModel.currentUser { - ZStack(alignment: .top) { - ChatListView(user: user) - .onAppear { - NtfManager.shared.requestAuthorization(onDeny: { - alertManager.showAlert(notificationAlert()) - }) + if userAuthorized == true { + if let step = chatModel.onboardingStage { + if case .onboardingComplete = step, + let user = chatModel.currentUser { + ZStack(alignment: .top) { + ChatListView(user: user) + .onAppear { + NtfManager.shared.requestAuthorization(onDeny: { + alertManager.showAlert(notificationAlert()) + }) + } + if chatModel.showCallView, let call = chatModel.activeCall { + ActiveCallView(call: call) + } + IncomingCallView() } - if chatModel.showCallView, let call = chatModel.activeCall { - ActiveCallView(call: call) - } - IncomingCallView() + } else { + OnboardingView(onboarding: step) } - } else { - OnboardingView(onboarding: step) } } } @@ -99,11 +99,15 @@ final class AlertManager: ObservableObject { } func showAlertMsg(title: LocalizedStringKey, message: LocalizedStringKey? = nil) { - if let message = message { - showAlert(Alert(title: Text(title), message: Text(message))) - } else { - showAlert(Alert(title: Text(title))) - } + showAlert(mkAlert(title: title, message: message)) + } +} + +func mkAlert(title: LocalizedStringKey, message: LocalizedStringKey? = nil) -> Alert { + if let message = message { + return Alert(title: Text(title), message: Text(message)) + } else { + return Alert(title: Text(title)) } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 4d4ae6ad91..0337b2be80 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -14,6 +14,7 @@ import WebKit final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var currentUser: User? + @Published var performLA: Bool = false // list of chat "previews" @Published var chats: [Chat] = [] // current chat diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 0575fd587a..291befb628 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -14,10 +14,12 @@ let logger = Logger() struct SimpleXApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject private var chatModel = ChatModel.shared + @ObservedObject var alertManager = AlertManager.shared @Environment(\.scenePhase) var scenePhase + @AppStorage(DEFAULT_PERFORM_LA) private var performLA = false @State private var userAuthorized: Bool? = nil - @State private var doAuthenticate: Bool? = nil - @State private var lastLA: Double? = nil + @State private var doAuthenticate: Bool = true + @State private var enteredBackground: Double? = nil init() { hs_init(0, nil) @@ -35,8 +37,8 @@ struct SimpleXApp: App { chatModel.appOpenUrl = url } .onAppear() { + chatModel.performLA = performLA initializeChat() - doAuthenticate = true } .onChange(of: scenePhase) { phase in logger.debug("scenePhase \(String(describing: scenePhase))") @@ -45,10 +47,11 @@ struct SimpleXApp: App { case .background: BGManager.shared.schedule() doAuthenticate = true + enteredBackground = ProcessInfo.processInfo.systemUptime case .inactive: - authenticateUser() + authenticateOnPhaseChange() case .active: - authenticateUser() + authenticateOnPhaseChange() default: break } @@ -56,34 +59,37 @@ struct SimpleXApp: App { } } - private func authenticateUser() { - if doAuthenticate == true, - authenticationExpired() { + private func authenticateOnPhaseChange() { + if doAuthenticate { doAuthenticate = false - userAuthorized = false - authenticate() { laResult in - switch (laResult) { - case .success: - userAuthorized = true - lastLA = ProcessInfo.processInfo.systemUptime - case .failed: - laFailedAlert() - case .unavailable: - userAuthorized = true - laUnavailableAlert() + if !performLA { + userAuthorized = true + } else { + if authenticationExpired() { + userAuthorized = false + authenticate(reason: "Unlock") { laResult in + switch (laResult) { + case .success: + userAuthorized = true + case .failed: + AlertManager.shared.showAlert(laFailedAlert()) + case .unavailable: + userAuthorized = true + performLA = false + chatModel.performLA = false + AlertManager.shared.showAlert(laUnavailableTurningOffAlert()) + } + } } } } } private func authenticationExpired() -> Bool { - if (lastLA == nil) { - return true - } - else if let lastLA = lastLA, ProcessInfo.processInfo.systemUptime - lastLA >= 30 { - return true + if let enteredBackground = enteredBackground { + return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30 } else { - return false + return true } } } diff --git a/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift b/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift index 2cc1885640..4776f660c3 100644 --- a/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift +++ b/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift @@ -15,11 +15,10 @@ enum LAResult { case unavailable(authError: String?) } -func authenticate(completed: @escaping (LAResult) -> Void) { +func authenticate(reason: String, completed: @escaping (LAResult) -> Void) { let laContext = LAContext() var authAvailabilityError: NSError? if laContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authAvailabilityError) { - let reason = "Access chats" laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in DispatchQueue.main.async { if success { @@ -36,16 +35,30 @@ func authenticate(completed: @escaping (LAResult) -> Void) { } } -func laFailedAlert() { - AlertManager.shared.showAlertMsg( +func laTurnedOnAlert() -> Alert { + mkAlert( + title: "SimpleX Lock turned on", + message: "You will be required to authenticate when you start or resume the app after 30 seconds in background." + ) +} + +func laFailedAlert() -> Alert { + mkAlert( title: "Authentication failed", message: "You could not be verified; please try again." ) } -func laUnavailableAlert() { - AlertManager.shared.showAlertMsg( +func laUnavailableInstructionAlert() -> Alert { + mkAlert( title: "Authentication unavailable", - message: "Your device is not configured for authentication." + message: "Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." + ) +} + +func laUnavailableTurningOffAlert() -> Alert { + mkAlert( + title: "Authentication unavailable", + message: "Device authentication is disabled. Turning off SimpleX Lock." ) } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 214e9182f0..38d506b352 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -14,11 +14,13 @@ let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionS let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String +let DEFAULT_PERFORM_LA = "performLocalAuthentication" let DEFAULT_USE_NOTIFICATIONS = "useNotifications" let DEFAULT_PENDING_CONNECTIONS = "pendingConnections" let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let appDefaults: [String:Any] = [ + DEFAULT_PERFORM_LA: false, DEFAULT_USE_NOTIFICATIONS: false, DEFAULT_PENDING_CONNECTIONS: true, DEFAULT_WEBRTC_POLICY_RELAY: true @@ -30,10 +32,22 @@ struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool + @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false + @State private var performLAToggleReset = false @AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false @AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true @State var showNotificationsAlert: Bool = false @State var whichNotificationsAlert = NotificationAlert.enable + @State var alert: SettingsViewAlert? = nil + + enum SettingsViewAlert: Identifiable { + case laTurnedOnAlert + case laFailedAlert + case laUnavailableInstructionAlert + case laUnavailableTurningOffAlert + + var id: SettingsViewAlert { get { self } } + } var body: some View { let user: User = chatModel.currentUser! @@ -57,6 +71,9 @@ struct SettingsView: View { } Section("Settings") { + settingsRow("lock") { + Toggle("SimpleX Lock", isOn: $chatModel.performLA) + } settingsRow("link") { Toggle("Show pending connections", isOn: $pendingConnections) } @@ -133,6 +150,68 @@ struct SettingsView: View { } } .navigationTitle("Your settings") + .onChange(of: chatModel.performLA) { performLAToggle in + if performLAToggleReset { + performLAToggleReset = false + } else { + if performLAToggle { + enableLA() + } else { + disableLA() + } + } + } + .alert(item: $alert) { alertItem in + switch alertItem { + case .laTurnedOnAlert: return laTurnedOnAlert() + case .laFailedAlert: return laFailedAlert() + case .laUnavailableInstructionAlert: return laUnavailableInstructionAlert() + case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert() + } + } + } + } + + private func enableLA() { + authenticate(reason: "Enable SimpleX Lock") { laResult in + switch laResult { + case .success: + prefPerformLA = true + alert = .laTurnedOnAlert + case .failed: + prefPerformLA = false + withAnimation() { + chatModel.performLA = false + } + performLAToggleReset = true + alert = .laFailedAlert + case .unavailable: + prefPerformLA = false + withAnimation() { + chatModel.performLA = false + } + performLAToggleReset = true + alert = .laUnavailableInstructionAlert + } + } + } + + private func disableLA() { + authenticate(reason: "Disable SimpleX Lock") { laResult in + switch (laResult) { + case .success: + prefPerformLA = false + case .failed: + prefPerformLA = true + withAnimation() { + chatModel.performLA = true + } + performLAToggleReset = true + alert = .laFailedAlert + case .unavailable: + prefPerformLA = false + alert = .laUnavailableTurningOffAlert + } } }