mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-03 16:17:45 +00:00
ios: lock toggle; android: fix lock timer (#702)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user