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

704 lines
25 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
import android.app.AlertDialog
2024-05-12 18:04:14 +03:00
import android.app.NotificationManager
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
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
import android.provider.Settings
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
import java.io.IOException
2023-11-23 20:30:03 +03:00
import kotlin.LazyThreadSafetyMode.NONE
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.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"
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-05-12 18:04:14 +03:00
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
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-01-20 16:40:12 +03:00
Log.d(TAG, "Create Amnezia activity: $intent")
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
)
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
}
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
)
) {
Log.d(
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-01-20 16:40:12 +03:00
Log.d(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() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Stop Amnezia activity")
2023-11-23 20:30:03 +03:00
doUnbindService()
2024-06-18 20:46:21 +03:00
QtAndroidController.onServiceDisconnected()
2023-11-23 20:30:03 +03:00
super.onStop()
}
override fun onDestroy() {
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 ->
Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
if (isServiceConnected) {
if (proto == vpnProto) {
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-05-12 18:04:14 +03:00
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
Log.d(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
}
}
}
))
}
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 {
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-05-12 18:04:14 +03:00
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
2023-12-26 16:23:05 +03:00
2024-05-12 18:04:14 +03:00
else -> type = "*/*"
}
2023-12-26 16:23:05 +03:00
}
2024-05-12 18:04:14 +03:00
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
val uri = it?.data?.toString() ?: ""
Log.d(TAG, "Open file: $uri")
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
))
2023-12-26 16:23:05 +03:00
}
}
}
@Suppress("unused")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@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)
}
}
@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()
}
/**
* Utils methods
*/
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 = {}
)