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 37 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
88c410c
add focusController class
CyAn84 Sep 14, 2024
8f0a98c
add more key handlers
CyAn84 Sep 19, 2024
63a918b
add focus navigation to qml
CyAn84 Sep 19, 2024
c3b5814
fixed language selector
CyAn84 Sep 19, 2024
08ca365
add reverse focus change to FocusController
CyAn84 Sep 24, 2024
eec659b
add default focus item
CyAn84 Sep 29, 2024
9084ccb
update transitions
CyAn84 Sep 29, 2024
b2374a9
update pages
CyAn84 Sep 29, 2024
971ee3d
add ListViewFocusController
CyAn84 Oct 13, 2024
59fb3be
fix ListView navigation
CyAn84 Oct 17, 2024
ffcd1c9
update CardType for using with focus navigation
CyAn84 Oct 17, 2024
e23ecaa
remove useless key navigation
CyAn84 Oct 17, 2024
f83f931
remove useless slots, logs, Drawer open and close
CyAn84 Oct 19, 2024
e9e6ef9
fix reverse focus move on listView
CyAn84 Oct 19, 2024
a2be286
fix drawer radio buttons selection
CyAn84 Oct 19, 2024
5a94abe
fix drawer layout and focus move
CyAn84 Oct 20, 2024
5807b5f
fix PageSetupWizardProtocolSettings focus move
CyAn84 Oct 21, 2024
baa9654
fix back navigation on default focus item
CyAn84 Oct 21, 2024
bd46b69
fix crashes after ListView navigation
CyAn84 Oct 21, 2024
400d393
fix protocol settings focus move
CyAn84 Oct 21, 2024
6bc3941
fix focus on users on page share
CyAn84 Oct 22, 2024
b77e431
clean up page share
CyAn84 Oct 22, 2024
5ab5f42
fix server rename
CyAn84 Oct 24, 2024
3ab8995
fix page share default server selection
CyAn84 Oct 24, 2024
005e660
refactor about page for correct focus move
CyAn84 Oct 24, 2024
e38a1d9
fix focus move on list views with header and-or footer
CyAn84 Oct 25, 2024
ff521b2
minor fixes
CyAn84 Oct 25, 2024
39716d6
fix server list back button handler
CyAn84 Oct 26, 2024
25585f1
fix spawn signals on switch
CyAn84 Oct 27, 2024
687f564
fix share details drawer
CyAn84 Oct 28, 2024
842bc4a
Merge branch 'dev' into feature/android-tv
albexk Oct 29, 2024
06c80e0
Bump version
albexk Oct 29, 2024
346de2b
Add mandatory requirement for android.software.leanback.
albexk Oct 29, 2024
81ef931
Fix importing files on TVs
albexk Oct 31, 2024
815d9fa
fix: add separate method for reading files to fix file reading on And…
albexk Nov 11, 2024
e663fe9
Merge branch 'dev' into feature/android-tv
albexk Nov 11, 2024
6a043a7
fix: add a workaround to open files on Android TV due to lack of SAF
albexk Nov 13, 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 2070)

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
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>
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>
134 changes: 99 additions & 35 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,6 +22,8 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.MotionEvent
import android.view.WindowManager.LayoutParams
Expand All @@ -28,6 +32,7 @@ import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import java.io.FileNotFoundException
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.text.RegexOption.IGNORE_CASE
Expand Down Expand Up @@ -71,6 +76,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 +520,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,34 +547,43 @@ 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 = {
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog()
}
val uri = it?.data?.toString() ?: ""
Log.v(TAG, "Open file: $uri")
mainScope.launch {
Expand All @@ -573,8 +592,53 @@ class AmneziaActivity : QtActivity() {
}
}
))
} 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 = try {
Log.v(TAG, "Get fd for $fileName")
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1
} catch (e: FileNotFoundException) {
Log.e(TAG, "Failed to get fd: $e")
-1
}

@Suppress("unused")
fun closeFd() {
Log.v(TAG, "Close fd")
pfd?.close()
pfd = null
}

@Suppress("unused")
fun getFileName(uri: String): String {
contentResolver.query(Uri.parse(uri), arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
if (cursor.moveToFirst() && !cursor.isNull(0)) {
return cursor.getString(0)
}
}
return ""
}

@Suppress("unused")
Expand Down
41 changes: 41 additions & 0 deletions client/android/src/org/amnezia/vpn/TvFilePicker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.amnezia.vpn

import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import org.amnezia.vpn.util.Log

private const val TAG = "TvFilePicker"

class TvFilePicker : ComponentActivity() {

private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
setResult(RESULT_OK, Intent().apply { data = it })
finish()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.v(TAG, "onCreate")
getFile()
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.v(TAG, "onNewIntent")
getFile()
}

private fun getFile() {
try {
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch("*/*")
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
finish()
}
}
}
23 changes: 20 additions & 3 deletions client/platforms/android/android_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ QString AndroidController::openFile(const QString &filter)
QString fileName;
connect(this, &AndroidController::fileOpened, this,
[&fileName, &wait](const QString &uri) {
qDebug() << "Android event: file opened; uri:" << uri;
fileName = QQmlFile::urlToLocalFileOrQrc(uri);
qDebug() << "Android opened filename:" << fileName;
fileName = uri;
wait.quit();
},
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
Expand All @@ -175,6 +173,25 @@ QString AndroidController::openFile(const QString &filter)
return fileName;
}

int AndroidController::getFd(const QString &fileName)
{
return callActivityMethod<jint>("getFd", "(Ljava/lang/String;)I",
QJniObject::fromString(fileName).object<jstring>());
}

void AndroidController::closeFd()
{
callActivityMethod("closeFd", "()V");
}

QString AndroidController::getFileName(const QString &uri)
{
auto fileName = callActivityMethod<jstring, jstring>("getFileName", "(Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(uri).object<jstring>());
QJniEnvironment env;
return AndroidUtils::convertJString(env.jniEnv(), fileName.object<jstring>());
}

bool AndroidController::isCameraPresent()
{
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
Expand Down
3 changes: 3 additions & 0 deletions client/platforms/android/android_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class AndroidController : public QObject
void resetLastServer(int serverIndex);
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
int getFd(const QString &fileName);
void closeFd();
QString getFileName(const QString &uri);
bool isCameraPresent();
bool isOnTv();
void startQrReaderActivity();
Expand Down
Loading
Loading