ios: lock toggle; android: fix lock timer (#702)

This commit is contained in:
JRoberts
2022-05-28 14:58:52 +04:00
committed by GitHub
parent ce2f3c0371
commit 5e476516cb
9 changed files with 190 additions and 88 deletions
+1
View File
@@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
@@ -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<SimplexViewModel>()
private val chatController by lazy { (application as SimplexApp).chatController }
private val userAuthorized = mutableStateOf<Boolean?>(null)
private val lastLA = mutableStateOf<Long?>(null)
private val enteredBackground = mutableStateOf<Long?>(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<Boolean>(false) }
var chatsAccessAuthorized by remember { mutableStateOf(false) }
LaunchedEffect(userAuthorized.value) {
delay(500L)
chatsAccessAuthorized = userAuthorized.value == true
@@ -72,7 +72,7 @@
<!-- LocalAuthentication.kt -->
<string name="auth_turned_on">Блокировка SimpleX включена</string>
<string name="auth_turned_on_desc">Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.</string>
<string name="auth_access_chats">Разблокировать SimpleX</string>
<string name="auth_unlock">Разблокировать</string>
<string name="auth_log_in_using_credential">Пройдите аутентификацию</string>
<string name="auth_enable">Включить блокировку SimpleX</string>
<string name="auth_disable">Отключить блокировку SimpleX</string>
@@ -72,7 +72,7 @@
<!-- LocalAuthentication.kt -->
<string name="auth_turned_on">SimpleX Lock turned on</string>
<string name="auth_turned_on_desc">You will be required to authenticate when you start or resume the app after 30 seconds in background.</string>
<string name="auth_access_chats">Access chats</string>
<string name="auth_unlock">Unlock</string>
<string name="auth_log_in_using_credential">Log in using your credential</string>
<string name="auth_enable">Enable SimpleX Lock</string>
<string name="auth_disable">Disable SimpleX Lock</string>
+26 -22
View File
@@ -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))
}
}
+1
View File
@@ -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
+31 -25
View File
@@ -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
}
}
}
@@ -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."
)
}
@@ -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
}
}
}