diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp
index 28b028ff..f0a7769a 100644
--- a/client/amnezia_application.cpp
+++ b/client/amnezia_application.cpp
@@ -91,6 +91,13 @@ void AmneziaApplication::init()
initControllers();
#ifdef Q_OS_ANDROID
+ if(!AndroidController::initLogging()) {
+ qFatal("Android logging initialization failed");
+ }
+ AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs());
+ connect(m_settings.get(), &Settings::saveLogsChanged,
+ AndroidController::instance(), &AndroidController::setSaveLogs);
+
connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
[this](Vpn::ConnectionState state) {
m_connectionController->onConnectionStateChanged(state);
@@ -98,10 +105,7 @@ void AmneziaApplication::init()
m_vpnConnection->restoreConnection();
});
if (!AndroidController::instance()->initialize()) {
- qCritical() << QString("Init failed");
- if (m_vpnConnection)
- emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Error);
- return;
+ qFatal("Android controller initialization failed");
}
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) {
@@ -143,11 +147,13 @@ void AmneziaApplication::init()
m_engine->load(url);
m_systemController->setQmlRoot(m_engine->rootObjects().value(0));
+#ifndef Q_OS_ANDROID
if (m_settings->isSaveLogs()) {
if (!Logger::init()) {
qWarning() << "Initialization of debug subsystem failed";
}
}
+#endif
#ifdef Q_OS_WIN
if (m_parser.isSet("a"))
diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml
index 960dc87d..fb417f05 100644
--- a/client/android/AndroidManifest.xml
+++ b/client/android/AndroidManifest.xml
@@ -1,6 +1,8 @@
-
+ android:theme="@style/NoActionBar"
+ android:fullBackupContent="@xml/backup_content"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s">
-
+
diff --git a/client/android/build.gradle.kts b/client/android/build.gradle.kts
index 757989aa..0a1fe83f 100644
--- a/client/android/build.gradle.kts
+++ b/client/android/build.gradle.kts
@@ -108,7 +108,6 @@ dependencies {
implementation(project(":cloak"))
implementation(libs.androidx.core)
implementation(libs.androidx.activity)
- implementation(libs.androidx.security.crypto)
implementation(libs.kotlinx.coroutines)
implementation(libs.bundles.androidx.camera)
implementation(libs.google.mlkit)
diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt
index c716a970..f489c980 100644
--- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt
+++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpnClient.kt
@@ -52,7 +52,7 @@ class OpenVpnClient(
// Callback to construct a new tun builder
// Should be called first.
override fun tun_builder_new(): Boolean {
- Log.v(TAG, "tun_builder_new")
+ Log.d(TAG, "tun_builder_new")
configBuilder.clearAddresses()
return true
}
@@ -60,7 +60,7 @@ class OpenVpnClient(
// Callback to set MTU of the VPN interface
// Never called more than once per tun_builder session.
override fun tun_builder_set_mtu(mtu: Int): Boolean {
- Log.v(TAG, "tun_builder_set_mtu: $mtu")
+ Log.d(TAG, "tun_builder_set_mtu: $mtu")
configBuilder.setMtu(mtu)
return true
}
@@ -71,7 +71,7 @@ class OpenVpnClient(
address: String, prefix_length: Int,
gateway: String, ipv6: Boolean, net30: Boolean
): Boolean {
- Log.v(TAG, "tun_builder_add_address: $address, $prefix_length, $gateway, $ipv6, $net30")
+ Log.d(TAG, "tun_builder_add_address: $address, $prefix_length, $gateway, $ipv6, $net30")
configBuilder.addAddress(InetNetwork(address, prefix_length))
return true
}
@@ -80,7 +80,7 @@ class OpenVpnClient(
// May be called more than once per tun_builder session
// metric is optional and should be ignored if < 0
override fun tun_builder_add_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean {
- Log.v(TAG, "tun_builder_add_route: $address, $prefix_length, $metric, $ipv6")
+ Log.d(TAG, "tun_builder_add_route: $address, $prefix_length, $metric, $ipv6")
if (address == "remote_host") return false
configBuilder.addRoute(InetNetwork(address, prefix_length))
return true
@@ -90,7 +90,7 @@ class OpenVpnClient(
// May be called more than once per tun_builder session
// metric is optional and should be ignored if < 0
override fun tun_builder_exclude_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean {
- Log.v(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6")
+ Log.d(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
configBuilder.excludeRoute(InetNetwork(address, prefix_length))
}
@@ -104,7 +104,7 @@ class OpenVpnClient(
// domain should be routed.
// Guaranteed to be called after tun_builder_reroute_gw.
override fun tun_builder_add_dns_server(address: String, ipv6: Boolean): Boolean {
- Log.v(TAG, "tun_builder_add_dns_server: $address, $ipv6")
+ Log.d(TAG, "tun_builder_add_dns_server: $address, $ipv6")
configBuilder.addDnsServer(parseInetAddress(address))
return true
}
@@ -119,28 +119,28 @@ class OpenVpnClient(
// ignored for that family
// See also Android's VPNService.Builder.allowFamily method
/* override fun tun_builder_set_allow_family(af: Int, allow: Boolean): Boolean {
- Log.v(TAG, "tun_builder_set_allow_family: $af, $allow")
+ Log.d(TAG, "tun_builder_set_allow_family: $af, $allow")
return true
} */
// Callback to set address of remote server
// Never called more than once per tun_builder session.
override fun tun_builder_set_remote_address(address: String, ipv6: Boolean): Boolean {
- Log.v(TAG, "tun_builder_set_remote_address: $address, $ipv6")
+ Log.d(TAG, "tun_builder_set_remote_address: $address, $ipv6")
return true
}
// Optional callback that indicates OSI layer, should be 2 or 3.
// Defaults to 3.
override fun tun_builder_set_layer(layer: Int): Boolean {
- Log.v(TAG, "tun_builder_set_layer: $layer")
+ Log.d(TAG, "tun_builder_set_layer: $layer")
return layer == 3
}
// Callback to set the session name
// Never called more than once per tun_builder session.
override fun tun_builder_set_session_name(name: String): Boolean {
- Log.v(TAG, "tun_builder_set_session_name: $name")
+ Log.d(TAG, "tun_builder_set_session_name: $name")
return true
}
@@ -149,7 +149,7 @@ class OpenVpnClient(
// if the tunnel could not be established.
// Always called last after tun_builder session has been configured.
override fun tun_builder_establish(): Int {
- Log.v(TAG, "tun_builder_establish")
+ Log.d(TAG, "tun_builder_establish")
return establish(configBuilder)
}
@@ -159,7 +159,7 @@ class OpenVpnClient(
// flags are defined in RGWFlags (rgwflags.hpp).
// Never called more than once per tun_builder session.
override fun tun_builder_reroute_gw(ipv4: Boolean, ipv6: Boolean, flags: Long): Boolean {
- Log.v(TAG, "tun_builder_reroute_gw: $ipv4, $ipv6, $flags")
+ Log.d(TAG, "tun_builder_reroute_gw: $ipv4, $ipv6, $flags")
if ((flags and EMULATED_EXCLUDE_ROUTES.toLong()) != 0L) return true
if (ipv4) {
configBuilder.addRoute(InetNetwork("0.0.0.0", 0))
@@ -176,7 +176,7 @@ class OpenVpnClient(
// reroute_dns parameter.
// Guaranteed to be called after tun_builder_reroute_gw.
override fun tun_builder_add_search_domain(domain: String): Boolean {
- Log.v(TAG, "tun_builder_add_search_domain: $domain")
+ Log.d(TAG, "tun_builder_add_search_domain: $domain")
configBuilder.setSearchDomain(domain)
return true
}
@@ -184,7 +184,7 @@ class OpenVpnClient(
// Callback to set the HTTP proxy
// Never called more than once per tun_builder session.
override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean {
- Log.v(TAG, "tun_builder_set_proxy_http: $host, $port")
+ Log.d(TAG, "tun_builder_set_proxy_http: $host, $port")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port))
@@ -199,7 +199,7 @@ class OpenVpnClient(
// Callback to set the HTTPS proxy
// Never called more than once per tun_builder session.
override fun tun_builder_set_proxy_https(host: String, port: Int): Boolean {
- Log.v(TAG, "tun_builder_set_proxy_https: $host, $port")
+ Log.d(TAG, "tun_builder_set_proxy_https: $host, $port")
return false
}
@@ -208,7 +208,7 @@ class OpenVpnClient(
// to exclude them from the VPN network are generated
// This should be a list of CIDR networks (e.g. 192.168.0.0/24)
override fun tun_builder_get_local_networks(ipv6: Boolean): ClientAPI_StringVec {
- Log.v(TAG, "tun_builder_get_local_networks: $ipv6")
+ Log.d(TAG, "tun_builder_get_local_networks: $ipv6")
val networks = ClientAPI_StringVec()
for (address in getLocalNetworks(ipv6)) {
networks.add(address.toString())
@@ -222,21 +222,21 @@ class OpenVpnClient(
// tun_builder_reroute_gw. Route metric is ignored
// if < 0.
/* override fun tun_builder_set_route_metric_default(metric: Int): Boolean {
- Log.v(TAG, "tun_builder_set_route_metric_default: $metric")
+ Log.d(TAG, "tun_builder_set_route_metric_default: $metric")
return super.tun_builder_set_route_metric_default(metric)
} */
// Callback to add a host which should bypass the proxy
// May be called more than once per tun_builder session
/* override fun tun_builder_add_proxy_bypass(bypass_host: String): Boolean {
- Log.v(TAG, "tun_builder_add_proxy_bypass: $bypass_host")
+ Log.d(TAG, "tun_builder_add_proxy_bypass: $bypass_host")
return super.tun_builder_add_proxy_bypass(bypass_host)
} */
// Callback to set the proxy "Auto Config URL"
// Never called more than once per tun_builder session.
/* override fun tun_builder_set_proxy_auto_config_url(url: String): Boolean {
- Log.v(TAG, "tun_builder_set_proxy_auto_config_url: $url")
+ Log.d(TAG, "tun_builder_set_proxy_auto_config_url: $url")
return super.tun_builder_set_proxy_auto_config_url(url)
} */
@@ -245,7 +245,7 @@ class OpenVpnClient(
// May be called more than once per tun_builder session.
// Guaranteed to be called after tun_builder_reroute_gw.
/* override fun tun_builder_add_wins_server(address: String): Boolean {
- Log.v(TAG, "tun_builder_add_wins_server: $address")
+ Log.d(TAG, "tun_builder_add_wins_server: $address")
return super.tun_builder_add_wins_server(address)
} */
@@ -254,7 +254,7 @@ class OpenVpnClient(
// set the "Connection-specific DNS Suffix" property on
// the TAP driver.
/* override fun tun_builder_set_adapter_domain_suffix(name: String): Boolean {
- Log.v(TAG, "tun_builder_set_adapter_domain_suffix: $name")
+ Log.d(TAG, "tun_builder_set_adapter_domain_suffix: $name")
return super.tun_builder_set_adapter_domain_suffix(name)
} */
@@ -266,13 +266,13 @@ class OpenVpnClient(
// tun_builder_establish_lite() will be called. Otherwise,
// tun_builder_establish() will be called.
/* override fun tun_builder_persist(): Boolean {
- Log.v(TAG, "tun_builder_persist")
+ Log.d(TAG, "tun_builder_persist")
return super.tun_builder_persist()
} */
// Indicates a reconnection with persisted tun state.
/* override fun tun_builder_establish_lite() {
- Log.v(TAG, "tun_builder_establish_lite")
+ Log.d(TAG, "tun_builder_establish_lite")
super.tun_builder_establish_lite()
} */
@@ -280,7 +280,7 @@ class OpenVpnClient(
// If disconnect == true, then the teardown is occurring
// prior to final disconnect.
/* override fun tun_builder_teardown(disconnect: Boolean) {
- Log.v(TAG, "tun_builder_teardown: $disconnect")
+ Log.d(TAG, "tun_builder_teardown: $disconnect")
super.tun_builder_teardown(disconnect)
} */
@@ -290,7 +290,7 @@ class OpenVpnClient(
// Parse OpenVPN configuration file.
override fun eval_config(arg0: ClientAPI_Config): ClientAPI_EvalConfig {
- Log.v(TAG, "eval_config")
+ Log.d(TAG, "eval_config")
return super.eval_config(arg0)
}
@@ -299,7 +299,7 @@ class OpenVpnClient(
// to event() and log() functions. Make sure to call eval_config()
// and possibly provide_creds() as well before this function.
override fun connect(): ClientAPI_Status {
- Log.v(TAG, "connect")
+ Log.d(TAG, "connect")
return super.connect()
}
@@ -307,7 +307,7 @@ class OpenVpnClient(
// Will be called from the thread executing connect().
// The remote and ipv6 are the remote host this socket will connect to
override fun socket_protect(socket: Int, remote: String, ipv6: Boolean): Boolean {
- Log.v(TAG, "socket_protect: $socket, $remote, $ipv6")
+ Log.d(TAG, "socket_protect: $socket, $remote, $ipv6")
return protect(socket)
}
@@ -315,7 +315,7 @@ class OpenVpnClient(
// May be called asynchronously from a different thread
// when connect() is running.
override fun stop() {
- Log.v(TAG, "stop")
+ Log.d(TAG, "stop")
super.stop()
}
@@ -323,21 +323,21 @@ class OpenVpnClient(
// when network is down. May be called from a different thread
// when connect() is running.
override fun pause(reason: String) {
- Log.v(TAG, "pause: $reason")
+ Log.d(TAG, "pause: $reason")
super.pause(reason)
}
// Resume the client after it has been paused. May be called from a
// different thread when connect() is running.
override fun resume() {
- Log.v(TAG, "resume")
+ Log.d(TAG, "resume")
super.resume()
}
// Do a disconnect/reconnect cycle n seconds from now. May be called
// from a different thread when connect() is running.
override fun reconnect(seconds: Int) {
- Log.v(TAG, "reconnect")
+ Log.d(TAG, "reconnect: $seconds")
super.reconnect(seconds)
}
@@ -346,14 +346,14 @@ class OpenVpnClient(
// CONNECTION_TIMEOUT event. If true, the core will enter a PAUSE
// state.
override fun pause_on_connection_timeout(): Boolean {
- Log.v(TAG, "pause_on_connection_timeout")
+ Log.d(TAG, "pause_on_connection_timeout")
return false
}
// Return information about the most recent connection. Should be called
// after an event of type "CONNECTED".
/* override fun connection_info(): ClientAPI_ConnectionInfo {
- Log.v(TAG, "connection_info")
+ Log.d(TAG, "connection_info")
return super.connection_info()
} */
@@ -366,7 +366,7 @@ class OpenVpnClient(
override fun event(event: ClientAPI_Event) {
val name = event.name
val info = event.info
- Log.v(TAG, "OpenVpn event: $name: $info")
+ Log.d(TAG, "OpenVpn event: $name: $info")
when (name) {
"COMPRESSION_ENABLED", "WARN" -> Log.w(TAG, "$name: $info")
"CONNECTED" -> state.value = CONNECTED
@@ -398,31 +398,31 @@ class OpenVpnClient(
// return transport stats only
override fun transport_stats(): ClientAPI_TransportStats {
- Log.v(TAG, "transport_stats")
+ Log.d(TAG, "transport_stats")
return super.transport_stats()
}
// return a stats value, index should be >= 0 and < stats_n()
/* override fun stats_value(index: Int): Long {
- Log.v(TAG, "stats_value: $index")
+ Log.d(TAG, "stats_value: $index")
return super.stats_value(index)
} */
// return all stats in a bundle
/* override fun stats_bundle(): ClientAPI_LLVector {
- Log.v(TAG, "stats_bundle")
+ Log.d(TAG, "stats_bundle")
return super.stats_bundle()
} */
// return tun stats only
/* override fun tun_stats(): ClientAPI_InterfaceStats {
- Log.v(TAG, "tun_stats")
+ Log.d(TAG, "tun_stats")
return super.tun_stats()
} */
// post control channel message
/* override fun post_cc_msg(msg: String) {
- Log.v(TAG, "post_cc_msg: $msg")
+ Log.d(TAG, "post_cc_msg: $msg")
super.post_cc_msg(msg)
} */
}
diff --git a/client/android/res/xml/backup_content.xml b/client/android/res/xml/backup_content.xml
new file mode 100644
index 00000000..33260809
--- /dev/null
+++ b/client/android/res/xml/backup_content.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/android/res/xml/data_extraction_rules.xml b/client/android/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..f5f079cc
--- /dev/null
+++ b/client/android/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
index 0c0ec0f9..2ed2c7f7 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
@@ -143,7 +143,7 @@ class AmneziaActivity : QtActivity() {
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- Log.v(TAG, "Create Amnezia activity: $intent")
+ Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
vpnServiceMessenger = IpcMessenger(
onDeadObjectException = ::doUnbindService,
@@ -154,7 +154,7 @@ class AmneziaActivity : QtActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
- Log.v(TAG, "onNewIntent: $intent")
+ Log.d(TAG, "onNewIntent: $intent")
intent?.let(::processIntent)
}
@@ -174,7 +174,7 @@ class AmneziaActivity : QtActivity() {
override fun onStart() {
super.onStart()
- Log.v(TAG, "Start Amnezia activity")
+ Log.d(TAG, "Start Amnezia activity")
mainScope.launch {
qtInitialized.await()
doBindService()
@@ -182,13 +182,13 @@ class AmneziaActivity : QtActivity() {
}
override fun onStop() {
- Log.v(TAG, "Stop Amnezia activity")
+ Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
super.onStop()
}
override fun onDestroy() {
- Log.v(TAG, "Destroy Amnezia activity")
+ Log.d(TAG, "Destroy Amnezia activity")
mainScope.cancel()
super.onDestroy()
}
@@ -217,7 +217,7 @@ class AmneziaActivity : QtActivity() {
CHECK_VPN_PERMISSION_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
- Log.v(TAG, "Vpn permission granted")
+ Log.d(TAG, "Vpn permission granted")
Toast.makeText(this, "Vpn permission granted", Toast.LENGTH_LONG).show()
checkVpnPermissionCallbacks?.run { onSuccess() }
}
@@ -240,7 +240,7 @@ class AmneziaActivity : QtActivity() {
*/
@MainThread
private fun doBindService() {
- Log.v(TAG, "Bind service")
+ Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
}
@@ -251,7 +251,7 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun doUnbindService() {
if (isInBoundState) {
- Log.v(TAG, "Unbind service")
+ Log.d(TAG, "Unbind service")
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
vpnServiceMessenger.reset()
@@ -286,7 +286,7 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun checkVpnPermission(onSuccess: () -> Unit, onFail: () -> Unit) {
- Log.v(TAG, "Check VPN permission")
+ Log.d(TAG, "Check VPN permission")
VpnService.prepare(applicationContext)?.let {
checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail)
startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE)
@@ -307,7 +307,7 @@ class AmneziaActivity : QtActivity() {
}
private fun connectToVpn(vpnConfig: String) {
- Log.v(TAG, "Connect to VPN")
+ Log.d(TAG, "Connect to VPN")
vpnServiceMessenger.send {
Action.CONNECT.packToMessage {
putString(VPN_CONFIG, vpnConfig)
@@ -316,7 +316,7 @@ class AmneziaActivity : QtActivity() {
}
private fun startVpnService(vpnConfig: String) {
- Log.v(TAG, "Start VPN service")
+ Log.d(TAG, "Start VPN service")
Intent(this, AmneziaVpnService::class.java).apply {
putExtra(VPN_CONFIG, vpnConfig)
}.also {
@@ -325,7 +325,7 @@ class AmneziaActivity : QtActivity() {
}
private fun disconnectFromVpn() {
- Log.v(TAG, "Disconnect from VPN")
+ Log.d(TAG, "Disconnect from VPN")
vpnServiceMessenger.send(Action.DISCONNECT)
}
@@ -369,7 +369,7 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun saveFile(fileName: String, data: String) {
- Log.v(TAG, "Save file $fileName")
+ Log.d(TAG, "Save file $fileName")
mainScope.launch {
tmpFileContentToSave = data
@@ -397,7 +397,7 @@ class AmneziaActivity : QtActivity() {
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
- Log.d(TAG, "File mimyType filter: $mimeTypes")
+ Log.v(TAG, "File mimyType filter: $mimeTypes")
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
@@ -416,13 +416,6 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun setNotificationText(title: String, message: String, timerSec: Int) {
Log.v(TAG, "Set notification text")
- Log.w(TAG, "Not yet implemented")
- }
-
- @Suppress("unused")
- fun cleanupLogs() {
- Log.v(TAG, "Cleanup logs")
- Log.w(TAG, "Not yet implemented")
}
@Suppress("unused")
@@ -432,4 +425,29 @@ class AmneziaActivity : QtActivity() {
startActivity(it)
}
}
+
+ @Suppress("unused")
+ fun setSaveLogs(enabled: Boolean) {
+ Log.d(TAG, "Set save logs: $enabled")
+ mainScope.launch {
+ Log.saveLogs = enabled
+ vpnServiceMessenger.send {
+ Action.SET_SAVE_LOGS.packToMessage {
+ putBoolean(SAVE_LOGS, enabled)
+ }
+ }
+ }
+ }
+
+ @Suppress("unused")
+ fun exportLogsFile(fileName: String) {
+ Log.v(TAG, "Export logs file")
+ saveFile(fileName, Log.getLogs())
+ }
+
+ @Suppress("unused")
+ fun clearLogs() {
+ Log.v(TAG, "Clear logs")
+ Log.clearLogs()
+ }
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
index e9d8fdfb..33182887 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
@@ -5,14 +5,20 @@ import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationManagerCompat
+import org.amnezia.vpn.util.Log
+import org.amnezia.vpn.util.Prefs
import org.qtproject.qt.android.bindings.QtApplication
+private const val TAG = "AmneziaApplication"
const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
override fun onCreate() {
super.onCreate()
+ Prefs.init(this)
+ Log.init(this)
+ Log.d(TAG, "Create Amnezia application")
createNotificationChannel()
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
index 094874c7..ab753716 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
@@ -50,6 +50,7 @@ import org.amnezia.vpn.protocol.putStatistics
import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
+import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState
import org.json.JSONException
import org.json.JSONObject
@@ -58,6 +59,8 @@ private const val TAG = "AmneziaVpnService"
const val VPN_CONFIG = "VPN_CONFIG"
const val ERROR_MSG = "ERROR_MSG"
+const val SAVE_LOGS = "SAVE_LOGS"
+
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val NOTIFICATION_ID = 1337
@@ -118,7 +121,7 @@ class AmneziaVpnService : VpnService() {
Action.CONNECT -> {
val vpnConfig = msg.data.getString(VPN_CONFIG)
- saveConfigToPrefs(vpnConfig)
+ Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
}
@@ -135,6 +138,10 @@ class AmneziaVpnService : VpnService() {
}
}
}
+
+ Action.SET_SAVE_LOGS -> {
+ Log.saveLogs = msg.data.getBoolean(SAVE_LOGS)
+ }
}
}
}
@@ -179,7 +186,7 @@ class AmneziaVpnService : VpnService() {
*/
override fun onCreate() {
super.onCreate()
- Log.v(TAG, "Create Amnezia VPN service")
+ Log.d(TAG, "Create Amnezia VPN service")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
clientMessenger = IpcMessenger(messengerName = "Client")
@@ -193,15 +200,15 @@ class AmneziaVpnService : VpnService() {
else intent?.component?.packageName != packageName
if (isAlwaysOnCompat) {
- Log.v(TAG, "Start service via Always-on")
- connect(loadConfigFromPrefs())
+ Log.d(TAG, "Start service via Always-on")
+ connect(Prefs.load(PREFS_CONFIG_KEY))
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
- Log.v(TAG, "Start service after permission check")
- connect(loadConfigFromPrefs())
+ Log.d(TAG, "Start service after permission check")
+ connect(Prefs.load(PREFS_CONFIG_KEY))
} else {
- Log.v(TAG, "Start service")
+ Log.d(TAG, "Start service")
val vpnConfig = intent?.getStringExtra(VPN_CONFIG)
- saveConfigToPrefs(vpnConfig)
+ Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
}
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
@@ -237,7 +244,7 @@ class AmneziaVpnService : VpnService() {
}
override fun onRevoke() {
- Log.v(TAG, "onRevoke")
+ Log.d(TAG, "onRevoke")
// Calls to onRevoke() method may not happen on the main thread of the process
mainScope.launch {
disconnect()
@@ -245,7 +252,7 @@ class AmneziaVpnService : VpnService() {
}
override fun onDestroy() {
- Log.v(TAG, "Destroy service")
+ Log.d(TAG, "Destroy service")
runBlocking {
disconnect()
disconnectionJob?.join()
@@ -256,7 +263,7 @@ class AmneziaVpnService : VpnService() {
}
private fun stopService() {
- Log.v(TAG, "Stop service")
+ Log.d(TAG, "Stop service")
// the coroutine below will be canceled during the onDestroy call
mainScope.launch {
delay(STOP_SERVICE_TIMEOUT)
@@ -272,7 +279,7 @@ class AmneziaVpnService : VpnService() {
private fun launchProtocolStateHandler() {
mainScope.launch {
protocolState.collect { protocolState ->
- Log.d(TAG, "Protocol state: $protocolState")
+ Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) {
CONNECTED -> {
clientMessenger.send(ServiceEvent.CONNECTED)
@@ -305,7 +312,7 @@ class AmneziaVpnService : VpnService() {
@MainThread
private fun launchSendingStatistics() {
- if (isServiceBound && isConnected) {
+ /* if (isServiceBound && isConnected) {
statisticsSendingJob = mainScope.launch {
while (true) {
clientMessenger.send {
@@ -316,7 +323,7 @@ class AmneziaVpnService : VpnService() {
delay(STATISTICS_SENDING_TIMEOUT)
}
}
- }
+ } */
}
@MainThread
@@ -328,7 +335,7 @@ class AmneziaVpnService : VpnService() {
private fun connect(vpnConfig: String?) {
if (isConnected || protocolState.value == CONNECTING) return
- Log.v(TAG, "Start VPN connection")
+ Log.d(TAG, "Start VPN connection")
protocolState.value = CONNECTING
@@ -357,7 +364,7 @@ class AmneziaVpnService : VpnService() {
private fun disconnect() {
if (isUnknown || isDisconnected || protocolState.value == DISCONNECTING) return
- Log.v(TAG, "Stop VPN connection")
+ Log.d(TAG, "Stop VPN connection")
protocolState.value = DISCONNECTING
@@ -383,7 +390,7 @@ class AmneziaVpnService : VpnService() {
private fun reconnect() {
if (!isConnected) return
- Log.v(TAG, "Reconnect VPN")
+ Log.d(TAG, "Reconnect VPN")
protocolState.value = RECONNECTING
@@ -439,10 +446,4 @@ class AmneziaVpnService : VpnService() {
} else {
true
}
-
- private fun loadConfigFromPrefs(): String? =
- Prefs.get(this).getString(PREFS_CONFIG_KEY, null)
-
- private fun saveConfigToPrefs(config: String?) =
- Prefs.get(this).edit().putString(PREFS_CONFIG_KEY, config).apply()
}
diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt
index 309333ef..cae7ab75 100644
--- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt
+++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt
@@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- Log.v(TAG, "Create Import Config Activity: $intent")
+ Log.d(TAG, "Create Import Config Activity: $intent")
intent?.let(::readConfig)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
- Log.v(TAG, "onNewIntent: $intent")
+ Log.d(TAG, "onNewIntent: $intent")
intent?.let(::readConfig)
}
private fun readConfig(intent: Intent) {
when (intent.action) {
ACTION_SEND -> {
- Log.v(TAG, "Process SEND action, type: ${intent.type}")
+ Log.d(TAG, "Process SEND action, type: ${intent.type}")
when (intent.type) {
"application/octet-stream" -> {
intent.getUriCompat()?.let { uri ->
@@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() {
}
ACTION_VIEW -> {
- Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}")
+ Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}")
when (intent.scheme) {
"file", "content" -> {
intent.data?.let { uri ->
@@ -128,7 +128,7 @@ class ImportConfigActivity : ComponentActivity() {
private fun startMainActivity(config: String) {
if (config.isNotBlank()) {
- Log.v(TAG, "startMainActivity")
+ Log.d(TAG, "startMainActivity")
Intent(applicationContext, AmneziaActivity::class.java).apply {
action = ACTION_IMPORT_CONFIG
addCategory(CATEGORY_DEFAULT)
diff --git a/client/android/src/org/amnezia/vpn/IpcMessage.kt b/client/android/src/org/amnezia/vpn/IpcMessage.kt
index c9d2bd3f..26c3b9de 100644
--- a/client/android/src/org/amnezia/vpn/IpcMessage.kt
+++ b/client/android/src/org/amnezia/vpn/IpcMessage.kt
@@ -32,7 +32,8 @@ enum class Action : IpcMessage {
REGISTER_CLIENT,
CONNECT,
DISCONNECT,
- REQUEST_STATUS
+ REQUEST_STATUS,
+ SET_SAVE_LOGS
}
fun T.packToMessage(): Message
diff --git a/client/android/src/org/amnezia/vpn/Prefs.kt b/client/android/src/org/amnezia/vpn/Prefs.kt
deleted file mode 100644
index 9b4cb2ba..00000000
--- a/client/android/src/org/amnezia/vpn/Prefs.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.amnezia.vpn
-
-import android.content.Context
-import android.content.SharedPreferences
-import androidx.security.crypto.EncryptedSharedPreferences
-import androidx.security.crypto.MasterKey
-import org.amnezia.vpn.util.Log
-
-private const val TAG = "Prefs"
-private const val PREFS_FILE = "org.amnezia.vpn.prefs"
-private const val SECURE_PREFS_FILE = "$PREFS_FILE.secure"
-
-object Prefs {
- fun get(context: Context, appContext: Context = context.applicationContext): SharedPreferences =
- try {
- EncryptedSharedPreferences(
- appContext,
- SECURE_PREFS_FILE,
- MasterKey(appContext)
- )
- } catch (e: Exception) {
- Log.e(TAG, "Getting Encryption Storage failed: ${e.message}, plaintext fallback")
- appContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
- }
-}
diff --git a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
index 9dce3d78..c5abbc39 100644
--- a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
+++ b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
@@ -25,7 +25,7 @@ class VpnRequestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- Log.v(TAG, "Start request activity")
+ Log.d(TAG, "Start request activity")
val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) {
if (getSystemService()!!.isKeyguardLocked) {
diff --git a/client/android/utils/build.gradle.kts b/client/android/utils/build.gradle.kts
index ac5d1efb..2ad03d61 100644
--- a/client/android/utils/build.gradle.kts
+++ b/client/android/utils/build.gradle.kts
@@ -15,3 +15,7 @@ android {
buildConfig = true
}
}
+
+dependencies {
+ implementation(libs.androidx.security.crypto)
+}
diff --git a/client/android/utils/src/main/kotlin/Log.kt b/client/android/utils/src/main/kotlin/Log.kt
index 82180c3d..03da4507 100644
--- a/client/android/utils/src/main/kotlin/Log.kt
+++ b/client/android/utils/src/main/kotlin/Log.kt
@@ -1,33 +1,252 @@
package org.amnezia.vpn.util
+import android.content.Context
+import android.icu.text.DateFormat
+import android.icu.text.SimpleDateFormat
+import android.os.Build
+import android.os.Process
+import java.io.File
+import java.io.IOException
+import java.io.RandomAccessFile
+import java.nio.channels.FileChannel
+import java.nio.channels.FileLock
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.locks.ReentrantLock
+import org.amnezia.vpn.util.Log.Priority.D
+import org.amnezia.vpn.util.Log.Priority.E
+import org.amnezia.vpn.util.Log.Priority.F
+import org.amnezia.vpn.util.Log.Priority.I
+import org.amnezia.vpn.util.Log.Priority.V
+import org.amnezia.vpn.util.Log.Priority.W
import android.util.Log as NativeLog
-class Log {
- companion object {
- fun v(tag: String, msg: String) = debugLog(tag, msg, NativeLog::v)
+private const val TAG = "Log"
+private const val LOG_FILE_NAME = "amneziaVPN.log"
+private const val ROTATE_LOG_FILE_NAME = "amneziaVPN.rotate.log"
+private const val LOCK_FILE_NAME = ".lock"
+private const val DATE_TIME_PATTERN = "MM-dd HH:mm:ss.SSS"
+private const val PREFS_SAVE_LOGS_KEY = "SAVE_LOGS"
+private const val LOG_MAX_FILE_SIZE = 1024 * 1024
- fun d(tag: String, msg: String) = debugLog(tag, msg, NativeLog::d)
+/**
+ * | Priority | Save to file | Logcat logging |
+ * |-------------------|--------------|----------------------------------------------|
+ * | Verbose | Don't save | Only in Debug build |
+ * | Debug | Save | In Debug build or if log saving is enabled |
+ * | Info, Warn, Error | Save | Enabled |
+ * | Fatal (Assert) | Save | Enabled. Depending on system configuration, |
+ * | | | create a report and/or terminate the process |
+ */
+object Log {
+ private val dateTimeFormat: Any =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
+ else object : ThreadLocal() {
+ override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US)
+ }
- fun i(tag: String, msg: String) = log(tag, msg, NativeLog::i)
+ private lateinit var logDir: File
+ private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
+ private val rotateLogFile: File by lazy { File(logDir, ROTATE_LOG_FILE_NAME) }
- fun w(tag: String, msg: String) = log(tag, msg, NativeLog::w)
+ private val fileLock: FileChannel by lazy { RandomAccessFile(File(logDir, LOCK_FILE_NAME).path, "rw").channel }
+ private val threadLock: ReentrantLock by lazy { ReentrantLock() }
- fun e(tag: String, msg: String) = log(tag, msg, NativeLog::e)
+ @Volatile
+ private var _saveLogs: Boolean = false
+ var saveLogs: Boolean
+ get() = _saveLogs
+ set(value) {
+ if (_saveLogs != value) {
+ if (value && !logDir.exists() && !logDir.mkdir()) {
+ NativeLog.e(TAG, "Failed to create dir: $logDir")
+ return
+ }
+ _saveLogs = value
+ Prefs.save(PREFS_SAVE_LOGS_KEY, value)
+ }
+ }
- fun v(tag: String, msg: Any?) = v(tag, msg.toString())
+ @JvmStatic
+ fun v(tag: String, msg: String) = log(tag, msg, V)
- fun d(tag: String, msg: Any?) = d(tag, msg.toString())
+ @JvmStatic
+ fun d(tag: String, msg: String) = log(tag, msg, D)
- fun i(tag: String, msg: Any?) = i(tag, msg.toString())
+ @JvmStatic
+ fun i(tag: String, msg: String) = log(tag, msg, I)
- fun w(tag: String, msg: Any?) = w(tag, msg.toString())
+ @JvmStatic
+ fun w(tag: String, msg: String) = log(tag, msg, W)
- fun e(tag: String, msg: Any?) = e(tag, msg.toString())
+ @JvmStatic
+ fun e(tag: String, msg: String) = log(tag, msg, E)
- private inline fun log(tag: String, msg: String, delegate: (String, String) -> Unit) = delegate(tag, msg)
+ @JvmStatic
+ fun f(tag: String, msg: String) = log(tag, msg, F)
- private inline fun debugLog(tag: String, msg: String, delegate: (String, String) -> Unit) {
- if (BuildConfig.DEBUG) delegate(tag, msg)
+ fun v(tag: String, msg: Any?) = v(tag, msg.toString())
+
+ fun d(tag: String, msg: Any?) = d(tag, msg.toString())
+
+ fun i(tag: String, msg: Any?) = i(tag, msg.toString())
+
+ fun w(tag: String, msg: Any?) = w(tag, msg.toString())
+
+ fun e(tag: String, msg: Any?) = e(tag, msg.toString())
+
+ fun f(tag: String, msg: Any?) = f(tag, msg.toString())
+
+ fun init(context: Context) {
+ v(TAG, "Init Log")
+ logDir = File(context.cacheDir, "logs")
+ saveLogs = Prefs.load(PREFS_SAVE_LOGS_KEY)
+ }
+
+ fun getLogs(): String =
+ "${deviceInfo()}\n${readLogs()}\nLOGCAT:\n${getLogcat()}"
+
+ fun clearLogs() {
+ withLock {
+ logFile.delete()
+ rotateLogFile.delete()
}
}
+
+ private fun log(tag: String, msg: String, priority: Priority) {
+ if (saveLogs && priority != V) saveLogMsg(formatLogMsg(tag, msg, priority))
+
+ if (priority == F) {
+ NativeLog.wtf(tag, msg)
+ } else if (
+ (priority != V && priority != D) ||
+ (priority == V && BuildConfig.DEBUG) ||
+ (priority == D && (BuildConfig.DEBUG || saveLogs))
+ ) {
+ NativeLog.println(priority.level, tag, msg)
+ }
+ }
+
+ private fun saveLogMsg(msg: String) {
+ withTryLock(condition = { logFile.length() > LOG_MAX_FILE_SIZE }) {
+ logFile.renameTo(rotateLogFile)
+ }
+ try {
+ logFile.appendText(msg)
+ } catch (e: IOException) {
+ NativeLog.e(TAG, "Failed to write log: $e")
+ }
+ }
+
+ private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
+ val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter)
+ } else {
+ @Suppress("UNCHECKED_CAST")
+ (dateTimeFormat as ThreadLocal).get()?.format(Date())
+ }
+ return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
+ "$tag: $msg\n"
+ }
+
+ private fun deviceInfo(): String {
+ val sb = StringBuilder()
+ sb.append("Model: ").appendLine(Build.MODEL)
+ sb.append("Brand: ").appendLine(Build.BRAND)
+ sb.append("Product: ").appendLine(Build.PRODUCT)
+ sb.append("Device: ").appendLine(Build.DEVICE)
+ sb.append("Codename: ").appendLine(Build.VERSION.CODENAME)
+ sb.append("Release: ").appendLine(Build.VERSION.RELEASE)
+ sb.append("SDK: ").appendLine(Build.VERSION.SDK_INT)
+ sb.append("ABI: ").appendLine(Build.SUPPORTED_ABIS.joinToString())
+ return sb.toString()
+ }
+
+ private fun readLogs(): String {
+ var logText = ""
+ withLock {
+ try {
+ if (rotateLogFile.exists()) logText = rotateLogFile.readText()
+ if (logFile.exists()) logText += logFile.readText()
+ } catch (e: IOException) {
+ val errorMsg = "Failed to read log: $e"
+ NativeLog.e(TAG, errorMsg)
+ logText += errorMsg
+ }
+ }
+ return logText
+ }
+
+ private fun getLogcat(): String {
+ try {
+ val process = ProcessBuilder("logcat", "-d").redirectErrorStream(true).start()
+ return process.inputStream.reader().readText()
+ } catch (e: IOException) {
+ val errorMsg = "Failed to get logcat log: $e"
+ NativeLog.e(TAG, errorMsg)
+ return errorMsg
+ }
+ }
+
+ private fun withLock(block: () -> Unit) {
+ threadLock.lock()
+ try {
+ var l: FileLock? = null
+ try {
+ l = fileLock.lock()
+ block()
+ } catch (e: IOException) {
+ NativeLog.e(TAG, "Failed to get file lock: $e")
+ } finally {
+ try {
+ l?.release()
+ } catch (e: IOException) {
+ NativeLog.e(TAG, "Failed to release file lock: $e")
+ }
+ }
+ } finally {
+ threadLock.unlock()
+ }
+ }
+
+ private fun withTryLock(condition: () -> Boolean, block: () -> Unit) {
+ if (condition()) {
+ if (threadLock.tryLock()) {
+ try {
+ if (condition()) {
+ var l: FileLock? = null
+ try {
+ l = fileLock.tryLock()
+ if (l != null) {
+ if (condition()) {
+ block()
+ }
+ }
+ } catch (e: IOException) {
+ NativeLog.e(TAG, "Failed to get file tryLock: $e")
+ } finally {
+ try {
+ l?.release()
+ } catch (e: IOException) {
+ NativeLog.e(TAG, "Failed to release file tryLock: $e")
+ }
+ }
+ }
+ } finally {
+ threadLock.unlock()
+ }
+ }
+ }
+ }
+
+ private enum class Priority(val level: Int) {
+ V(2),
+ D(3),
+ I(4),
+ W(5),
+ E(6),
+ F(7)
+ }
}
diff --git a/client/android/utils/src/main/kotlin/Prefs.kt b/client/android/utils/src/main/kotlin/Prefs.kt
new file mode 100644
index 00000000..9afbc7be
--- /dev/null
+++ b/client/android/utils/src/main/kotlin/Prefs.kt
@@ -0,0 +1,58 @@
+package org.amnezia.vpn.util
+
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import kotlin.reflect.typeOf
+
+private const val TAG = "Prefs"
+private const val PREFS_FILE = "org.amnezia.vpn.prefs"
+private const val SECURE_PREFS_FILE = "$PREFS_FILE.secure"
+
+object Prefs {
+ private lateinit var app: Application
+ val prefs: SharedPreferences
+ get() = try {
+ EncryptedSharedPreferences(
+ app,
+ SECURE_PREFS_FILE,
+ MasterKey(app)
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Getting Encryption Storage failed: $e, plaintext fallback")
+ app.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
+ }
+
+ fun init(app: Application) {
+ Log.v(TAG, "Init Prefs")
+ this.app = app
+ }
+
+ fun save(key: String, value: Boolean) =
+ prefs.edit().putBoolean(key, value).apply()
+
+ fun save(key: String, value: String?) =
+ prefs.edit().putString(key, value).apply()
+
+ fun save(key: String, value: Int) =
+ prefs.edit().putInt(key, value).apply()
+
+ fun save(key: String, value: Long) =
+ prefs.edit().putLong(key, value).apply()
+
+ fun save(key: String, value: Float) =
+ prefs.edit().putFloat(key, value).apply()
+
+ inline fun load(key: String): T {
+ return when (typeOf()) {
+ typeOf() -> prefs.getBoolean(key, false)
+ typeOf() -> prefs.getString(key, "")
+ typeOf() -> prefs.getInt(key, 0)
+ typeOf() -> prefs.getLong(key, 0L)
+ typeOf() -> prefs.getFloat(key, 0f)
+ else -> throw IllegalArgumentException("SharedPreferences does not support type: ${typeOf()}")
+ } as T
+ }
+}
diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt
index 42d7baee..957fc3cb 100644
--- a/client/android/utils/src/main/kotlin/net/NetworkState.kt
+++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt
@@ -82,7 +82,7 @@ class NetworkState(
fun bindNetworkListener() {
if (isListenerBound) return
- Log.v(TAG, "Bind network listener")
+ Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -95,7 +95,7 @@ class NetworkState(
fun unbindNetworkListener() {
if (!isListenerBound) return
- Log.v(TAG, "Unbind network listener")
+ Log.d(TAG, "Unbind network listener")
connectivityManager.unregisterNetworkCallback(networkCallback)
isListenerBound = false
currentNetwork = null
diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp
index 225ceebe..5d442b81 100644
--- a/client/platforms/android/android_controller.cpp
+++ b/client/platforms/android/android_controller.cpp
@@ -12,13 +12,15 @@ namespace
AndroidController *s_instance = nullptr;
constexpr auto QT_ANDROID_CONTROLLER_CLASS = "org/amnezia/vpn/qt/QtAndroidController";
+ constexpr auto ANDROID_LOG_CLASS = "org/amnezia/vpn/util/Log";
+ constexpr auto TAG = "AmneziaQt";
} // namespace
AndroidController::AndroidController() : QObject()
{
connect(this, &AndroidController::status, this,
[this](AndroidController::ConnectionState state) {
- qDebug() << "Android event: status; state:" << textConnectionState(state);
+ qDebug() << "Android event: status =" << textConnectionState(state);
if (isWaitingStatus) {
qDebug() << "Initialization by service status";
isWaitingStatus = false;
@@ -126,24 +128,19 @@ bool AndroidController::initialize()
// static
template
-auto AndroidController::callActivityMethod(const char *methodName, const char *signature,
- const std::function &defValue, Args &&...args)
+auto AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args)
{
qDebug() << "Call activity method:" << methodName;
QJniObject activity = AndroidUtils::getActivity();
- if (activity.isValid()) {
- return activity.callMethod(methodName, signature, std::forward(args)...);
- } else {
- qCritical() << "Activity is not valid";
- return defValue();
- }
+ Q_ASSERT(activity.isValid());
+ return activity.callMethod(methodName, signature, std::forward(args)...);
}
// static
template
void AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args)
{
- callActivityMethod(methodName, signature, [] {}, std::forward(args)...);
+ callActivityMethod(methodName, signature, std::forward(args)...);
}
ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
@@ -199,6 +196,104 @@ void AndroidController::startQrReaderActivity()
callActivityMethod("startQrCodeReader", "()V");
}
+void AndroidController::setSaveLogs(bool enabled)
+{
+ callActivityMethod("setSaveLogs", "(Z)V", enabled);
+}
+
+void AndroidController::exportLogsFile(const QString &fileName)
+{
+ callActivityMethod("exportLogsFile", "(Ljava/lang/String;)V",
+ QJniObject::fromString(fileName).object());
+}
+
+void AndroidController::clearLogs()
+{
+ callActivityMethod("clearLogs", "()V");
+}
+
+// Moving log processing to the Android side
+jclass AndroidController::log;
+jmethodID AndroidController::logDebug;
+jmethodID AndroidController::logInfo;
+jmethodID AndroidController::logWarning;
+jmethodID AndroidController::logError;
+jmethodID AndroidController::logFatal;
+
+// static
+bool AndroidController::initLogging()
+{
+ QJniEnvironment env;
+
+ log = env.findClass(ANDROID_LOG_CLASS);
+ if (log == nullptr) {
+ qCritical() << "Android log class" << ANDROID_LOG_CLASS << "not found";
+ return false;
+ }
+
+ auto logMethodSignature = "(Ljava/lang/String;Ljava/lang/String;)V";
+
+ logDebug = env.findStaticMethod(log, "d", logMethodSignature);
+ if (logDebug == nullptr) {
+ qCritical() << "Android debug log method not found";
+ return false;
+ }
+
+ logInfo = env.findStaticMethod(log, "i", logMethodSignature);
+ if (logInfo == nullptr) {
+ qCritical() << "Android info log method not found";
+ return false;
+ }
+
+ logWarning = env.findStaticMethod(log, "w", logMethodSignature);
+ if (logWarning == nullptr) {
+ qCritical() << "Android warning log method not found";
+ return false;
+ }
+
+ logError = env.findStaticMethod(log, "e", logMethodSignature);
+ if (logError == nullptr) {
+ qCritical() << "Android error log method not found";
+ return false;
+ }
+
+ logFatal = env.findStaticMethod(log, "f", logMethodSignature);
+ if (logFatal == nullptr) {
+ qCritical() << "Android fatal log method not found";
+ return false;
+ }
+
+ qInstallMessageHandler(messageHandler);
+ return true;
+}
+
+// static
+void AndroidController::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
+{
+ jmethodID logMethod = logDebug;
+ switch (type) {
+ case QtDebugMsg:
+ logMethod = logDebug;
+ break;
+ case QtInfoMsg:
+ logMethod = logInfo;
+ break;
+ case QtWarningMsg:
+ logMethod = logWarning;
+ break;
+ case QtCriticalMsg:
+ logMethod = logError;
+ break;
+ case QtFatalMsg:
+ logMethod = logFatal;
+ break;
+ }
+ QString formattedMessage = qFormatLogMessage(type, context, message);
+ QJniObject::callStaticMethod(log, logMethod,
+ QJniObject::fromString(TAG).object(),
+ QJniObject::fromString(formattedMessage).object());
+}
+
void AndroidController::qtAndroidControllerInitialized()
{
callActivityMethod("qtAndroidControllerInitialized", "()V");
diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h
index 481f4b49..48044544 100644
--- a/client/platforms/android/android_controller.h
+++ b/client/platforms/android/android_controller.h
@@ -34,6 +34,12 @@ public:
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
void startQrReaderActivity();
+ void setSaveLogs(bool enabled);
+ void exportLogsFile(const QString &fileName);
+ void clearLogs();
+
+ static bool initLogging();
+ static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
signals:
void connectionStateChanged(Vpn::ConnectionState state);
@@ -53,6 +59,13 @@ signals:
private:
bool isWaitingStatus = true;
+ static jclass log;
+ static jmethodID logDebug;
+ static jmethodID logInfo;
+ static jmethodID logWarning;
+ static jmethodID logError;
+ static jmethodID logFatal;
+
void qtAndroidControllerInitialized();
static Vpn::ConnectionState convertState(ConnectionState state);
@@ -72,8 +85,7 @@ private:
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
template
- static auto callActivityMethod(const char *methodName, const char *signature,
- const std::function &defValue, Args &&...args);
+ static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
template
static void callActivityMethod(const char *methodName, const char *signature, Args &&...args);
};
diff --git a/client/settings.cpp b/client/settings.cpp
index f3fb57a7..475c52e7 100644
--- a/client/settings.cpp
+++ b/client/settings.cpp
@@ -216,6 +216,7 @@ QString Settings::nextAvailableServerName() const
void Settings::setSaveLogs(bool enabled)
{
setValue("Conf/saveLogs", enabled);
+#ifndef Q_OS_ANDROID
if (!isSaveLogs()) {
Logger::deInit();
} else {
@@ -223,7 +224,8 @@ void Settings::setSaveLogs(bool enabled)
qWarning() << "Initialization of debug subsystem failed";
}
}
- emit saveLogsChanged();
+#endif
+ emit saveLogsChanged(enabled);
}
QString Settings::routeModeString(RouteMode mode) const
diff --git a/client/settings.h b/client/settings.h
index 50a28dc4..ed302653 100644
--- a/client/settings.h
+++ b/client/settings.h
@@ -190,7 +190,7 @@ public:
void clearSettings();
signals:
- void saveLogsChanged();
+ void saveLogsChanged(bool enabled);
private:
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp
index f7345608..8ac4cca0 100644
--- a/client/ui/controllers/settingsController.cpp
+++ b/client/ui/controllers/settingsController.cpp
@@ -8,6 +8,7 @@
#include "version.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_utils.h"
+ #include "platforms/android/android_controller.h"
#include
#endif
@@ -91,13 +92,21 @@ void SettingsController::openLogsFolder()
void SettingsController::exportLogsFile(const QString &fileName)
{
+#ifdef Q_OS_ANDROID
+ AndroidController::instance()->exportLogsFile(fileName);
+#else
SystemController::saveFile(fileName, Logger::getLogFile());
+#endif
}
void SettingsController::clearLogs()
{
+#ifdef Q_OS_ANDROID
+ AndroidController::instance()->clearLogs();
+#else
Logger::clearLogs();
Logger::clearServiceLogs();
+#endif
}
void SettingsController::backupAppConfig(const QString &fileName)