Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android TV #1248

Draft
wants to merge 56 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
02bbcd3
add focusController class
CyAn84 Sep 14, 2024
cecee37
add more key handlers
CyAn84 Sep 19, 2024
01e31b4
add focus navigation to qml
CyAn84 Sep 19, 2024
78a4caa
fixed language selector
CyAn84 Sep 19, 2024
f189f7b
add reverse focus change to FocusController
CyAn84 Sep 24, 2024
b4f4ec4
add default focus item
CyAn84 Sep 29, 2024
3c655d0
update transitions
CyAn84 Sep 29, 2024
75f189e
update pages
CyAn84 Sep 29, 2024
f3df9eb
add ListViewFocusController
CyAn84 Oct 13, 2024
89ac585
fix ListView navigation
CyAn84 Oct 17, 2024
852e90e
update CardType for using with focus navigation
CyAn84 Oct 17, 2024
0638514
remove useless key navigation
CyAn84 Oct 17, 2024
ada3f9a
remove useless slots, logs, Drawer open and close
CyAn84 Oct 19, 2024
626b9e1
fix reverse focus move on listView
CyAn84 Oct 19, 2024
2c9fa10
fix drawer radio buttons selection
CyAn84 Oct 19, 2024
9cfa4c1
fix drawer layout and focus move
CyAn84 Oct 20, 2024
c962211
fix PageSetupWizardProtocolSettings focus move
CyAn84 Oct 21, 2024
db5d289
fix back navigation on default focus item
CyAn84 Oct 21, 2024
dac45a9
fix crashes after ListView navigation
CyAn84 Oct 21, 2024
21755cb
fix protocol settings focus move
CyAn84 Oct 21, 2024
766e1c9
fix focus on users on page share
CyAn84 Oct 22, 2024
b6c59b0
clean up page share
CyAn84 Oct 22, 2024
c77e01f
fix server rename
CyAn84 Oct 24, 2024
7c3d08d
fix page share default server selection
CyAn84 Oct 24, 2024
42645a9
refactor about page for correct focus move
CyAn84 Oct 24, 2024
3d7209e
fix focus move on list views with header and-or footer
CyAn84 Oct 25, 2024
1633998
minor fixes
CyAn84 Oct 25, 2024
2e896ed
fix server list back button handler
CyAn84 Oct 26, 2024
e4d21dc
fix spawn signals on switch
CyAn84 Oct 27, 2024
88958c0
fix share details drawer
CyAn84 Oct 28, 2024
f020bdb
fix drawer open close usage
CyAn84 Oct 30, 2024
dc6c1cd
refactor listViewFocusController
CyAn84 Oct 30, 2024
5e9202f
refactor focusController to make the logic more
CyAn84 Oct 30, 2024
a92f706
fix focus on notification
CyAn84 Nov 1, 2024
45b8235
update config page for scrolling with tab
CyAn84 Nov 1, 2024
416421c
fix crash on return with esc key
CyAn84 Nov 1, 2024
ed6fc27
fix focus navigation in dynamic delegate of list view
CyAn84 Nov 1, 2024
942805c
fix focus move on qr code on share page
CyAn84 Nov 2, 2024
d956be9
refactor page logging settings for focus navigation
CyAn84 Nov 3, 2024
0620b45
update popup
CyAn84 Nov 3, 2024
940806a
Bump version
albexk Oct 29, 2024
65870ad
Add mandatory requirement for android.software.leanback.
albexk Oct 29, 2024
7df0503
Fix importing files on TVs
albexk Oct 31, 2024
a5abab8
fix: add separate method for reading files to fix file reading on And…
albexk Nov 11, 2024
341d6f5
fix(android): add CHANGE_NETWORK_STATE permission for all Android ver…
albexk Nov 3, 2024
53e766f
Fix connection check for AWG/WG
albexk Nov 1, 2024
4e1862a
chore: minor fixes (#1235)
Nethius Nov 6, 2024
d02a6df
fix: add a workaround to open files on Android TV due to lack of SAF
albexk Nov 13, 2024
51092e9
fix: change the banner format for TV
albexk Nov 18, 2024
392caca
refactor: make TvFilePicker activity more sustainable
albexk Nov 18, 2024
2987e03
fix: add the touch emulation method for Android TV
albexk Nov 24, 2024
3eeab2f
fix: null uri processing
albexk Nov 25, 2024
50bd364
Merge branch 'dev' into feature/android-tv
albexk Nov 25, 2024
c72d76a
Merge branch 'improve_navigation_cpp' into feature/android-tv
albexk Nov 25, 2024
09b1f32
fix: hide UI elements that use file saving
albexk Nov 25, 2024
c9205af
chore: bump version code
albexk Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)

set(PROJECT AmneziaVPN)

