Compare commits

...

12 Commits

Author SHA1 Message Date
Evgeny Poberezkin 2f730d54e9 6.0.5: ios 239, android 241, desktop 68 2024-09-24 21:48:30 +01:00
Evgeny Poberezkin fe0013c4a9 ios: update core library 2024-09-24 17:51:34 +01:00
Stanislav Dmitrenko 5261886b31 android, desktop: proxy configuration includes credentials (#4892)
* android, desktop: proxy configuration includes credentials

* migration

* changes for disabled socks

* migration

* port

* new logic

* migration

* check validity of fields

* validity of host

* import changes proxy just in case

* send port always

* non-nullable

* Revert "send port always"

This reverts commit 14dd066d80.

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-24 17:07:39 +01:00
Evgeny 93ab3076d4 ios: SOCKS proxy UI (#4893)
* ios: SOCKS proxy UI

* update network config

* proxy

* adapt

* move, dont default to localhost:9050

* move socks proxy to defaults

* sock proxy preference

* rename

* rename

* fix

* fix

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-24 17:06:00 +01:00
Stanislav Dmitrenko 3b88ddbd4f desktop: fix vlc dependency (2) (#4869) 2024-09-24 17:04:08 +01:00
Evgeny Poberezkin d6dc35738e core: 6.0.5.0 (simplexmq 6.0.5.0) 2024-09-24 12:42:22 +01:00
Evgeny 5b3aba9db2 ci: dont build when files in core do not change (#4797) 2024-09-24 12:40:58 +01:00
Stanislav Dmitrenko 981cbb8bf9 android: target API level 34 (Android 14) (#4697) 2024-09-23 09:05:14 +01:00
Stanislav Dmitrenko 25fb099c44 android, desktop: fix always connecting state in call (#4911) 2024-09-19 18:00:08 +01:00
Evgeny 40e93cc61e core: reduce max message sizes (#4882)
* core: reduce max message sizes

* reduce

* comment
2024-09-16 18:05:09 +01:00
Evgeny c22d23750f core: support different SOCKS proxy authentication modes (#4886)
* core: support different SOCKS proxy authentication modes

* use defaultSocksProxyWithAuth

* hostMode CLI option

* simplexmq
2024-09-16 07:33:48 +01:00
Stanislav Dmitrenko 2539255957 android: remove Worker provider's interface implementation (#4874) 2024-09-13 12:35:12 +01:00
37 changed files with 833 additions and 375 deletions
+11 -1
View File
@@ -5,12 +5,22 @@ on:
branches:
- master
- stable
- users
tags:
- "v*"
- "!*-fdroid"
- "!*-armv7a"
pull_request:
paths-ignore:
- "apps/ios"
- "apps/multiplatform"
- "blog"
- "docs"
- "fastlane"
- "images"
- "packages"
- "website"
- "README.md"
- "PRIVACY.md"
jobs:
prepare-release:
@@ -407,12 +407,18 @@ struct ServersSummaryView: View {
struct SubscriptionStatusIndicatorView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var subs: SMPServerSubs
var hasSess: Bool
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(
online: m.networkInfo.online,
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
subs: subs,
hasSess: hasSess,
primaryColor: theme.colors.primary
)
if #available(iOS 16.0, *) {
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
.foregroundColor(color)
@@ -425,26 +431,32 @@ struct SubscriptionStatusIndicatorView: View {
struct SubscriptionStatusPercentageView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var subs: SMPServerSubs
var hasSess: Bool
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(
online: m.networkInfo.online,
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
subs: subs,
hasSess: hasSess,
primaryColor: theme.colors.primary
)
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
.foregroundColor(.secondary)
.font(.caption)
}
}
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) {
func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) {
func roundedToQuarter(_ n: Double) -> Double {
n >= 1 ? 1
: n <= 0 ? 0
: (n * 4).rounded() / 4
}
let activeColor: Color = onionHosts == .require ? .indigo : .accentColor
let activeColor: Color = usesProxy ? .indigo : primaryColor
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
@@ -534,9 +534,15 @@ struct MigrateFromDevice: View {
}
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
let cfg = getNetCfg()
let proxy: NetworkProxy? = if cfg.socksProxy == nil {
nil
} else {
networkProxyDefault.get()
}
let data = MigrationFileLinkData.init(
networkConfig: MigrationFileLinkData.NetworkConfig(
socksProxy: cfg.socksProxy,
networkProxy: proxy,
hostMode: cfg.hostMode,
requiredHostMode: cfg.requiredHostMode
)
@@ -36,6 +36,10 @@ struct AdvancedNetworkSettings: View {
@State private var showSettingsAlert: NetworkSettingsAlert?
@State private var onionHosts: OnionHosts = .no
@State private var showSaveDialog = false
@State private var netProxy = networkProxyDefault.get()
@State private var currentNetProxy = networkProxyDefault.get()
@State private var useNetProxy = false
@State private var netProxyAuth = false
var body: some View {
VStack {
@@ -102,6 +106,76 @@ struct AdvancedNetworkSettings: View {
.foregroundColor(theme.colors.secondary)
}
Section {
Toggle("Use SOCKS proxy", isOn: $useNetProxy)
Group {
TextField("IP address", text: $netProxy.host)
TextField(
"Port",
text: Binding(
get: { netProxy.port > 0 ? "\(netProxy.port)" : "" },
set: { s in
netProxy.port = if let port = Int(s), port > 0 {
port
} else {
0
}
}
)
)
Toggle("Proxy requires password", isOn: $netProxyAuth)
if netProxyAuth {
TextField("Username", text: $netProxy.username)
PassphraseField(
key: $netProxy.password,
placeholder: "Password",
valid: NetworkProxy.validCredential(netProxy.password)
)
}
}
.if(!useNetProxy) { $0.foregroundColor(theme.colors.secondary) }
.disabled(!useNetProxy)
} header: {
HStack {
Text("SOCKS proxy").foregroundColor(theme.colors.secondary)
if useNetProxy && !netProxy.valid {
Spacer()
Image(systemName: "exclamationmark.circle.fill").foregroundColor(.red)
}
}
} footer: {
if netProxyAuth {
Text("Your credentials may be sent unencrypted.")
.foregroundColor(theme.colors.secondary)
} else {
Text("Do not use credentials with proxy.")
.foregroundColor(theme.colors.secondary)
}
}
.onChange(of: useNetProxy) { useNetProxy in
netCfg.socksProxy = useNetProxy && currentNetProxy.valid
? currentNetProxy.toProxyString()
: nil
netProxy = currentNetProxy
netProxyAuth = netProxy.username != "" || netProxy.password != ""
}
.onChange(of: netProxyAuth) { netProxyAuth in
if netProxyAuth {
netProxy.auth = currentNetProxy.auth
netProxy.username = currentNetProxy.username
netProxy.password = currentNetProxy.password
} else {
netProxy.auth = .username
netProxy.username = ""
netProxy.password = ""
}
}
.onChange(of: netProxy) { netProxy in
netCfg.socksProxy = useNetProxy && netProxy.valid
? netProxy.toProxyString()
: nil
}
Section {
Picker("Use .onion hosts", selection: $onionHosts) {
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
@@ -156,19 +230,19 @@ struct AdvancedNetworkSettings: View {
Section {
Button("Reset to defaults") {
updateNetCfgView(NetCfg.defaults)
updateNetCfgView(NetCfg.defaults, NetworkProxy.def)
}
.disabled(netCfg == NetCfg.defaults)
Button("Set timeouts for proxy/VPN") {
updateNetCfgView(netCfg.withProxyTimeouts)
updateNetCfgView(netCfg.withProxyTimeouts, netProxy)
}
.disabled(netCfg.hasProxyTimeouts)
Button("Save and reconnect") {
showSettingsAlert = .update
}
.disabled(netCfg == currentNetCfg)
.disabled(netCfg == currentNetCfg || (useNetProxy && !netProxy.valid))
}
}
}
@@ -182,7 +256,8 @@ struct AdvancedNetworkSettings: View {
if cfgLoaded { return }
cfgLoaded = true
currentNetCfg = getNetCfg()
updateNetCfgView(currentNetCfg)
currentNetProxy = networkProxyDefault.get()
updateNetCfgView(currentNetCfg, currentNetProxy)
}
.alert(item: $showSettingsAlert) { a in
switch a {
@@ -206,7 +281,7 @@ struct AdvancedNetworkSettings: View {
if netCfg == currentNetCfg {
dismiss()
cfgLoaded = false
} else {
} else if !useNetProxy || netProxy.valid {
showSaveDialog = true
}
})
@@ -221,18 +296,26 @@ struct AdvancedNetworkSettings: View {
}
}
private func updateNetCfgView(_ cfg: NetCfg) {
private func updateNetCfgView(_ cfg: NetCfg, _ proxy: NetworkProxy) {
netCfg = cfg
netProxy = proxy
onionHosts = OnionHosts(netCfg: netCfg)
enableKeepAlive = netCfg.enableKeepAlive
keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults
useNetProxy = netCfg.socksProxy != nil
netProxyAuth = switch netProxy.auth {
case .username: netProxy.username != "" || netProxy.password != ""
case .isolate: false
}
}
private func saveNetCfg() -> Bool {
do {
try setNetworkConfig(netCfg)
currentNetCfg = netCfg
setNetCfg(netCfg)
setNetCfg(netCfg, networkProxy: useNetProxy ? netProxy : nil)
currentNetProxy = netProxy
networkProxyDefault.set(netProxy)
return true
} catch let error {
let err = responseError(error)
@@ -19,9 +19,15 @@ extension AppSettings {
val.hostMode = .publicHost
val.requiredHostMode = true
}
val.socksProxy = nil
setNetCfg(val)
if val.socksProxy != nil {
val.socksProxy = networkProxy?.toProxyString()
setNetCfg(val, networkProxy: networkProxy)
} else {
val.socksProxy = nil
setNetCfg(val, networkProxy: nil)
}
}
if let val = networkProxy { networkProxyDefault.set(val) }
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
if let val = privacyAcceptImages {
@@ -63,6 +69,7 @@ extension AppSettings {
let def = UserDefaults.standard
var c = AppSettings.defaults
c.networkConfig = getNetCfg()
c.networkProxy = networkProxyDefault.get()
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
@@ -73,6 +73,8 @@ let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
let DEFAULT_NETWORK_PROXY = "networkProxy"
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
let appDefaults: [String: Any] = [
@@ -245,6 +247,7 @@ public class CodableDefault<T: Codable> {
}
}
let networkProxyDefault: CodableDefault<NetworkProxy> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_NETWORK_PROXY, withDefault: NetworkProxy.def)
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
+40 -40
View File
@@ -216,11 +216,11 @@
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84482C8220D0008C24D1 /* libffi.a */; };
E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84492C8220D0008C24D1 /* libgmpxx.a */; };
E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */; };
E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */; };
E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844C2C8220D0008C24D1 /* libgmp.a */; };
E5CC47842CA31C3A00551ACF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC477F2CA31C3900551ACF /* libgmpxx.a */; };
E5CC47852CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */; };
E5CC47862CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */; };
E5CC47872CA31C3A00551ACF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47822CA31C3A00551ACF /* libgmp.a */; };
E5CC47882CA31C3A00551ACF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47832CA31C3A00551ACF /* libffi.a */; };
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
@@ -554,11 +554,11 @@
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
E5BD84482C8220D0008C24D1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5BD84492C8220D0008C24D1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a"; sourceTree = "<group>"; };
E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a"; sourceTree = "<group>"; };
E5BD844C2C8220D0008C24D1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5CC477F2CA31C3900551ACF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a"; sourceTree = "<group>"; };
E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a"; sourceTree = "<group>"; };
E5CC47822CA31C3A00551ACF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5CC47832CA31C3A00551ACF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -649,14 +649,14 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E5CC47852CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a in Frameworks */,
E5CC47872CA31C3A00551ACF /* libgmp.a in Frameworks */,
E5CC47842CA31C3A00551ACF /* libgmpxx.a in Frameworks */,
E5CC47882CA31C3A00551ACF /* libffi.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */,
E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */,
E5CC47862CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */,
E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */,
E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -733,11 +733,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E5BD84482C8220D0008C24D1 /* libffi.a */,
E5BD844C2C8220D0008C24D1 /* libgmp.a */,
E5BD84492C8220D0008C24D1 /* libgmpxx.a */,
E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */,
E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */,
E5CC47832CA31C3A00551ACF /* libffi.a */,
E5CC47822CA31C3A00551ACF /* libgmp.a */,
E5CC477F2CA31C3900551ACF /* libgmpxx.a */,
E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */,
E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1887,7 +1887,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1912,7 +1912,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1936,7 +1936,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1961,7 +1961,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1977,11 +1977,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1997,11 +1997,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2022,7 +2022,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2037,7 +2037,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2059,7 +2059,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2074,7 +2074,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2096,7 +2096,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2122,7 +2122,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2147,7 +2147,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2173,7 +2173,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2198,7 +2198,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2213,7 +2213,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2232,7 +2232,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 239;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2247,7 +2247,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+65 -1
View File
@@ -8,6 +8,7 @@
import Foundation
import SwiftUI
import Network
public let jsonDecoder = getJSONDecoder()
public let jsonEncoder = getJSONEncoder()
@@ -1476,6 +1477,63 @@ public struct KeepAliveOpts: Codable, Equatable {
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
}
public struct NetworkProxy: Equatable, Codable {
public var host: String = ""
public var port: Int = 0
public var auth: NetworkProxyAuth = .username
public var username: String = ""
public var password: String = ""
public static var def: NetworkProxy {
NetworkProxy()
}
public var valid: Bool {
let hostOk = switch NWEndpoint.Host(host) {
case .ipv4: true
case .ipv6: true
default: false
}
return hostOk &&
port > 0 && port <= 65535 &&
NetworkProxy.validCredential(username) && NetworkProxy.validCredential(password)
}
public static func validCredential(_ s: String) -> Bool {
!s.contains(":") && !s.contains("@")
}
public func toProxyString() -> String? {
if !valid { return nil }
var res = ""
switch auth {
case .username:
let usernameTrimmed = username.trimmingCharacters(in: .whitespaces)
let passwordTrimmed = password.trimmingCharacters(in: .whitespaces)
if usernameTrimmed != "" || passwordTrimmed != "" {
res += usernameTrimmed + ":" + passwordTrimmed + "@"
} else {
res += "@"
}
case .isolate: ()
}
if host != "" {
if host.contains(":") {
res += "[\(host.trimmingCharacters(in: [" ", "[", "]"]))]"
} else {
res += host.trimmingCharacters(in: .whitespaces)
}
}
res += ":\(port)"
return res
}
}
public enum NetworkProxyAuth: String, Codable {
case username
case isolate
}
public enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
@@ -2099,11 +2157,13 @@ public struct MigrationFileLinkData: Codable {
public struct NetworkConfig: Codable {
let socksProxy: String?
let networkProxy: NetworkProxy?
let hostMode: HostMode?
let requiredHostMode: Bool?
public init(socksProxy: String?, hostMode: HostMode?, requiredHostMode: Bool?) {
public init(socksProxy: String?, networkProxy: NetworkProxy?, hostMode: HostMode?, requiredHostMode: Bool?) {
self.socksProxy = socksProxy
self.networkProxy = networkProxy
self.hostMode = hostMode
self.requiredHostMode = requiredHostMode
}
@@ -2112,6 +2172,7 @@ public struct MigrationFileLinkData: Codable {
return if let hostMode, let requiredHostMode {
NetworkConfig(
socksProxy: nil,
networkProxy: nil,
hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode,
requiredHostMode: requiredHostMode
)
@@ -2131,6 +2192,7 @@ public struct MigrationFileLinkData: Codable {
public struct AppSettings: Codable, Equatable {
public var networkConfig: NetCfg? = nil
public var networkProxy: NetworkProxy? = nil
public var privacyEncryptLocalFiles: Bool? = nil
public var privacyAskToApproveRelays: Bool? = nil
public var privacyAcceptImages: Bool? = nil
@@ -2162,6 +2224,7 @@ public struct AppSettings: Codable, Equatable {
var empty = AppSettings()
let def = AppSettings.defaults
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
if networkProxy != def.networkProxy { empty.networkProxy = networkProxy }
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
@@ -2194,6 +2257,7 @@ public struct AppSettings: Codable, Equatable {
public static var defaults: AppSettings {
AppSettings (
networkConfig: NetCfg.defaults,
networkProxy: NetworkProxy.def,
privacyEncryptLocalFiles: true,
privacyAskToApproveRelays: true,
privacyAcceptImages: true,
+6 -1
View File
@@ -35,6 +35,7 @@ public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRel
// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS
public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
public let GROUP_DEFAULT_NETWORK_SOCKS_PROXY = "networkSocksProxy"
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode"
@@ -327,6 +328,7 @@ public class Default<T> {
}
public func getNetCfg() -> NetCfg {
let socksProxy = groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY)
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (hostMode, requiredHostMode) = onionHosts.hostMode
let sessionMode = networkSessionModeGroupDefault.get()
@@ -349,6 +351,7 @@ public func getNetCfg() -> NetCfg {
tcpKeepAlive = nil
}
return NetCfg(
socksProxy: socksProxy,
hostMode: hostMode,
requiredHostMode: requiredHostMode,
sessionMode: sessionMode,
@@ -365,11 +368,13 @@ public func getNetCfg() -> NetCfg {
)
}
public func setNetCfg(_ cfg: NetCfg) {
public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) {
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
networkSessionModeGroupDefault.set(cfg.sessionMode)
networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode)
networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback)
let socksProxy = networkProxy?.toProxyString()
groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY)
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
+11 -11
View File
@@ -15,7 +15,7 @@ android {
namespace = "chat.simplex.app"
minSdk = 26
//noinspection OldTargetApi
targetSdk = 33
targetSdk = 34
// !!!
// skip version code after release to F-Droid, as it uses two version codes
versionCode = (extra["android.version_code"] as String).toInt()
@@ -126,29 +126,29 @@ android {
dependencies {
implementation(project(":common"))
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.13.1")
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
//implementation("androidx.compose.material:material:$compose_version")
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
val workVersion = "2.9.0"
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("androidx.work:work-multiprocess:$workVersion")
implementation("com.jakewharton:process-phoenix:2.2.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
//androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.4")
}
@@ -21,6 +21,12 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- Requirements that allows to specify foreground service types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:name="SimplexApp"
android:allowBackup="false"
@@ -133,7 +139,9 @@
android:name=".SimplexService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"></service>
android:stopWithTask="false"
android:foregroundServiceType="remoteMessaging"
/>
<!-- SimplexService restart on reboot -->
@@ -141,7 +149,9 @@
android:name=".CallService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"/>
android:stopWithTask="false"
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
/>
<receiver
android:name=".CallService$CallActionReceiver"
@@ -2,17 +2,18 @@ package chat.simplex.app
import android.app.*
import android.content.*
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.*
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import chat.simplex.app.model.NtfManager.EndCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.model.NotificationPreviewMode
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.CallState
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Instant
@@ -34,7 +35,7 @@ class CallService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(CALL_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
return START_STICKY
}
@@ -42,8 +43,7 @@ class CallService: Service() {
super.onCreate()
Log.d(TAG, "Call service created")
notificationManager = createNotificationChannel()
updateNotification()
startForeground(CALL_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType())
}
override fun onDestroy() {
@@ -69,7 +69,14 @@ class CallService: Service() {
}
}
fun updateNotification() {
private fun createNotificationIfNeeded(): Notification {
val ntf = serviceNotification
if (ntf != null) return ntf
return updateNotification()
}
fun updateNotification(): Notification {
val call = chatModel.activeCall.value
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
@@ -83,8 +90,31 @@ class CallService: Service() {
else
base64ToBitmap(image).asAndroidBitmap()
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
startForeground(CALL_SERVICE_ID, serviceNotification)
val ntf = createNotification(title, text, largeIcon, call?.connectedAt)
serviceNotification = ntf
ServiceCompat.startForeground(this, CALL_SERVICE_ID, ntf, foregroundServiceType())
return ntf
}
private fun foregroundServiceType(): Int {
val call = chatModel.activeCall.value
return if (call == null) {
if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
0
}
} else if (Build.VERSION.SDK_INT >= 30) {
if (call.supportsVideo()) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
} else if (Build.VERSION.SDK_INT >= 29) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
} else {
0
}
}
private fun createNotificationChannel(): NotificationManager? {
@@ -54,7 +54,7 @@ class MainActivity: FragmentActivity() {
SimplexApp.context.schedulePeriodicWakeUp()
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
processIntent(intent)
processExternalIntent(intent)
@@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit
const val TAG = "SIMPLEX"
class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider {
class SimplexApp: Application(), LifecycleEventObserver {
val chatModel: ChatModel
get() = chatController.chatModel
@@ -119,7 +119,10 @@ class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider
* */
if (chatModel.chatRunning.value != false &&
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
// New installation passes all checks above and tries to start the service which is not needed at all
// because preferred notification type is not yet chosen. So, check that the user has initialized db already
appPrefs.newDatabaseInitialized.get()
) {
SimplexService.start()
}
@@ -363,9 +366,4 @@ class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider
}
}
}
// Fix for an exception:
// WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().build()
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.*
import android.os.SystemClock
@@ -15,8 +16,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.common.AppLock
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
@@ -52,18 +55,15 @@ class SimplexService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
return START_STICKY // to restart if killed
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Simplex service created")
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
notificationManager = createNotificationChannel()
serviceNotification = createNotification(title, text)
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
createNotificationIfNeeded()
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
/**
* The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and
* we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown.
@@ -103,6 +103,26 @@ class SimplexService: Service() {
super.onDestroy()
}
private fun createNotificationIfNeeded(): Notification {
val ntf = serviceNotification
if (ntf != null) return ntf
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
notificationManager = createNotificationChannel()
val newNtf = createNotification(title, text)
serviceNotification = newNtf
return newNtf
}
private fun foregroundServiceType(): Int {
return if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
0
}
}
private fun startService() {
Log.d(TAG, "SimplexService startService")
if (wakeLock != null || isCheckingNewMessages) return
@@ -292,6 +312,10 @@ class SimplexService: Service() {
}
private suspend fun serviceAction(action: Action) {
if (!NtfManager.areNotificationsEnabledInSystem()) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service")
return
}
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) {
Intent(androidAppContext, SimplexService::class.java).also {
@@ -53,7 +53,7 @@ object NtfManager {
private val msgNtfTimeoutMs = 30000L
init {
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert()
}
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
@@ -287,6 +287,8 @@ object NtfManager {
}
}
fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled()
/**
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
@@ -120,6 +120,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
return grantedAudio && grantedCamera
}
@Deprecated("Was deprecated in OS")
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
@@ -139,6 +140,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
@@ -248,6 +250,9 @@ fun CallActivityView() {
)
if (permissionsState.allPermissionsGranted) {
ActiveCallView()
LaunchedEffect(Unit) {
activity.startServiceAndBind()
}
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
withBGApi { chatModel.callManager.endCall(call) }
@@ -285,11 +290,6 @@ fun CallActivityView() {
AlertManager.shared.showInView()
}
}
LaunchedEffect(call == null) {
if (call != null) {
activity.startServiceAndBind()
}
}
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "CallActivityView: finishing activity")
@@ -2,6 +2,5 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="highOrLowLight">#8b8786</color>
<color name="window_background_dark">#121212</color>
</resources>
+14 -9
View File
@@ -61,8 +61,8 @@ kotlin {
val androidMain by getting {
kotlin.srcDir("build/generated/moko/androidMain/src")
dependencies {
implementation("androidx.activity:activity-compose:1.8.2")
val workVersion = "2.9.0"
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("com.google.accompanist:accompanist-insets:0.30.1")
@@ -78,31 +78,36 @@ kotlin {
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
implementation("androidx.webkit:webkit:1.10.0")
implementation("androidx.webkit:webkit:1.11.0")
// GIFs support
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("io.coil-kt:coil-gif:2.6.0")
implementation("com.jakewharton:process-phoenix:2.2.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
val cameraXVersion = "1.3.2"
val cameraXVersion = "1.3.4"
implementation("androidx.camera:camera-core:${cameraXVersion}")
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
implementation("androidx.camera:camera-view:${cameraXVersion}")
// Calls lifecycle listener
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
}
}
val desktopMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0")
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") {
exclude("net.java.dev.jna")
}
// For jSystemThemeDetector only
implementation("net.java.dev.jna:jna-platform:5.14.0")
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
implementation("org.slf4j:slf4j-simple:2.0.12")
implementation("uk.co.caprica:vlcj:4.8.3")
implementation("net.java.dev.jna:jna:5.14.0")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
@@ -119,8 +124,8 @@ android {
defaultConfig {
minSdk = 26
}
testOptions.targetSdk = 33
lint.targetSdk = 33
testOptions.targetSdk = 34
lint.targetSdk = 34
val isAndroid = gradle.startParameter.taskNames.find {
val lower = it.lowercase()
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/highOrLowLight" />
<solid android:color="#8b8786" />
<size android:width="1dp" />
</shape>
@@ -129,7 +129,22 @@ class AppPreferences {
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false)
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
private val _networkProxy = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, json.encodeToString(NetworkProxy()))
val networkProxy: SharedPreference<NetworkProxy> = SharedPreference(
get = fun(): NetworkProxy {
val value = _networkProxy.get() ?: return NetworkProxy()
return try {
if (value.startsWith("{")) {
json.decodeFromString(value)
} else {
NetworkProxy(host = value.substringBefore(":").ifBlank { "localhost" }, port = value.substringAfter(":").toIntOrNull() ?: 9050)
}
} catch (e: Throwable) {
NetworkProxy()
}
},
set = fun(proxy: NetworkProxy) { _networkProxy.set(json.encodeToString(proxy)) }
)
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
get = fun(): TransportSessionMode {
@@ -531,7 +546,7 @@ object ChatController {
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
Log.d(TAG, "startChatWithTemporaryDatabase")
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
if (!apiSetNetworkConfig(netCfg, ctrl)) {
if (!apiSetNetworkConfig(netCfg, ctrl = ctrl)) {
Log.e(TAG, "Error setting network config, stopping migration")
return null
}
@@ -976,16 +991,18 @@ object ChatController {
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
}
suspend fun apiSetNetworkConfig(cfg: NetCfg, ctrl: ChatCtrl? = null): Boolean {
suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean {
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
return when (r) {
is CR.CmdOk -> true
else -> {
Log.e(TAG, "apiSetNetworkConfig bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.error_setting_network_config),
"${r.responseType}: ${r.details}"
)
if (showAlertOnError) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.error_setting_network_config),
"${r.responseType}: ${r.details}"
)
}
false
}
}
@@ -2745,13 +2762,9 @@ object ChatController {
fun getNetCfg(): NetCfg {
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
val proxyHostPort = appPrefs.networkProxyHostPort.get()
val networkProxy = appPrefs.networkProxy.get()
val socksProxy = if (useSocksProxy) {
if (proxyHostPort?.startsWith("localhost:") == true) {
proxyHostPort.removePrefix("localhost")
} else {
proxyHostPort ?: ":9050"
}
networkProxy.toProxyString()
} else {
null
}
@@ -2793,7 +2806,7 @@ object ChatController {
}
/**
* [AppPreferences.networkProxyHostPort] is not changed here, use appPrefs to set it
* [AppPreferences.networkProxy] is not changed here, use appPrefs to set it
* */
fun setNetCfg(cfg: NetCfg) {
appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy)
@@ -3529,13 +3542,8 @@ data class NetCfg(
val useSocksProxy: Boolean get() = socksProxy != null
val enableKeepAlive: Boolean get() = tcpKeepAlive != null
fun withHostPort(hostPort: String?, default: String? = ":9050"): NetCfg {
val socksProxy = if (hostPort?.startsWith("localhost:") == true) {
hostPort.removePrefix("localhost")
} else {
hostPort ?: default
}
return copy(socksProxy = socksProxy)
fun withProxy(proxy: NetworkProxy?, default: String? = ":9050"): NetCfg {
return copy(socksProxy = proxy?.toProxyString() ?: default)
}
companion object {
@@ -3577,6 +3585,39 @@ data class NetCfg(
}
}
@Serializable
data class NetworkProxy(
val username: String = "",
val password: String = "",
val auth: NetworkProxyAuth = NetworkProxyAuth.ISOLATE,
val host: String = "localhost",
val port: Int = 9050
) {
fun toProxyString(): String {
var res = ""
if (auth == NetworkProxyAuth.USERNAME && (username.isNotBlank() || password.isNotBlank())) {
res += username.trim() + ":" + password.trim() + "@"
} else if (auth == NetworkProxyAuth.USERNAME) {
res += "@"
}
if (host != "localhost") {
res += if (host.contains(':')) "[${host.trim(' ', '[', ']')}]" else host.trim()
}
if (port != 9050 || res.isEmpty()) {
res += ":$port"
}
return res
}
}
@Serializable
enum class NetworkProxyAuth {
@SerialName("isolate")
ISOLATE,
@SerialName("username")
USERNAME,
}
enum class OnionHosts {
NEVER, PREFER, REQUIRED
}
@@ -6139,6 +6180,7 @@ enum class NotificationsMode() {
@Serializable
data class AppSettings(
var networkConfig: NetCfg? = null,
var networkProxy: NetworkProxy? = null,
var privacyEncryptLocalFiles: Boolean? = null,
var privacyAskToApproveRelays: Boolean? = null,
var privacyAcceptImages: Boolean? = null,
@@ -6170,6 +6212,7 @@ data class AppSettings(
val empty = AppSettings()
val def = defaults
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
if (networkProxy != def.networkProxy) { empty.networkProxy = networkProxy }
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
@@ -6207,8 +6250,12 @@ data class AppSettings(
if (net.hostMode == HostMode.Onion) {
net = net.copy(hostMode = HostMode.Public, requiredHostMode = true)
}
if (net.socksProxy != null) {
net = net.copy(socksProxy = networkProxy?.toProxyString())
}
setNetCfg(net)
}
networkProxy?.let { def.networkProxy.set(it) }
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
@@ -6241,6 +6288,7 @@ data class AppSettings(
val defaults: AppSettings
get() = AppSettings(
networkConfig = NetCfg.defaults,
networkProxy = null,
privacyEncryptLocalFiles = true,
privacyAskToApproveRelays = true,
privacyAcceptImages = true,
@@ -6274,6 +6322,7 @@ data class AppSettings(
val def = appPreferences
return defaults.copy(
networkConfig = getNetCfg(),
networkProxy = def.networkProxy.get(),
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
privacyAcceptImages = def.privacyAcceptImages.get(),
@@ -4,7 +4,6 @@ import SectionBottomSpacer
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -17,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.startChat
import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase
@@ -38,7 +38,6 @@ import kotlinx.serialization.*
import java.io.File
import java.net.URLEncoder
import kotlin.math.max
import kotlin.math.sqrt
@Serializable
data class MigrationFileLinkData(
@@ -46,16 +45,20 @@ data class MigrationFileLinkData(
) {
@Serializable
data class NetworkConfig(
val socksProxy: String?,
// Legacy. Remove in 2025
@SerialName("socksProxy")
val legacySocksProxy: String?,
val networkProxy: NetworkProxy?,
val hostMode: HostMode?,
val requiredHostMode: Boolean?
) {
fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion
fun hasProxyConfigured(): Boolean = networkProxy != null || legacySocksProxy != null || hostMode == HostMode.Onion
fun transformToPlatformSupported(): NetworkConfig {
return if (hostMode != null && requiredHostMode != null) {
NetworkConfig(
socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy,
legacySocksProxy = if (hostMode == HostMode.Onion) legacySocksProxy ?: NetCfg.proxyDefaults.socksProxy else legacySocksProxy,
networkProxy = if (hostMode == HostMode.Onion) networkProxy ?: NetworkProxy() else networkProxy,
hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode,
requiredHostMode = requiredHostMode
)
@@ -570,7 +573,8 @@ private fun MutableState<MigrationFromState>.startUploading(
val cfg = getNetCfg()
val data = MigrationFileLinkData(
networkConfig = MigrationFileLinkData.NetworkConfig(
socksProxy = cfg.socksProxy,
legacySocksProxy = null,
networkProxy = if (appPrefs.networkUseSocksProxy.get()) appPrefs.networkProxy.get() else null,
hostMode = cfg.hostMode,
requiredHostMode = cfg.requiredHostMode
)
@@ -5,16 +5,15 @@ import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import chat.simplex.common.model.*
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.startChat
import chat.simplex.common.model.ChatCtrl
@@ -41,10 +40,10 @@ import kotlin.math.max
@Serializable
sealed class MigrationToDeviceState {
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val networkProxy: NetworkProxy?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
companion object {
// Here we check whether it's needed to show migration process after app restart or not
@@ -66,10 +65,10 @@ sealed class MigrationToDeviceState {
null
} else {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg, state.networkProxy)
}
}
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
is Passphrase -> MigrationToState.Passphrase("", state.netCfg, state.networkProxy)
}
if (initial == null) {
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
@@ -91,16 +90,24 @@ sealed class MigrationToDeviceState {
@Serializable
sealed class MigrationToState {
@Serializable object PasteOrScanLink: MigrationToState()
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState()
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
@Serializable data class Onion(
val link: String,
// Legacy, remove in 2025
@SerialName("socksProxy")
val legacySocksProxy: String?,
val networkProxy: NetworkProxy?,
val hostMode: HostMode,
val requiredHostMode: Boolean
): MigrationToState()
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?, val ctrl: ChatCtrl?): MigrationToState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
}
private var MutableState<MigrationToState?>.state: MigrationToState?
@@ -175,16 +182,16 @@ private fun ModalData.SectionByState(
when (val s = migrationState.value) {
null -> {}
is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
is MigrationToState.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg, s.networkProxy)
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg, s.networkProxy)
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg, s.networkProxy)
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg, s.networkProxy)
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg, s.networkProxy)
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg, s.networkProxy)
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg, s.networkProxy)
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, s.networkProxy, close)
}
}
@@ -216,21 +223,24 @@ private fun MutableState<MigrationToState?>.PasteLinkView() {
}
@Composable
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
val onionHosts = remember { stateGetOrPut("onionHosts") {
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
} }
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } }
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { linkNetworkProxy != null || legacyLinkSocksProxy != null } }
val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} }
val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") {
var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get())
if (proxy?.startsWith(":") == true) proxy = "localhost$proxy"
proxy
}
val networkProxy = remember { stateGetOrPut("networkProxy") {
linkNetworkProxy
?: if (legacyLinkSocksProxy != null) {
NetworkProxy(host = legacyLinkSocksProxy.substringBefore(":").ifBlank { "localhost" }, port = legacyLinkSocksProxy.substringAfter(":").toIntOrNull() ?: 9050)
} else {
appPrefs.networkProxy.get()
}
}
}
val netCfg = rememberSaveable(stateSaver = serializableSaver()) {
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value))
}
SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) {
@@ -241,12 +251,12 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
click = {
val updated = netCfg.value
.withOnionHosts(onionHosts.value)
.withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null)
.withProxy(if (networkUseSocksProxy.value) networkProxy.value else null, null)
.copy(
sessionMode = sessionMode.value
)
withBGApi {
state.value = MigrationToState.DatabaseInit(link, updated)
state.value = MigrationToState.DatabaseInit(link, updated, if (networkUseSocksProxy.value) networkProxy.value else null)
}
}
){}
@@ -255,8 +265,8 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
SectionSpacer()
val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = {
networkProxyHostPort.value = it
val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = {
networkProxy.value = it
})
SectionView(stringResource(MR.strings.network_settings_title).uppercase()) {
OnionRelatedLayout(
@@ -264,13 +274,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
networkUseSocksProxy,
onionHosts,
sessionMode,
networkProxyHostPortPref,
networkProxyPref,
toggleSocksProxy = { enable ->
networkUseSocksProxy.value = enable
},
useOnion = {
onionHosts.value = it
},
updateSessionMode = {
sessionMode.value = it
}
@@ -279,13 +286,13 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
}
@Composable
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
prepareDatabase(link, tempDatabaseFile, netCfg)
prepareDatabase(link, tempDatabaseFile, netCfg, networkProxy)
}
}
@@ -297,14 +304,15 @@ private fun MutableState<MigrationToState?>.LinkDownloadingView(
archivePath: String,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
netCfg: NetCfg
netCfg: NetCfg,
networkProxy: NetworkProxy?
) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg)
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg, networkProxy)
}
}
@@ -319,14 +327,14 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
}
@Composable
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migrate_to_device_repeat_download),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.DatabaseInit(link, netCfg)
state = MigrationToState.DatabaseInit(link, netCfg, networkProxy)
}
) {}
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
@@ -339,25 +347,25 @@ private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, cha
}
@Composable
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
importArchive(archivePath, netCfg)
importArchive(archivePath, netCfg, networkProxy)
}
}
@Composable
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migrate_to_device_repeat_import),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.ArchiveImport(archivePath, netCfg)
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
}
) {}
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
@@ -365,7 +373,7 @@ private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath:
}
@Composable
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
@@ -395,9 +403,9 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
if (success) {
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg, networkProxy)
} else if (status is DBMigrationResult.ErrorMigration) {
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg, networkProxy)
} else {
showErrorOnMigrationIfNeeded(status)
}
@@ -414,7 +422,7 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
}
@Composable
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) {
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
@@ -449,7 +457,7 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
text = button,
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg, networkProxy)
}
) {}
}
@@ -458,13 +466,13 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
}
@Composable
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
startChat(passphrase, confirmation, useKeychain, netCfg, close)
startChat(passphrase, confirmation, useKeychain, netCfg, networkProxy, close)
}
}
@@ -476,19 +484,21 @@ private fun ProgressView() {
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
if (strHasSimplexFileLink(link.trim())) {
val data = MigrationFileLinkData.readFromLink(link)
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
// If any of iOS or Android had onion enabled, show onion screen
if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
if (hasProxyConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
state = MigrationToState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
} else {
val current = getNetCfg()
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
socksProxy = networkConfig?.socksProxy,
socksProxy = null,
hostMode = networkConfig?.hostMode ?: current.hostMode,
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
))
),
networkProxy = null
)
}
} else {
AlertManager.shared.showAlertMsg(
@@ -502,6 +512,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
link: String,
tempDatabaseFile: File,
netCfg: NetCfg,
networkProxy: NetworkProxy?
) {
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
@@ -513,7 +524,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
}
val (ctrl, user) = ctrlAndUser
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg, networkProxy)
}
}
@@ -526,13 +537,14 @@ private fun MutableState<MigrationToState?>.startDownloading(
link: String,
archivePath: String,
netCfg: NetCfg,
networkProxy: NetworkProxy?
) {
withBGApi {
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.RcvFileProgressXFTP -> {
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
}
is CR.RcvStandaloneFileComplete -> {
delay(500)
@@ -540,8 +552,8 @@ private fun MutableState<MigrationToState?>.startDownloading(
if (state == null) {
MigrationToDeviceState.save(null)
} else {
state = MigrationToState.ArchiveImport(archivePath, netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
}
}
is CR.RcvFileError -> {
@@ -549,7 +561,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
}
is CR.ChatRespError -> {
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
@@ -557,7 +569,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
} else {
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}")
}
@@ -569,7 +581,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
if (res == null) {
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
error
@@ -578,7 +590,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
}
}
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
withLongRunningApi {
try {
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
@@ -592,14 +604,14 @@ private fun MutableState<MigrationToState?>.importArchive(archivePath: String, n
if (archiveErrors.isNotEmpty()) {
showArchiveImportedWithErrorsAlert(archiveErrors)
}
state = MigrationToState.Passphrase("", netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
state = MigrationToState.Passphrase("", netCfg, networkProxy)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg, networkProxy))
} catch (e: Exception) {
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
}
} catch (e: Exception) {
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
}
}
@@ -609,7 +621,7 @@ private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) {
controller.apiCancelFile(null, fileId, ctrl)
}
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
if (useKeychain) {
ksDatabasePassword.set(passphrase)
} else {
@@ -621,7 +633,8 @@ private fun startChat(passphrase: String, confirmation: MigrationConfirmation, u
try {
initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) }
val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy(
networkConfig = netCfg
networkConfig = netCfg,
networkProxy = networkProxy
)
finishMigration(appSettings, close)
} catch (e: Exception) {
@@ -1,10 +1,10 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionTextFooter
import SectionView
import SectionViewSelectableCards
import androidx.compose.desktop.ui.tooling.preview.Preview
@@ -40,12 +40,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } }
val currentCfgVal = currentCfg.value // used only on initialization
val onionHosts = remember { mutableStateOf(currentCfgVal.onionHosts) }
val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) }
val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) }
val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(currentCfgVal.useSocksProxy) }
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) }
@@ -90,11 +88,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
tcpKeepAlive = tcpKeepAlive,
smpPingInterval = networkSMPPingInterval.value,
smpPingCount = networkSMPPingCount.value
).withOnionHosts(onionHosts.value)
).withOnionHosts(currentCfg.value.onionHosts)
}
fun updateView(cfg: NetCfg) {
onionHosts.value = cfg.onionHosts
sessionMode.value = cfg.sessionMode
smpProxyMode.value = cfg.smpProxyMode
smpProxyFallback.value = cfg.smpProxyFallback
@@ -148,10 +145,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
) {
AdvancedNetworkSettingsLayout(
currentRemoteHost = currentRemoteHost,
networkUseSocksProxy = networkUseSocksProxy,
developerTools = developerTools,
onionHosts = onionHosts,
useOnion = { onionHosts.value = it; currentCfg.value = currentCfg.value.withOnionHosts(it) },
sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
@@ -183,10 +177,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
@Composable fun AdvancedNetworkSettingsLayout(
currentRemoteHost: RemoteHostInfo?,
networkUseSocksProxy: State<Boolean>,
developerTools: Boolean,
onionHosts: MutableState<OnionHosts>,
useOnion: (OnionHosts) -> Unit,
sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
@@ -223,21 +214,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } })
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
}
SectionCustomFooter {
Text(stringResource(MR.strings.private_routing_explanation))
}
SectionDividerSpaced(maxTopPadding = true)
}
if (currentRemoteHost == null && networkUseSocksProxy.value) {
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
}
}
}
SectionTextFooter(stringResource(MR.strings.private_routing_explanation))
SectionDividerSpaced(maxTopPadding = true)
}
@@ -562,7 +539,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
SimpleXTheme {
AdvancedNetworkSettingsLayout(
currentRemoteHost = null,
networkUseSocksProxy = remember { mutableStateOf(false) },
developerTools = false,
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
@@ -577,8 +553,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
networkTCPKeepIdle = remember { mutableStateOf(10) },
networkTCPKeepIntvl = remember { mutableStateOf(10) },
networkTCPKeepCnt = remember { mutableStateOf(10) },
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
useOnion = {},
updateSessionMode = {},
updateSMPProxyMode = {},
updateSMPProxyFallback = {},
@@ -1,10 +1,10 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionTextFooter
import SectionView
import SectionViewSelectable
import TextIconSpaced
@@ -37,10 +37,11 @@ fun NetworkAndServersView() {
val netCfg = remember { chatModel.controller.getNetCfg() }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost,
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
toggleSocksProxy = { enable ->
val def = NetCfg.defaults
val proxyDef = NetCfg.proxyDefaults
@@ -51,7 +52,7 @@ fun NetworkAndServersView() {
confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = {
withBGApi {
var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get())
var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get())
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
}
@@ -104,6 +105,7 @@ fun NetworkAndServersView() {
@Composable fun NetworkAndServersLayout(
currentRemoteHost: RemoteHostInfo?,
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
toggleSocksProxy: (Boolean) -> Unit,
) {
val m = chatModel
@@ -120,14 +122,10 @@ fun NetworkAndServersView() {
if (currentRemoteHost == null) {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxyHostPort, false, it) }})
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }})
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
if (networkUseSocksProxy.value) {
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
}
}
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
SectionDividerSpaced(maxTopPadding = true)
} else {
SectionDividerSpaced()
@@ -158,16 +156,14 @@ fun NetworkAndServersView() {
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
networkProxyHostPort: SharedPreference<String?>,
networkProxy: SharedPreference<NetworkProxy>,
toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
) {
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) }
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }}
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxyHostPort, true, it) } })
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxy, onionHosts, sessionMode.value, true, it) } })
if (developerTools) {
SessionModePicker(sessionMode, showModal, updateSessionMode)
}
@@ -205,46 +201,98 @@ fun UseSocksProxySwitch(
@Composable
fun SocksProxySettings(
networkUseSocksProxy: Boolean,
networkProxyHostPort: SharedPreference<String?> = appPrefs.networkProxyHostPort,
networkProxy: SharedPreference<NetworkProxy>,
onionHosts: MutableState<OnionHosts>,
sessionMode: TransportSessionMode,
migration: Boolean,
close: () -> Unit
) {
val defaultHostPort = remember { "localhost:9050" }
val hostPortSaved by remember { networkProxyHostPort.state }
val networkProxySaved by remember { networkProxy.state }
val onionHostsSaved = remember { mutableStateOf(onionHosts.value) }
val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(networkProxySaved.username))
}
val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(networkProxySaved.password))
}
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
mutableStateOf(TextFieldValue(networkProxySaved.host))
}
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
mutableStateOf(TextFieldValue(networkProxySaved.port.toString()))
}
val save = {
val oldValue = networkProxyHostPort.get()
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
if (networkUseSocksProxy && !migration) {
withBGApi {
if (!controller.apiSetNetworkConfig(controller.getNetCfg())) {
networkProxyHostPort.set(oldValue)
}
val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(networkProxySaved.auth == NetworkProxyAuth.ISOLATE) }
LaunchedEffect(proxyAuthRandomUnsaved.value) {
if (!proxyAuthRandomUnsaved.value && onionHosts.value != OnionHosts.NEVER) {
onionHosts.value = OnionHosts.NEVER
}
}
val proxyAuthModeUnsaved = remember(proxyAuthRandomUnsaved.value, usernameUnsaved.value.text, passwordUnsaved.value.text) {
derivedStateOf {
if (proxyAuthRandomUnsaved.value) {
NetworkProxyAuth.ISOLATE
} else {
NetworkProxyAuth.USERNAME
}
}
}
val saveAndClose = {
val oldValue = networkProxyHostPort.get()
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
val save: (Boolean) -> Unit = { closeOnSuccess ->
val oldValue = networkProxy.get()
usernameUnsaved.value = usernameUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) usernameUnsaved.value.text.trim() else "")
passwordUnsaved.value = passwordUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) passwordUnsaved.value.text.trim() else "")
hostUnsaved.value = hostUnsaved.value.copy(hostUnsaved.value.text.trim())
portUnsaved.value = portUnsaved.value.copy(portUnsaved.value.text.trim())
networkProxy.set(
NetworkProxy(
username = usernameUnsaved.value.text,
password = passwordUnsaved.value.text,
host = hostUnsaved.value.text,
port = portUnsaved.value.text.toIntOrNull() ?: 9050,
auth = proxyAuthModeUnsaved.value
)
)
val oldCfg = controller.getNetCfg()
val cfg = oldCfg.withOnionHosts(onionHosts.value)
val oldOnionHosts = onionHostsSaved.value
onionHostsSaved.value = onionHosts.value
if (!migration) {
controller.setNetCfg(cfg)
}
if (networkUseSocksProxy && !migration) {
withBGApi {
if (controller.apiSetNetworkConfig(controller.getNetCfg())) {
close()
if (controller.apiSetNetworkConfig(cfg, showAlertOnError = false)) {
onionHosts.value = cfg.onionHosts
onionHostsSaved.value = onionHosts.value
if (closeOnSuccess) {
close()
}
} else {
networkProxyHostPort.set(oldValue)
controller.setNetCfg(oldCfg)
networkProxy.set(oldValue)
onionHostsSaved.value = oldOnionHosts
showWrongProxyConfigAlert()
}
}
}
}
val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort
val saveDisabled =
(
networkProxySaved.username == usernameUnsaved.value.text.trim() &&
networkProxySaved.password == passwordUnsaved.value.text.trim() &&
networkProxySaved.host == hostUnsaved.value.text.trim() &&
networkProxySaved.port.toString() == portUnsaved.value.text.trim() &&
networkProxySaved.auth == proxyAuthModeUnsaved.value &&
onionHosts.value == onionHostsSaved.value
) ||
!validCredential(usernameUnsaved.value.text) ||
!validCredential(passwordUnsaved.value.text) ||
!validHost(hostUnsaved.value.text) ||
!validPort(portUnsaved.value.text)
val resetDisabled = hostUnsaved.value.text.trim() == "localhost" && portUnsaved.value.text.trim() == "9050" && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts
ModalView(
close = {
if (saveDisabled) {
@@ -252,7 +300,7 @@ fun SocksProxySettings(
} else {
showUnsavedSocksHostPortAlert(
confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save),
save = saveAndClose,
save = { save(true) },
close = close
)
}
@@ -263,38 +311,78 @@ fun SocksProxySettings(
.fillMaxWidth()
) {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
DefaultConfigurableTextField(
portUnsaved,
stringResource(MR.strings.port_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
keyboardType = KeyboardType.Number,
)
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
DefaultConfigurableTextField(
portUnsaved,
stringResource(MR.strings.port_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save(false) }),
keyboardType = KeyboardType.Number,
)
}
UseOnionHosts(onionHosts, rememberUpdatedState(networkUseSocksProxy && proxyAuthRandomUnsaved.value)) {
onionHosts.value = it
}
SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
}
SectionDividerSpaced(maxBottomPadding = false)
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) {
PreferenceToggle(
stringResource(MR.strings.network_proxy_random_credentials),
checked = proxyAuthRandomUnsaved.value,
onChange = { proxyAuthRandomUnsaved.value = it }
)
if (!proxyAuthRandomUnsaved.value) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
usernameUnsaved,
stringResource(MR.strings.network_proxy_username),
modifier = Modifier.fillMaxWidth(),
isValid = ::validCredential,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
DefaultConfigurableTextField(
passwordUnsaved,
stringResource(MR.strings.network_proxy_password),
modifier = Modifier.fillMaxWidth(),
isValid = ::validCredential,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Password,
)
}
}
SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode))
}
SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true)
SectionView {
SectionItemView({
val newHost = defaultHostPort.split(":").first()
val newPort = defaultHostPort.split(":").last()
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
hostUnsaved.value = hostUnsaved.value.copy("localhost", TextRange(9))
portUnsaved.value = portUnsaved.value.copy("9050", TextRange(4))
usernameUnsaved.value = TextFieldValue()
passwordUnsaved.value = TextFieldValue()
proxyAuthRandomUnsaved.value = true
onionHosts.value = NetCfg.defaults.onionHosts
}, disabled = resetDisabled) {
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save(false) } else save(false) },
disabled = saveDisabled
) {
Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
@@ -305,6 +393,12 @@ fun SocksProxySettings(
}
}
private fun proxyAuthFooter(username: String, password: String, auth: NetworkProxyAuth, sessionMode: TransportSessionMode): String = when {
auth == NetworkProxyAuth.ISOLATE -> generalGetString(if (sessionMode == TransportSessionMode.User) MR.strings.network_proxy_auth_mode_isolate_by_auth_user else MR.strings.network_proxy_auth_mode_isolate_by_auth_entity)
username.isBlank() && password.isBlank() -> generalGetString(MR.strings.network_proxy_auth_mode_no_auth)
else -> generalGetString(MR.strings.network_proxy_auth_mode_username_password)
}
private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.update_network_settings_question),
@@ -319,7 +413,6 @@ private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit,
fun UseOnionHosts(
onionHosts: MutableState<OnionHosts>,
enabled: State<Boolean>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
useOnion: (OnionHosts) -> Unit,
) {
val values = remember {
@@ -331,36 +424,29 @@ fun UseOnionHosts(
}
}
}
val onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
SectionViewSelectable(null, onionHosts, values, useOnion)
}
}
}
if (enabled.value) {
SectionItemWithValue(
generalGetString(MR.strings.network_use_onion_hosts),
onionHosts,
values,
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = onSelected
)
} else {
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
SectionItemWithValue(
generalGetString(MR.strings.network_use_onion_hosts),
remember { mutableStateOf(OnionHosts.NEVER) },
listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))),
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = {}
)
Column {
if (enabled.value) {
ExposedDropDownSettingRow(
generalGetString(MR.strings.network_use_onion_hosts),
values.map { it.value to it.title },
onionHosts,
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = useOnion
)
} else {
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
ExposedDropDownSettingRow(
generalGetString(MR.strings.network_use_onion_hosts),
listOf(OnionHosts.NEVER to generalGetString(MR.strings.network_use_onion_hosts_no)),
remember { mutableStateOf(OnionHosts.NEVER) },
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = {}
)
}
SectionTextFooter(values.first { it.value == onionHosts.value }.description)
}
}
@@ -398,12 +484,8 @@ fun SessionModePicker(
)
}
// https://stackoverflow.com/a/106223
private fun validHost(s: String): Boolean {
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
return s.matches(validIp) || s.matches(validHostname)
}
private fun validHost(s: String): Boolean =
!s.contains('@')
// https://ihateregex.io/expr/port/
fun validPort(s: String): Boolean {
@@ -411,6 +493,16 @@ fun validPort(s: String): Boolean {
return s.isNotBlank() && s.matches(validPort)
}
private fun validCredential(s: String): Boolean =
!s.contains(':') && !s.contains('@')
fun showWrongProxyConfigAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.network_proxy_incorrect_config_title),
text = generalGetString(MR.strings.network_proxy_incorrect_config_desc),
)
}
fun showUpdateNetworkSettingsDialog(
title: String,
startsWith: String = "",
@@ -435,6 +527,7 @@ fun PreviewNetworkAndServersLayout() {
NetworkAndServersLayout(
currentRemoteHost = null,
networkUseSocksProxy = remember { mutableStateOf(true) },
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
toggleSocksProxy = {},
)
}
@@ -765,7 +765,17 @@
<string name="network_socks_proxy">SOCKS proxy</string>
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
<string name="network_proxy_auth">Proxy authentication</string>
<string name="network_proxy_random_credentials">Use random credentials</string>
<string name="network_proxy_auth_mode_isolate_by_auth_user">Use different proxy credentials for each profile.</string>
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Use different proxy credentials for each connection.</string>
<string name="network_proxy_auth_mode_no_auth">Do not use credentials with proxy.</string>
<string name="network_proxy_auth_mode_username_password">Your credentials may be sent unencrypted.</string>
<string name="network_proxy_username">Username</string>
<string name="network_proxy_password">Password</string>
<string name="network_proxy_port">port %d</string>
<string name="network_proxy_incorrect_config_title">Error saving proxy</string>
<string name="network_proxy_incorrect_config_desc">Make sure proxy configuration is correct.</string>
<string name="host_verb">Host</string>
<string name="port_verb">Port</string>
<string name="network_enable_socks">Use SOCKS proxy?</string>
@@ -34,6 +34,9 @@ var useWorker = false;
var isDesktop = false;
var localizedState = "";
var localizedDescription = "";
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
// will be stored here and then set when the call will be ready to process them
let afterCallInitializedCandidates = [];
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stuns:stun.simplex.im:443"] },
@@ -234,6 +237,8 @@ const processCommand = (function () {
const pc = activeCall.connection;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
// for debugging, returning the command for callee to use
// resp = {
// type: "offer",
@@ -272,6 +277,8 @@ const processCommand = (function () {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
addIceCandidates(pc, remoteIceCandidates);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
// same as command for caller to use
resp = {
type: "answer",
@@ -297,17 +304,20 @@ const processCommand = (function () {
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(answer));
addIceCandidates(pc, remoteIceCandidates);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
resp = { type: "ok" };
}
break;
case "ice":
const remoteIceCandidates = parse(command.iceCandidates);
if (pc) {
const remoteIceCandidates = parse(command.iceCandidates);
addIceCandidates(pc, remoteIceCandidates);
resp = { type: "ok" };
}
else {
resp = { type: "error", message: "ice: call not started" };
afterCallInitializedCandidates.push(...remoteIceCandidates);
resp = { type: "error", message: "ice: call not started yet, will add candidates later" };
}
break;
case "media":
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.0.4
android.version_code=237
android.version_name=6.0.5
android.version_code=241
desktop.version_name=6.0.4
desktop.version_code=65
desktop.version_name=6.0.5
desktop.version_code=68
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: dab1980d79b35634bea9a259b633bd06ed8d5ebf
tag: 4268b90763c58358809a3ea7dd8bc7d78eeb3077
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 6.0.4.0
version: 6.0.5.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+12 -2
View File
@@ -219,6 +219,9 @@ var useWorker = false
var isDesktop = false
var localizedState = ""
var localizedDescription = ""
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
// will be stored here and then set when the call will be ready to process them
let afterCallInitializedCandidates: RTCIceCandidateInit[] = []
const processCommand = (function () {
type RTCRtpSenderWithEncryption = RTCRtpSender & {
@@ -445,6 +448,8 @@ const processCommand = (function () {
const pc = activeCall.connection
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
addIceCandidates(pc, afterCallInitializedCandidates)
afterCallInitializedCandidates = []
// for debugging, returning the command for callee to use
// resp = {
// type: "offer",
@@ -481,6 +486,8 @@ const processCommand = (function () {
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
addIceCandidates(pc, remoteIceCandidates)
addIceCandidates(pc, afterCallInitializedCandidates)
afterCallInitializedCandidates = []
// same as command for caller to use
resp = {
type: "answer",
@@ -503,16 +510,19 @@ const processCommand = (function () {
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(answer))
addIceCandidates(pc, remoteIceCandidates)
addIceCandidates(pc, afterCallInitializedCandidates)
afterCallInitializedCandidates = []
resp = {type: "ok"}
}
break
case "ice":
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
if (pc) {
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
addIceCandidates(pc, remoteIceCandidates)
resp = {type: "ok"}
} else {
resp = {type: "error", message: "ice: call not started"}
afterCallInitializedCandidates.push(...remoteIceCandidates)
resp = {type: "error", message: "ice: call not started yet, will add candidates later"}
}
break
case "media":
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."dab1980d79b35634bea9a259b633bd06ed8d5ebf" = "0wf0p02bz8zq31mgfnfxy5allhbfr6vcmv8cii92qwj95m6ibwcp";
"https://github.com/simplex-chat/simplexmq.git"."4268b90763c58358809a3ea7dd8bc7d78eeb3077" = "0w444jbxxi5hgipf35xniwmffnpg4qb46sz112hrwxyf5syfi2k5";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 6.0.4.0
version: 6.0.5.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
+9 -7
View File
@@ -105,7 +105,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig)
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig, textToHostMode)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -114,12 +114,12 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TransportError (..))
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
@@ -341,11 +341,11 @@ newChatController
userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user')
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_
cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_
cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_
in cfg3 {socksProxy, socksMode, logTLSErrors}
in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, logTLSErrors}
withChatLock :: String -> CM a -> CM a
withChatLock name action = asks chatLock >>= \l -> withLock l name action
@@ -8052,14 +8052,16 @@ chatCommandP =
<|> ("yes" $> TMEEnableKeepTTL)
<|> ("no" $> TMEDisableKeepTTL)
netCfgP = do
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP)
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxyWithAuth <|> Just <$> strP)
socksMode <- " socks-mode=" *> strP <|> pure SMAlways
hostMode <- " host-mode=" *> (textToHostMode . safeDecodeUtf8 <$?> A.takeTill (== ' ')) <|> pure (defaultHostMode socksProxy)
requiredHostMode <- " required-host-mode" *> onOffP <|> pure False
smpProxyMode_ <- optional $ " smp-proxy=" *> strP
smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP
t_ <- optional $ " timeout=" *> A.decimal
logTLSErrors <- " log=" *> onOffP <|> pure False
let tcpTimeout_ = (1000000 *) <$> t_
pure $ SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
+6 -4
View File
@@ -76,7 +76,7 @@ import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -87,7 +87,7 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, p
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost)
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>))
import Simplex.RemoteControl.Client
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
@@ -974,8 +974,10 @@ data AppFilePathsConfig = AppFilePathsConfig
deriving (Show)
data SimpleNetCfg = SimpleNetCfg
{ socksProxy :: Maybe SocksProxy,
{ socksProxy :: Maybe SocksProxyWithAuth,
socksMode :: SocksMode,
hostMode :: HostMode,
requiredHostMode :: Bool,
smpProxyMode_ :: Maybe SMPProxyMode,
smpProxyFallback_ :: Maybe SMPProxyFallback,
tcpTimeout_ :: Maybe Int,
@@ -984,7 +986,7 @@ data SimpleNetCfg = SimpleNetCfg
deriving (Show)
defaultSimpleNetCfg :: SimpleNetCfg
defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways Nothing Nothing Nothing False
defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways HMOnionViaSocks True Nothing Nothing Nothing False
data ContactSubStatus = ContactSubStatus
{ contact :: Contact,
+37 -4
View File
@@ -13,6 +13,7 @@ module Simplex.Chat.Options
coreChatOptsP,
getChatOpts,
protocolServersP,
defaultHostMode,
)
where
@@ -20,6 +21,7 @@ import Control.Logger.Simple (LogLevel (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteString.Char8 as B
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@@ -27,11 +29,11 @@ import Numeric.Natural (Natural)
import Options.Applicative
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
import Simplex.FileTransfer.Description (mb)
import Simplex.Messaging.Client (SocksMode (..))
import Simplex.Messaging.Client (HostMode (..), SocksMode (..), textToHostMode)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth)
import System.FilePath (combine)
data ChatOpts = ChatOpts
@@ -123,7 +125,7 @@ coreChatOptsP appDir defaultDbFileName = do
<> value []
)
socksProxy <-
flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
flag' (Just defaultSocksProxyWithAuth) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
<|> option
strParse
( long "socks-proxy"
@@ -139,6 +141,19 @@ coreChatOptsP appDir defaultDbFileName = do
<> help "Use SOCKS5 proxy: always (default), onion (with onion-only relays)"
<> value SMAlways
)
hostMode_ <-
optional $
option
parseHostMode
( long "host-mode"
<> metavar "HOST_MODE"
<> help "Preferred server host type: onion (when SOCKS proxy with isolate-by-auth is used), public"
)
requiredHostMode <-
switch
( long "required-host-mode"
<> help "Refuse connection if preferred server host type is not available"
)
smpProxyMode_ <-
optional $
option
@@ -226,7 +241,17 @@ coreChatOptsP appDir defaultDbFileName = do
dbKey,
smpServers,
xftpServers,
simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors},
simpleNetCfg =
SimpleNetCfg
{ socksProxy,
socksMode,
hostMode = fromMaybe (defaultHostMode socksProxy) hostMode_,
requiredHostMode,
smpProxyMode_,
smpProxyFallback_,
tcpTimeout_ = Just $ useTcpTimeout socksProxy t,
logTLSErrors
},
logLevel,
logConnections = logConnections || logLevel <= CLLInfo,
logServerHosts = logServerHosts || logLevel <= CLLInfo,
@@ -240,6 +265,11 @@ coreChatOptsP appDir defaultDbFileName = do
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
defaultDbFilePath = combine appDir defaultDbFileName
defaultHostMode :: Maybe SocksProxyWithAuth -> HostMode
defaultHostMode = \case
Just (SocksProxyWithAuth SocksIsolateByAuth _) -> HMOnionViaSocks;
_ -> HMPublic
chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
chatOptsP appDir defaultDbFileName = do
coreOptions <- coreChatOptsP appDir defaultDbFileName
@@ -360,6 +390,9 @@ parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack
strParse :: StrEncoding a => ReadM a
strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack
parseHostMode :: ReadM HostMode
parseHostMode = eitherReader $ textToHostMode . T.pack
parseServerPort :: ReadM (Maybe String)
parseServerPort = eitherReader $ parseAll serverPortP . B.pack
+5 -5
View File
@@ -540,21 +540,21 @@ data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: Maybe FileInv
$(JQ.deriveJSON defaultJSON ''QuotedMsg)
-- this limit reserves space for metadata in forwarded messages
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, - 16 for block encryption ("rounded" to 15602)
maxEncodedMsgLength :: Int
maxEncodedMsgLength = 15610
maxEncodedMsgLength = 15602
-- maxEncodedMsgLength - 2222, see e2eEncUserMsgLength in agent
maxCompressedMsgLength :: Int
maxCompressedMsgLength = 13388
maxCompressedMsgLength = 13380
-- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead)
-- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008
maxEncodedInfoLength :: Int
maxEncodedInfoLength = 14702
maxEncodedInfoLength = 14694
maxCompressedInfoLength :: Int
maxCompressedInfoLength = 10976 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
maxCompressedInfoLength = 10968 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
data EncodedChatMessage = ECMEncoded ByteString | ECMLarge