Files
amnezia-client/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
T

1201 lines
43 KiB
Kotlin
Raw Normal View History

2023-11-16 20:16:28 +03:00
package org.amnezia.vpn
2024-05-12 18:04:14 +03:00
import android.Manifest
2024-08-06 12:41:44 +03:00
import android.annotation.SuppressLint
import android.app.AlertDialog
2024-05-12 18:04:14 +03:00
import android.app.NotificationManager
2024-12-31 04:16:52 +01:00
import android.content.ActivityNotFoundException
2024-05-12 18:04:14 +03:00
import android.content.BroadcastReceiver
2023-11-23 20:30:03 +03:00
import android.content.ComponentName
import android.content.Intent
2023-12-26 16:23:05 +03:00
import android.content.Intent.EXTRA_MIME_TYPES
2023-12-11 22:56:01 +03:00
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
2023-11-23 20:30:03 +03:00
import android.content.ServiceConnection
import android.content.pm.PackageManager
2024-04-01 18:45:00 +07:00
import android.graphics.Bitmap
2024-12-31 04:16:52 +01:00
import android.net.Uri
2023-11-23 20:30:03 +03:00
import android.net.VpnService
2024-05-12 18:04:14 +03:00
import android.os.Build
2023-11-23 20:30:03 +03:00
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
2024-12-31 04:16:52 +01:00
import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.InputDevice
import android.view.KeyEvent
2024-10-18 12:52:24 +03:00
import android.view.MotionEvent
2024-12-31 04:16:52 +01:00
import android.view.View
import android.view.ViewGroup
2024-03-06 04:18:19 +03:00
import android.view.WindowManager.LayoutParams
2023-12-26 16:23:05 +03:00
import android.webkit.MimeTypeMap
2023-11-23 20:30:03 +03:00
import android.widget.Toast
import androidx.annotation.MainThread
2024-05-12 18:04:14 +03:00
import androidx.annotation.RequiresApi
2023-11-23 20:30:03 +03:00
import androidx.core.content.ContextCompat
2025-11-11 17:03:27 +03:00
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
2025-11-04 11:43:36 +08:00
import androidx.core.view.WindowInsetsControllerCompat
import java.io.IOException
2023-11-23 20:30:03 +03:00
import kotlin.LazyThreadSafetyMode.NONE
2024-12-31 04:16:52 +01:00
import kotlin.coroutines.CoroutineContext
2023-12-26 16:23:05 +03:00
import kotlin.text.RegexOption.IGNORE_CASE
2024-04-01 18:45:00 +07:00
import AppListProvider
2023-11-23 20:30:03 +03:00
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
2024-06-18 20:46:21 +03:00
import kotlinx.coroutines.async
2023-11-23 20:30:03 +03:00
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
2024-04-01 18:45:00 +07:00
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
2023-11-23 20:30:03 +03:00
import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
import org.amnezia.vpn.util.Log
2024-05-12 18:04:14 +03:00
import org.amnezia.vpn.util.Prefs
2024-06-18 20:46:21 +03:00
import org.json.JSONException
import org.json.JSONObject
2022-12-23 17:32:20 +03:00
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
2024-03-04 18:08:55 +03:00
const val ACTIVITY_MESSENGER_NAME = "Activity"
2023-03-29 16:09:46 +03:00
2023-11-23 20:30:03 +03:00
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
2023-12-26 16:23:05 +03:00
private const val OPEN_FILE_ACTION_CODE = 3
2024-05-12 18:04:14 +03:00
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
2026-02-27 17:43:36 +07:00
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
class AmneziaActivity : QtActivity() {
2023-11-23 20:30:03 +03:00
private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>()
2024-06-18 20:46:21 +03:00
private var vpnProto: VpnProto? = null
2023-11-23 20:30:03 +03:00
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
2024-05-12 18:04:14 +03:00
private var notificationStateReceiver: BroadcastReceiver? = null
2023-11-24 17:10:08 +03:00
private lateinit var vpnServiceMessenger: IpcMessenger
2024-12-31 04:16:52 +01:00
private var pfd: ParcelFileDescriptor? = null
2024-05-12 18:04:14 +03:00
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
2026-02-10 06:15:31 +03:00
private var isActivityResumed = false
private var hasWindowFocus = false
private val resumeHandler = Handler(Looper.getMainLooper())
2026-02-27 17:43:36 +07:00
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
2023-11-23 20:30:03 +03:00
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
val event = msg.extractIpcMessage<ServiceEvent>()
Log.d(TAG, "Handle event: $event")
when (event) {
2024-03-04 18:08:55 +03:00
ServiceEvent.STATUS_CHANGED -> {
msg.data?.getStatus()?.let { (state) ->
Log.d(TAG, "Handle protocol state: $state")
QtAndroidController.onVpnStateChanged(state.ordinal)
}
}
2023-11-23 20:30:03 +03:00
ServiceEvent.STATUS -> {
if (isWaitingStatus) {
isWaitingStatus = false
2024-03-04 18:08:55 +03:00
msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
2023-11-23 20:30:03 +03:00
}
}
ServiceEvent.STATISTICS_UPDATE -> {
msg.data?.getStatistics()?.let { (rxBytes, txBytes) ->
QtAndroidController.onStatisticsUpdate(rxBytes, txBytes)
}
}
ServiceEvent.ERROR -> {
2024-03-04 18:08:55 +03:00
msg.data?.getString(MSG_ERROR)?.let { error ->
2023-11-24 21:51:09 +03:00
Log.e(TAG, "From VpnService: $error")
}
2023-11-23 20:30:03 +03:00
// todo: add error reporting to Qt
QtAndroidController.onServiceError()
}
}
}
}
}
private val activityMessenger: Messenger by lazy(NONE) {
Messenger(vpnServiceEventHandler)
}
private val serviceConnection: ServiceConnection by lazy(NONE) {
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "Service ${name?.flattenToString()} was connected")
// get a messenger from the service to send actions to the service
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.set(Messenger(service))
2023-11-23 20:30:03 +03:00
// send a messenger to the service to process service events
2024-03-04 18:08:55 +03:00
vpnServiceMessenger.send(
Action.REGISTER_CLIENT.packToMessage {
putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
},
replyTo = activityMessenger
)
2023-11-23 20:30:03 +03:00
isServiceConnected = true
if (isWaitingStatus) {
2024-03-04 18:08:55 +03:00
vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
2023-11-23 20:30:03 +03:00
}
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.w(TAG, "Service ${name?.flattenToString()} was unexpectedly disconnected")
isServiceConnected = false
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.reset()
2023-11-23 20:30:03 +03:00
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
2024-03-04 18:08:55 +03:00
doBindService()
2023-11-23 20:30:03 +03:00
}
override fun onBindingDied(name: ComponentName?) {
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
doUnbindService()
2024-06-18 20:46:21 +03:00
QtAndroidController.onServiceDisconnected()
2023-11-23 20:30:03 +03:00
doBindService()
}
}
}
/**
* Activity overloaded methods
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2024-10-18 12:52:24 +03:00
Log.d(TAG, "Create Amnezia activity")
loadLibs()
2025-11-04 11:43:36 +08:00
// Configure window for edge-to-edge display
configureWindowForEdgeToEdge()
2023-11-23 20:30:03 +03:00
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
2024-06-18 20:46:21 +03:00
val proto = mainScope.async(Dispatchers.IO) {
VpnStateStore.getVpnState().vpnProto
}
2023-11-24 17:10:08 +03:00
vpnServiceMessenger = IpcMessenger(
2024-03-04 18:08:55 +03:00
"VpnService",
onDeadObjectException = {
doUnbindService()
2024-06-18 20:46:21 +03:00
QtAndroidController.onServiceDisconnected()
2024-03-04 18:08:55 +03:00
doBindService()
}
2023-11-24 17:10:08 +03:00
)
2026-02-27 17:43:36 +07:00
pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI)
openFileDeliveryScheduled = false
2024-05-12 18:04:14 +03:00
registerBroadcastReceivers()
2023-12-11 22:56:01 +03:00
intent?.let(::processIntent)
2024-06-18 20:46:21 +03:00
runBlocking { vpnProto = proto.await() }
2023-12-11 22:56:01 +03:00
}
2026-02-27 17:43:36 +07:00
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
pendingOpenFileUri?.let { outState.putString(KEY_PENDING_OPEN_FILE_URI, it) }
}
private fun loadLibs() {
listOf(
"rsapss",
"crypto_3",
"ssl_3",
"ssh"
).forEach {
loadSharedLibrary(this.applicationContext, it)
}
}
2024-05-12 18:04:14 +03:00
private fun registerBroadcastReceivers() {
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
2024-10-18 12:52:24 +03:00
Log.v(
2024-05-12 18:04:14 +03:00
TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
)
mainScope.launch {
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
}
} else null
}
2023-12-11 22:56:01 +03:00
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
2024-10-18 12:52:24 +03:00
Log.v(TAG, "onNewIntent: $intent")
2023-12-11 22:56:01 +03:00
intent?.let(::processIntent)
}
private fun processIntent(intent: Intent) {
// disable config import when starting activity from history
if (intent.flags and FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0) {
if (intent.action == ACTION_IMPORT_CONFIG) {
intent.getStringExtra(EXTRA_CONFIG)?.let {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onConfigImported(it)
}
}
}
}
2023-11-23 20:30:03 +03:00
}
override fun onStart() {
super.onStart()
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Start Amnezia activity")
2023-11-23 20:30:03 +03:00
mainScope.launch {
qtInitialized.await()
2024-06-18 20:46:21 +03:00
vpnProto?.let { proto ->
if (AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
doBindService()
}
}
2023-11-23 20:30:03 +03:00
}
}
override fun onStop() {
2026-02-10 06:15:31 +03:00
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity stops
resumeHandler.removeCallbacksAndMessages(null)
2026-02-27 17:43:36 +07:00
openFileDeliveryScheduled = false
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Stop Amnezia activity")
2023-11-23 20:30:03 +03:00
doUnbindService()
2024-08-06 12:44:51 +03:00
mainScope.launch {
qtInitialized.await()
QtAndroidController.onServiceDisconnected()
}
2023-11-23 20:30:03 +03:00
super.onStop()
}
2025-12-15 16:56:36 +03:00
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
2026-02-10 06:15:31 +03:00
hasWindowFocus = hasFocus
2025-12-15 16:56:36 +03:00
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
2026-02-10 06:15:31 +03:00
if (!hasFocus) {
2026-03-25 14:48:32 +03:00
// Cancel pending operations if window loses focus
2026-02-10 06:15:31 +03:00
resumeHandler.removeCallbacksAndMessages(null)
2026-03-25 14:48:32 +03:00
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 50)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
requestLayout()
invalidate()
}
}, 150)
}
2026-02-10 06:15:31 +03:00
}
2025-12-15 16:56:36 +03:00
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val keyCode = event.keyCode
val pressed = event.action == KeyEvent.ACTION_DOWN
2026-03-06 12:05:16 +03:00
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT -> {
2026-03-09 12:39:50 +03:00
nativeGamepadKeyEvent(0, keyCode, pressed)
return true
2026-03-06 12:05:16 +03:00
}
KeyEvent.KEYCODE_DPAD_CENTER,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT -> {
2026-03-09 12:39:50 +03:00
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
val synthetic = KeyEvent(
event.downTime, event.eventTime, event.action, syntheticKeyCode,
event.repeatCount, event.metaState, -1, event.scanCode,
event.flags, InputDevice.SOURCE_KEYBOARD
)
return super.dispatchKeyEvent(synthetic)
}
}
return super.dispatchKeyEvent(event)
}
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
2025-12-15 16:56:36 +03:00
override fun onPause() {
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
// Using a coroutine here would be too late — the surface is gone by the time
// the coroutine runs. A direct synchronous call gives Qt's render thread the
// best chance to process visible=false before surface destruction.
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
2025-12-15 16:56:36 +03:00
super.onPause()
2026-02-10 06:15:31 +03:00
isActivityResumed = false
// Cancel all pending operations when activity pauses
resumeHandler.removeCallbacksAndMessages(null)
2026-02-27 17:43:36 +07:00
openFileDeliveryScheduled = false
2025-12-15 16:56:36 +03:00
Log.d(TAG, "Pause Amnezia activity")
}
2025-11-04 11:43:36 +08:00
override fun onResume() {
super.onResume()
2026-02-10 06:15:31 +03:00
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
2026-03-24 17:13:31 +03:00
QtAndroidController.onActivityResumed()
}
2026-02-10 06:15:31 +03:00
2026-02-27 17:43:36 +07:00
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!!
openFileDeliveryScheduled = true
resumeHandler.postDelayed({
if (!isFinishing && !isDestroyed) {
pendingOpenFileUri = null
openFileDeliveryScheduled = false
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
}, OPEN_FILE_AFTER_RESUME_DELAY_MS)
}
2026-02-10 06:15:31 +03:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2025-11-04 11:43:36 +08:00
window.decorView.apply {
invalidate()
2026-02-10 06:15:31 +03:00
resumeHandler.postDelayed({
// Check if activity is still resumed and has focus before executing
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
2025-11-04 11:43:36 +08:00
}, 100)
2026-02-10 06:15:31 +03:00
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
}
2025-11-04 11:43:36 +08:00
}, 200)
2026-02-10 06:15:31 +03:00
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
requestLayout()
invalidate()
}
2025-11-04 11:43:36 +08:00
}, 250)
}
2026-02-10 06:15:31 +03:00
}
2025-11-04 11:43:36 +08:00
}
private fun configureWindowForEdgeToEdge() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
addFlags(LayoutParams.FLAG_LAYOUT_NO_LIMITS)
statusBarColor = android.graphics.Color.TRANSPARENT
navigationBarColor = android.graphics.Color.TRANSPARENT
}
WindowInsetsControllerCompat(window, window.decorView).apply {
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = false
}
2025-11-11 17:03:27 +03:00
// Workaround for Android 14 (API 34+) IME adjustResize bug
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setupImeInsetsListener()
}
2025-11-04 11:43:36 +08:00
} else {
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = getColor(R.color.black)
}
2025-12-15 16:56:36 +03:00
WindowInsetsControllerCompat(window, window.decorView).apply {
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = false
}
2025-11-04 11:43:36 +08:00
}
}
2025-11-11 17:03:27 +03:00
private fun setupImeInsetsListener() {
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
2025-11-11 17:03:27 +03:00
val imeHeight = if (imeVisible) imeInsets.bottom else 0
val density = resources.displayMetrics.density
val imeHeightDp = (imeHeight / density).toInt()
2025-11-11 17:03:27 +03:00
// Also track system bars (navigation bar, status bar) changes
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val navBarHeight = systemBarsInsets.bottom
val navBarHeightDp = (navBarHeight / density).toInt()
val statusBarHeight = systemBarsInsets.top
val statusBarHeightDp = (statusBarHeight / density).toInt()
2025-11-11 17:03:27 +03:00
mainScope.launch {
qtInitialized.await()
QtAndroidController.onImeInsetsChanged(imeHeightDp)
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
}
2025-11-11 17:03:27 +03:00
// Return windowInsets instead of CONSUMED to allow proper handling
windowInsets
}
}
2023-11-23 20:30:03 +03:00
override fun onDestroy() {
2026-02-10 06:15:31 +03:00
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity is destroyed
resumeHandler.removeCallbacksAndMessages(null)
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Destroy Amnezia activity")
2024-05-12 18:04:14 +03:00
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
2023-11-23 20:30:03 +03:00
mainScope.cancel()
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
2024-05-12 18:04:14 +03:00
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")
actionResultHandlers[requestCode]?.let { handler ->
when (resultCode) {
RESULT_OK -> handler.onSuccess(data)
else -> handler.onFail(data)
2023-12-26 16:23:05 +03:00
}
2024-05-12 18:04:14 +03:00
handler.onAny(data)
actionResultHandlers.remove(requestCode)
} ?: super.onActivityResult(requestCode, resultCode, data)
}
2023-12-26 16:23:05 +03:00
2024-05-12 18:04:14 +03:00
private fun startActivityForResult(intent: Intent, requestCode: Int, handler: ActivityResultHandler) {
actionResultHandlers[requestCode] = handler
startActivityForResult(intent, requestCode)
}
2023-11-23 20:30:03 +03:00
2024-05-12 18:04:14 +03:00
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Log.d(TAG, "Process permission result, code: ${actionCodeToString(requestCode)}, " +
"permissions: ${permissions.contentToString()}, results: ${grantResults.contentToString()}")
permissionRequestHandlers[requestCode]?.let { handler ->
if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) handler.onSuccess()
else handler.onFail()
2023-11-23 20:30:03 +03:00
}
2024-05-12 18:04:14 +03:00
handler.onAny()
permissionRequestHandlers.remove(requestCode)
} ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
2023-11-23 20:30:03 +03:00
2024-05-12 18:04:14 +03:00
private fun requestPermission(permission: String, requestCode: Int, handler: PermissionRequestHandler) {
permissionRequestHandlers[requestCode] = handler
requestPermissions(arrayOf(permission), requestCode)
2023-11-23 20:30:03 +03:00
}
/**
* Methods for service binding
*/
@MainThread
private fun doBindService() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Bind service")
2024-06-18 20:46:21 +03:00
vpnProto?.let { proto ->
Intent(this, proto.serviceClass).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
}
isInBoundState = true
2023-11-23 20:30:03 +03:00
}
}
@MainThread
private fun doUnbindService() {
if (isInBoundState) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Unbind service")
2023-11-23 20:30:03 +03:00
isWaitingStatus = true
isServiceConnected = false
2024-03-04 18:08:55 +03:00
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
2023-11-23 20:30:03 +03:00
isInBoundState = false
unbindService(serviceConnection)
}
}
/**
* Methods of starting and stopping VpnService
*/
@MainThread
2024-05-12 18:04:14 +03:00
private fun checkVpnPermission(onPermissionGranted: () -> Unit) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Check VPN permission")
2024-05-12 18:04:14 +03:00
VpnService.prepare(applicationContext)?.let { intent ->
startActivityForResult(intent, CHECK_VPN_PERMISSION_ACTION_CODE, ActivityResultHandler(
onSuccess = {
Log.d(TAG, "Vpn permission granted")
Toast.makeText(this@AmneziaActivity, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show()
onPermissionGranted()
},
onFail = {
Log.w(TAG, "Vpn permission denied")
showOnVpnPermissionRejectDialog()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onVpnPermissionRejected()
}
}
))
} ?: onPermissionGranted()
2023-11-23 20:30:03 +03:00
}
private fun showOnVpnPermissionRejectDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.vpnSetupFailed)
.setMessage(R.string.vpnSetupFailedMessage)
.setNegativeButton(R.string.ok) { _, _ -> }
.setPositiveButton(R.string.openVpnSettings) { _, _ ->
startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
}
.show()
}
2024-05-12 18:04:14 +03:00
private fun checkNotificationPermission(onChecked: () -> Unit) {
Log.d(TAG, "Check notification permission")
if (
!isNotificationPermissionGranted() &&
!Prefs.load<Boolean>(PREFS_NOTIFICATION_PERMISSION_ASKED)
) {
showNotificationPermissionDialog(onChecked)
} else {
onChecked()
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun showNotificationPermissionDialog(onChecked: () -> Unit) {
AlertDialog.Builder(this)
.setTitle(R.string.notificationDialogTitle)
.setMessage(R.string.notificationDialogMessage)
.setNegativeButton(R.string.no) { _, _ ->
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
onChecked()
}
.setPositiveButton(R.string.yes) { _, _ ->
val saveAsked: () -> Unit = {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
}
requestPermission(
Manifest.permission.POST_NOTIFICATIONS,
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = saveAsked,
onFail = saveAsked,
onAny = onChecked
)
)
}
.show()
}
2023-11-23 20:30:03 +03:00
@MainThread
private fun startVpn(vpnConfig: String) {
2024-06-18 20:46:21 +03:00
getVpnProto(vpnConfig)?.let { proto ->
2024-10-18 12:52:24 +03:00
Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto")
2024-06-18 20:46:21 +03:00
if (isServiceConnected) {
2024-07-06 18:44:34 +03:00
if (proto.serviceClass == vpnProto?.serviceClass) {
vpnProto = proto
2024-06-18 20:46:21 +03:00
connectToVpn(vpnConfig)
return
}
doUnbindService()
}
vpnProto = proto
2023-11-23 20:30:03 +03:00
isWaitingStatus = false
2024-06-18 20:46:21 +03:00
startVpnService(vpnConfig, proto)
2023-11-23 20:30:03 +03:00
doBindService()
2024-06-18 20:46:21 +03:00
} ?: QtAndroidController.onServiceError()
}
private fun getVpnProto(vpnConfig: String): VpnProto? = try {
require(vpnConfig.isNotBlank()) { "Blank VPN config" }
VpnProto.get(JSONObject(vpnConfig).getString("protocol"))
} catch (e: JSONException) {
Log.e(TAG, "Invalid VPN config json format: ${e.message}")
null
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Protocol not found: ${e.message}")
null
2023-11-23 20:30:03 +03:00
}
private fun connectToVpn(vpnConfig: String) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Connect to VPN")
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.send {
2023-11-23 20:30:03 +03:00
Action.CONNECT.packToMessage {
2024-03-04 18:08:55 +03:00
putString(MSG_VPN_CONFIG, vpnConfig)
2023-03-29 16:09:46 +03:00
}
}
}
2024-06-18 20:46:21 +03:00
private fun startVpnService(vpnConfig: String, proto: VpnProto) {
Log.d(TAG, "Start VPN service: $proto")
Intent(this, proto.serviceClass).apply {
2024-03-04 18:08:55 +03:00
putExtra(MSG_VPN_CONFIG, vpnConfig)
2023-11-23 20:30:03 +03:00
}.also {
2024-05-12 18:04:14 +03:00
try {
ContextCompat.startForegroundService(this, it)
} catch (e: SecurityException) {
2024-06-18 20:46:21 +03:00
Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
2024-05-12 18:04:14 +03:00
QtAndroidController.onServiceError()
}
2023-11-23 20:30:03 +03:00
}
}
2024-05-12 18:04:14 +03:00
@MainThread
2023-11-23 20:30:03 +03:00
private fun disconnectFromVpn() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Disconnect from VPN")
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.send(Action.DISCONNECT)
2023-11-23 20:30:03 +03:00
}
/**
* Methods called by Qt
*/
@Suppress("unused")
fun qtAndroidControllerInitialized() {
Log.v(TAG, "Qt Android controller initialized")
2023-11-23 20:30:03 +03:00
qtInitialized.complete(Unit)
}
@Suppress("unused")
fun start(vpnConfig: String) {
Log.v(TAG, "Start VPN")
2023-11-23 20:30:03 +03:00
mainScope.launch {
2024-05-12 18:04:14 +03:00
checkVpnPermission {
checkNotificationPermission {
startVpn(vpnConfig)
}
}
2023-11-23 20:30:03 +03:00
}
2022-12-23 17:32:20 +03:00
}
@Suppress("unused")
fun stop() {
Log.v(TAG, "Stop VPN")
2023-11-23 20:30:03 +03:00
mainScope.launch {
disconnectFromVpn()
}
2022-12-23 17:32:20 +03:00
}
2024-03-04 18:08:55 +03:00
@Suppress("unused")
fun resetLastServer(index: Int) {
Log.v(TAG, "Reset server: $index")
mainScope.launch {
VpnStateStore.store {
if (index == -1 || it.serverIndex == index) {
VpnState.defaultState
} else if (it.serverIndex > index) {
it.copy(serverIndex = it.serverIndex - 1)
} else {
it
}
}
}
}
@Suppress("unused")
fun saveFile(fileName: String, data: String) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Save file $fileName")
mainScope.launch {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
2024-12-31 04:16:52 +01:00
try {
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
Log.v(TAG, "Save file to $uri")
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) }
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
2024-05-12 18:04:14 +03:00
}
}
}
2024-12-31 04:16:52 +01:00
))
} catch (_: ActivityNotFoundException) {
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
}
}
2022-12-23 17:32:20 +03:00
}
}
2023-12-26 16:23:05 +03:00
@Suppress("unused")
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
2024-05-12 18:04:14 +03:00
mainScope.launch {
2024-12-31 04:16:52 +01:00
val intent = if (!isOnTv()) {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
2023-12-26 16:23:05 +03:00
2024-12-31 04:16:52 +01:00
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
2023-12-26 16:23:05 +03:00
2024-12-31 04:16:52 +01:00
else -> type = "*/*"
}
2024-05-12 18:04:14 +03:00
}
2023-12-26 16:23:05 +03:00
}
2024-12-31 04:16:52 +01:00
} else {
Intent(this@AmneziaActivity, TvFilePicker::class.java)
}
try {
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
2024-08-06 12:44:51 +03:00
onAny = {
2024-12-31 04:16:52 +01:00
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog()
}
val uri = it?.data?.apply {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}?.toString() ?: ""
2024-10-18 12:52:24 +03:00
Log.v(TAG, "Open file: $uri")
2026-02-27 17:43:36 +07:00
if (uri.isNotEmpty()) {
pendingOpenFileUri = uri
} else {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
2024-05-12 18:04:14 +03:00
}
}
))
2024-12-31 04:16:52 +01:00
} catch (_: ActivityNotFoundException) {
showNoFileBrowserAlertDialog()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened("")
}
2023-12-26 16:23:05 +03:00
}
}
}
2024-12-31 04:16:52 +01:00
private fun showNoFileBrowserAlertDialog() {
AlertDialog.Builder(this)
.setMessage(R.string.tvNoFileBrowser)
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ ->
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
} catch (_: Throwable) {}
}
.show()
}
@Suppress("unused")
fun getFd(fileName: String): Int {
Log.v(TAG, "Get fd for $fileName")
2026-03-21 06:46:46 +03:00
return blockingCall(Dispatchers.IO) {
2024-12-31 04:16:52 +01:00
try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1
} catch (e: Exception) {
Log.e(TAG, "Failed to get fd: $e")
-1
}
}
}
@Suppress("unused")
fun closeFd() {
Log.v(TAG, "Close fd")
mainScope.launch {
pfd?.close()
pfd = null
}
}
@Suppress("unused")
fun getFileName(uri: String): String {
Log.v(TAG, "Get file name for uri: $uri")
return blockingCall {
try {
contentResolver.query(Uri.parse(uri), arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
if (cursor.moveToFirst() && !cursor.isNull(0)) {
return@blockingCall cursor.getString(0) ?: ""
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get file name: $e")
}
""
}
}
@Suppress("unused")
2024-08-06 12:41:44 +03:00
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
2024-08-06 12:41:44 +03:00
@Suppress("unused")
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
2025-11-04 11:43:36 +08:00
@Suppress("unused")
fun isEdgeToEdgeEnabled(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
@Suppress("unused")
fun getStatusBarHeight(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
val heightPx = if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else {
0
}
// Convert physical pixels to device-independent pixels for QML
val density = resources.displayMetrics.density
val heightDp = (heightPx / density).toInt()
return heightDp
}
@Suppress("unused")
fun getNavigationBarHeight(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
val heightPx = if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else {
0
}
// Convert physical pixels to device-independent pixels for QML
val density = resources.displayMetrics.density
val heightDp = (heightPx / density).toInt()
return heightDp
}
@Suppress("unused")
fun startQrCodeReader() {
Log.v(TAG, "Start camera")
2023-11-21 22:48:52 +03:00
Intent(this, CameraActivity::class.java).also {
startActivity(it)
}
}
2024-01-20 16:40:12 +03:00
@Suppress("unused")
fun setSaveLogs(enabled: Boolean) {
Log.v(TAG, "Set save logs: $enabled")
2024-01-20 16:40:12 +03:00
mainScope.launch {
Log.saveLogs = enabled
vpnServiceMessenger.send {
Action.SET_SAVE_LOGS.packToMessage {
2024-03-04 18:08:55 +03:00
putBoolean(MSG_SAVE_LOGS, enabled)
2024-01-20 16:40:12 +03:00
}
}
}
}
@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")
mainScope.launch {
Log.clearLogs()
}
2024-01-20 16:40:12 +03:00
}
2024-03-06 04:18:19 +03:00
@Suppress("unused")
fun setScreenshotsEnabled(enabled: Boolean) {
Log.v(TAG, "Set screenshots enabled: $enabled")
mainScope.launch {
val flag = if (enabled) 0 else LayoutParams.FLAG_SECURE
window.setFlags(flag, LayoutParams.FLAG_SECURE)
}
}
2024-09-09 14:36:33 +03:00
@Suppress("unused")
fun setNavigationBarColor(color: Int) {
Log.v(TAG, "Change navigation bar color: ${"#%08X".format(color)}")
mainScope.launch {
window.navigationBarColor = color
}
}
@Suppress("unused")
fun minimizeApp() {
Log.v(TAG, "Minimize application")
mainScope.launch {
moveTaskToBack(false)
}
}
2024-04-01 18:45:00 +07:00
@Suppress("unused")
fun getAppList(): String {
Log.v(TAG, "Get app list")
var appList = ""
runBlocking {
mainScope.launch {
withContext(Dispatchers.IO) {
appList = AppListProvider.getAppList(packageManager, packageName)
}
}.join()
}
return appList
}
@Suppress("unused")
fun getAppIcon(packageName: String, width: Int, height: Int): Bitmap {
Log.v(TAG, "Get app icon")
2024-04-01 18:45:00 +07:00
return AppListProvider.getAppIcon(packageManager, packageName, width, height)
}
2024-05-12 18:04:14 +03:00
@Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
@Suppress("unused")
fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
requestPermission(
Manifest.permission.POST_NOTIFICATIONS,
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = {
mainScope.launch {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
vpnServiceMessenger.send(Action.NOTIFICATION_PERMISSION_GRANTED)
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
},
onFail = {
if (!Prefs.load<Boolean>(PREFS_NOTIFICATION_PERMISSION_ASKED)) {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
} else {
val shouldShowPostRequest =
shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
if (!shouldShowPreRequest && !shouldShowPostRequest) {
showNotificationSettingsDialog()
}
}
}
)
)
}
private fun showNotificationSettingsDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.notificationSettingsDialogTitle)
.setMessage(R.string.notificationSettingsDialogMessage)
.setNegativeButton(R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.openNotificationSettings) { _, _ ->
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
})
}
.show()
}
2024-09-09 14:36:33 +03:00
@Suppress("unused")
fun requestAuthentication() {
Log.v(TAG, "Request authentication")
mainScope.launch {
qtInitialized.await()
Intent(this@AmneziaActivity, AuthActivity::class.java).also {
startActivity(it)
}
}
}
2024-12-31 04:16:52 +01:00
// method to workaround Qt's problem with calling the keyboard on TVs
@Suppress("unused")
fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y")
blockingCall {
findQtWindow(window.decorView)?.let {
Log.v(TAG, "Send touch to $it")
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
}
}
}
private fun findQtWindow(view: View): View? {
Log.v(TAG, "findQtWindow: process $view")
if (view::class.simpleName == "QtWindow") return view
else if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val result = findQtWindow(view.getChildAt(i))
if (result != null) return result
}
return null
} else return null
}
private fun createEvent(x: Float, y: Float, eventTime: Long, action: Int): MotionEvent =
MotionEvent.obtain(
eventTime,
eventTime,
action,
1,
arrayOf(MotionEvent.PointerProperties().apply {
id = 0
toolType = MotionEvent.TOOL_TYPE_FINGER
}),
arrayOf(MotionEvent.PointerCoords().apply {
this.x = x
this.y = y
pressure = 1f
size = 1f
}),
0, 0, 1.0f, 1.0f, 0, 0, 0,0
)
2024-10-18 12:52:24 +03:00
// workaround for a bug in Qt that causes the mouse click event not to be handled
// also disable right-click, as it causes the application to crash
private var lastButtonState = 0
private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain(
downTime,
eventTime,
action,
pointerCount,
(0 until pointerCount).map { i ->
MotionEvent.PointerProperties().apply {
getPointerProperties(i, this)
}
}.toTypedArray(),
(0 until pointerCount).map { i ->
MotionEvent.PointerCoords().apply {
getPointerCoords(i, this)
}
}.toTypedArray(),
metaState,
MotionEvent.BUTTON_PRIMARY,
xPrecision,
yPrecision,
deviceId,
edgeFlags,
source,
flags
)
private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastButtonState = ev.buttonState
if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true
}
MotionEvent.ACTION_UP -> {
when (lastButtonState) {
MotionEvent.BUTTON_SECONDARY -> return true
MotionEvent.BUTTON_PRIMARY -> {
val modEvent = ev.fixCopy()
return superDispatch(modEvent).apply { modEvent.recycle() }
}
}
}
}
return superDispatch(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
2024-12-31 04:16:52 +01:00
Log.v(TAG, "dispatchTouch: $ev")
2024-10-18 12:52:24 +03:00
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
}
return super.dispatchTouchEvent(ev)
}
override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }}
return super.dispatchTrackballEvent(ev)
}
2024-05-12 18:04:14 +03:00
/**
* Utils methods
*/
2024-12-31 04:16:52 +01:00
private fun <T> blockingCall(
context: CoroutineContext = Dispatchers.Main.immediate,
block: suspend () -> T
) = runBlocking {
mainScope.async(context) { block() }.await()
}
2024-05-12 18:04:14 +03:00
companion object {
private fun actionCodeToString(actionCode: Int): String =
when (actionCode) {
CHECK_VPN_PERMISSION_ACTION_CODE -> "CHECK_VPN_PERMISSION"
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
else -> actionCode.toString()
}
}
}
2024-05-12 18:04:14 +03:00
private class ActivityResultHandler(
val onSuccess: (data: Intent?) -> Unit = {},
val onFail: (data: Intent?) -> Unit = {},
val onAny: (data: Intent?) -> Unit = {}
)
private class PermissionRequestHandler(
val onSuccess: () -> Unit = {},
val onFail: () -> Unit = {},
val onAny: () -> Unit = {}
)