project(${PROJECT} VERSION 4.8.2.4
project(${PROJECT} VERSION 4.8.3.0
DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/"
)
Expand All @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")

set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2071)
set(APP_ANDROID_VERSION_CODE 2072)

if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
Expand Down
3 changes: 3 additions & 0 deletions client/amnezia_application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ void AmneziaApplication::initControllers()
m_pageController.reset(new PageController(m_serversModel, m_settings));
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());

m_focusController.reset(new FocusController(m_engine, this));
m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get());

m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel,
m_apiServicesModel, m_settings));
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
Expand Down
2 changes: 2 additions & 0 deletions client/amnezia_application.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "ui/controllers/exportController.h"
#include "ui/controllers/importController.h"
#include "ui/controllers/installController.h"
#include "ui/controllers/focusController.h"
#include "ui/controllers/pageController.h"
#include "ui/controllers/settingsController.h"
#include "ui/controllers/sitesController.h"
Expand Down Expand Up @@ -124,6 +125,7 @@ class AmneziaApplication : public AMNEZIA_BASE_CLASS
#endif

QScopedPointer<ConnectionController> m_connectionController;
QScopedPointer<FocusController> m_focusController;
QScopedPointer<PageController> m_pageController;
QScopedPointer<InstallController> m_installController;
QScopedPointer<ImportController> m_importController;
Expand Down
9 changes: 8 additions & 1 deletion client/android/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<!-- for TV -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />

<!-- The following comment will be replaced upon deployment with default features based on the dependencies
Expand Down Expand Up @@ -91,6 +91,13 @@
android:exported="false"
android:theme="@style/Translucent" />

<activity android:name=".TvFilePicker"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:exported="false"
android:theme="@style/Translucent" />

<activity
android:name=".ImportConfigActivity"
android:excludeFromRecents="true"
Expand Down
5 changes: 0 additions & 5 deletions client/android/res/mipmap-anydpi-v26/ic_banner.xml

This file was deleted.

Binary file added client/android/res/mipmap-hdpi/ic_banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/android/res/mipmap-mdpi/ic_banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
2 changes: 2 additions & 0 deletions client/android/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@
<string name="notificationSettingsDialogTitle">Настройки уведомлений</string>
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>

<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources>
4 changes: 0 additions & 4 deletions client/android/res/values/ic_banner_background.xml

This file was deleted.

2 changes: 2 additions & 0 deletions client/android/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@
<string name="notificationSettingsDialogTitle">Notification settings</string>
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string>

<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources>
206 changes: 170 additions & 36 deletions client/android/src/org/amnezia/vpn/AmneziaActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
Expand All @@ -12,6 +13,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
Expand All @@ -20,8 +22,13 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager.LayoutParams
import android.webkit.MimeTypeMap
import android.widget.Toast
Expand All @@ -30,6 +37,7 @@ import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
import kotlin.text.RegexOption.IGNORE_CASE
import AppListProvider
import kotlinx.coroutines.CompletableDeferred
Expand Down Expand Up @@ -71,6 +79,7 @@ class AmneziaActivity : QtActivity() {
private var isInBoundState = false
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var pfd: ParcelFileDescriptor? = null

private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
Expand Down Expand Up @@ -514,21 +523,25 @@ class AmneziaActivity : QtActivity() {
type = "text/*"
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
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) }
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
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
}
}
}
))
))
} catch (_: ActivityNotFoundException) {
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
}
}
}
}
Expand All @@ -537,46 +550,115 @@ class AmneziaActivity : QtActivity() {
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
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()
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()

in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}

else -> type = "*/*"
else -> type = "*/*"
}
}
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
} else {
Intent(this@AmneziaActivity, TvFilePicker::class.java)
}

try {
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
onAny = {
val uri = it?.data?.toString() ?: ""
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog()
}
val uri = it?.data?.apply {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}?.toString() ?: ""
Log.v(TAG, "Open file: $uri")
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
))
} catch (_: ActivityNotFoundException) {
showNoFileBrowserAlertDialog()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened("")
}
}
}
}

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")
return blockingCall {
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")
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
Expand Down Expand Up @@ -721,6 +803,50 @@ class AmneziaActivity : QtActivity() {
}
}

// 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
)

// 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
Expand Down Expand Up @@ -770,6 +896,7 @@ class AmneziaActivity : QtActivity() {
}

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.v(TAG, "dispatchTouch: $ev")
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
}
Expand All @@ -784,6 +911,13 @@ class AmneziaActivity : QtActivity() {
/**
* Utils methods
*/
private fun <T> blockingCall(
context: CoroutineContext = Dispatchers.Main.immediate,
block: suspend () -> T
) = runBlocking {
mainScope.async(context) { block() }.await()
}

companion object {
private fun actionCodeToString(actionCode: Int): String =
when (actionCode) {
Expand Down
Loading