mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 787089f4ae | |||
| ebffda68c2 | |||
| 8ce5237a5f | |||
| 129ae44edc | |||
| 16fc44f989 | |||
| ef909d3605 | |||
| b9ca3315c6 | |||
| e9ed5b59a4 | |||
| 047dbb2677 | |||
| e9efe32f9b | |||
| 2dd3531e78 | |||
| 129f79ca2c | |||
| 50769f231d | |||
| 2edd7de413 | |||
| f0da2b003f | |||
| 650c1c6ebb | |||
| 8dbded1624 | |||
| cebfcc846e |
@@ -17,6 +17,7 @@ jobs:
|
||||
QIF_VERSION: 4.7
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -98,6 +99,7 @@ jobs:
|
||||
BUILD_ARCH: 64
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -204,6 +206,7 @@ jobs:
|
||||
CXX: c++
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -318,6 +321,7 @@ jobs:
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -395,6 +399,7 @@ jobs:
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -477,6 +482,7 @@ jobs:
|
||||
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
@@ -541,10 +547,11 @@ jobs:
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-36
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.10.3
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
QIF_VERSION: 4.5
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.15.0)
|
||||
set(AMNEZIAVPN_VERSION 4.8.19.0)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,7 +12,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 2118)
|
||||
set(APP_ANDROID_VERSION_CODE 2129)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
+1
-1
Submodule client/3rd-prebuilt updated: 51bb4703a4...4680bd8fb4
@@ -25,6 +25,7 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
|
||||
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
||||
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
||||
add_definitions(-DFALLBACK_S3_ENDPOINT="$ENV{FALLBACK_S3_ENDPOINT}")
|
||||
|
||||
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
||||
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -766,7 +767,13 @@ class AmneziaActivity : QtActivity() {
|
||||
fun openFile(filter: String?) {
|
||||
Log.v(TAG, "Open file with filter: $filter")
|
||||
mainScope.launch {
|
||||
val intent = if (!isOnTv()) {
|
||||
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
|
||||
.firstOrNull { pkg ->
|
||||
try { packageManager.getPackageInfo(pkg, 0); true }
|
||||
catch (_: PackageManager.NameNotFoundException) { false }
|
||||
}
|
||||
|
||||
val intent = if (!isOnTv() && systemPickerPackage != null) {
|
||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||
val mime = MimeTypeMap.getSingleton()
|
||||
@@ -792,6 +799,7 @@ class AmneziaActivity : QtActivity() {
|
||||
else -> type = "*/*"
|
||||
}
|
||||
}
|
||||
`package` = systemPickerPackage
|
||||
}
|
||||
} else {
|
||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||
@@ -803,8 +811,11 @@ class AmneziaActivity : QtActivity() {
|
||||
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
||||
showNoFileBrowserAlertDialog()
|
||||
}
|
||||
val uri = it?.data?.apply {
|
||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val uri = it?.data?.let { u ->
|
||||
if (u.scheme == "content") {
|
||||
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
|
||||
}
|
||||
u
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
if (uri.isNotEmpty()) {
|
||||
@@ -844,7 +855,12 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
val uri = Uri.parse(fileName)
|
||||
pfd = if (uri.scheme == "file") {
|
||||
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} else {
|
||||
contentResolver.openFileDescriptor(uri, "r")
|
||||
}
|
||||
pfd?.fd ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get fd: $e")
|
||||
@@ -1064,13 +1080,11 @@ class AmneziaActivity : QtActivity() {
|
||||
@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? {
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import org.amnezia.vpn.util.Log
|
||||
import java.io.File
|
||||
|
||||
private const val TAG = "TvFilePicker"
|
||||
private const val READ_STORAGE_REQUEST_CODE = 1001
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
// SAF launcher for Android 10+ where File API is blocked by scoped storage
|
||||
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
}
|
||||
if (activitiesToResolveIntent.all {
|
||||
if (activities.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||
}) {
|
||||
@@ -32,38 +38,140 @@ class TvFilePicker : ComponentActivity() {
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) {
|
||||
}) { uri ->
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = it
|
||||
data = uri
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
|
||||
private val directoryStack = ArrayDeque<File>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.v(TAG, "onCreate")
|
||||
getFile()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
launchSaf()
|
||||
} else {
|
||||
checkPermissionAndBrowse()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent")
|
||||
getFile()
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
navigateBack()
|
||||
}
|
||||
|
||||
private fun getFile() {
|
||||
private fun launchSaf() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
safLauncher.launch(arrayOf("*/*"))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
Log.w(TAG, "No SAF activity found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get file: $e")
|
||||
Log.e(TAG, "SAF launch failed: $e")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissionAndBrowse() {
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_REQUEST_CODE)
|
||||
} else {
|
||||
showRootDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == READ_STORAGE_REQUEST_CODE &&
|
||||
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
showRootDirectory()
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRootDirectory() {
|
||||
@Suppress("DEPRECATION")
|
||||
val primaryExternal = Environment.getExternalStorageDirectory()
|
||||
val storageDir = File("/storage")
|
||||
// Pre-seed stack with /storage so Back from primary storage goes there (USB drives etc.)
|
||||
if (storageDir.exists() && storageDir.canonicalPath != primaryExternal.canonicalPath) {
|
||||
directoryStack.addLast(storageDir)
|
||||
}
|
||||
showDirectory(primaryExternal)
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
if (directoryStack.size > 1) {
|
||||
directoryStack.removeLast()
|
||||
val parent = directoryStack.removeLast()
|
||||
showDirectory(parent)
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDirectory(dir: File) {
|
||||
directoryStack.addLast(dir)
|
||||
Log.v(TAG, "Showing directory: ${dir.absolutePath}")
|
||||
|
||||
val entries = try {
|
||||
dir.listFiles()
|
||||
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
|
||||
?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to list directory: $e")
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val names = entries.map { if (it.isDirectory) "[${it.name}]" else it.name }.toTypedArray()
|
||||
|
||||
val builder = AlertDialog.Builder(this)
|
||||
.setTitle(dir.absolutePath)
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
builder.setMessage("No files available")
|
||||
} else {
|
||||
builder.setItems(names) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
val selected = entries[which]
|
||||
if (selected.isDirectory) {
|
||||
showDirectory(selected)
|
||||
} else {
|
||||
Log.v(TAG, "Selected file: ${selected.absolutePath}")
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
data = Uri.fromFile(selected)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
})
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (directoryStack.size > 1) {
|
||||
builder.setNegativeButton("↑ Back") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
navigateBack()
|
||||
}
|
||||
} else {
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
builder.setOnCancelListener {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace apiDefs
|
||||
AmneziaFreeV3,
|
||||
AmneziaPremiumV1,
|
||||
AmneziaPremiumV2,
|
||||
AmneziaTrialV2,
|
||||
SelfHosted,
|
||||
ExternalPremium,
|
||||
ExternalTrial
|
||||
@@ -57,6 +56,7 @@ namespace apiDefs
|
||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
||||
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
|
||||
constexpr QLatin1String subscriptionStatus("subscription_status");
|
||||
constexpr QLatin1String subscription("subscription");
|
||||
constexpr QLatin1String endDate("end_date");
|
||||
constexpr QLatin1String issuedConfigs("issued_configs");
|
||||
@@ -83,6 +83,7 @@ namespace apiDefs
|
||||
|
||||
constexpr QLatin1String serviceInfo("service_info");
|
||||
constexpr QLatin1String isAdVisible("is_ad_visible");
|
||||
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
|
||||
constexpr QLatin1String adHeader("ad_header");
|
||||
constexpr QLatin1String adDescription("ad_description");
|
||||
constexpr QLatin1String adEndpoint("ad_endpoint");
|
||||
|
||||
@@ -101,7 +101,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
};
|
||||
case apiDefs::ConfigSource::AmneziaGateway: {
|
||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||
constexpr QLatin1String serviceTrial("amnezia-trial");
|
||||
constexpr QLatin1String serviceFree("amnezia-free");
|
||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||
@@ -111,8 +110,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
|
||||
if (serviceType == servicePremium) {
|
||||
return apiDefs::ConfigType::AmneziaPremiumV2;
|
||||
} else if (serviceType == serviceTrial) {
|
||||
return apiDefs::ConfigType::AmneziaTrialV2;
|
||||
} else if (serviceType == serviceFree) {
|
||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||
} else if (serviceType == serviceExternalPremium) {
|
||||
@@ -198,8 +195,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
||||
apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
|
||||
apiDefs::ConfigType::ExternalTrial };
|
||||
apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalTrial };
|
||||
return premiumTypes.contains(getConfigType(serverConfigObject));
|
||||
}
|
||||
|
||||
@@ -244,8 +240,8 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
auto configType = apiUtils::getConfigType(serverConfigObject);
|
||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
|
||||
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
|
||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium
|
||||
&& configType != apiDefs::ConfigType::ExternalTrial) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace apiUtils
|
||||
|
||||
bool isSubscriptionExpired(const QString &subscriptionEndDate);
|
||||
|
||||
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 10);
|
||||
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30);
|
||||
|
||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "amnezia_application.h"
|
||||
#include "core/api/apiUtils.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include "settings.h"
|
||||
#include "utilities.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
@@ -49,15 +50,80 @@ namespace
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
|
||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||
|
||||
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
|
||||
{
|
||||
QStringList shuffled = proxyUrls;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
|
||||
}
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
{
|
||||
try {
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
}
|
||||
return true;
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
|
||||
{
|
||||
if (cachedProxyUrlsEncrypted.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cachedProxyUrlsDecrypted;
|
||||
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
|
||||
qCritical() << "error decrypting cached proxy urls payload";
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
|
||||
QStringList endpoints;
|
||||
endpoints.reserve(endpointsArray.size());
|
||||
for (const QJsonValue &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -284,25 +350,33 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList baseUrls;
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
|
||||
+ ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls)
|
||||
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
@@ -327,40 +401,48 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList baseUrls;
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
if (baseUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return {};
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(baseUrls.begin(), baseUrls.end(), generator);
|
||||
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
target.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
};
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (proxyStorageUrls.empty()) {
|
||||
qDebug() << "empty storage endpoint list";
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
@@ -375,26 +457,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray hashResult = hash.result().toHex();
|
||||
|
||||
QByteArray key = QByteArray::fromHex(hashResult.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
|
||||
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
|
||||
} else {
|
||||
responseBody = encryptedResponseBody;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
|
||||
continue;
|
||||
}
|
||||
@@ -405,6 +469,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
for (const auto &endpoint : endpointsArray) {
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
|
||||
|
||||
return endpoints;
|
||||
} else {
|
||||
auto replyError = reply->error();
|
||||
@@ -416,7 +482,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
@@ -554,15 +620,17 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete)
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete({});
|
||||
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||
|
||||
@@ -570,33 +638,17 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encrypted);
|
||||
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||
} else {
|
||||
responseBody = encrypted;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
|
||||
qCritical() << "error decrypting payload";
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -604,13 +656,9 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
endpoints.push_back(endpoint.toString());
|
||||
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
|
||||
|
||||
QStringList shuffled = endpoints;
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||
|
||||
onComplete(shuffled);
|
||||
onComplete(shuffledProxyUrls(endpoints));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -619,7 +667,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <QPair>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <memory>
|
||||
|
||||
#include "core/defs.h"
|
||||
|
||||
@@ -14,13 +17,16 @@
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
class Settings;
|
||||
|
||||
class GatewayController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
@@ -53,7 +59,7 @@ private:
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete);
|
||||
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
@@ -63,6 +69,7 @@ private:
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
|
||||
inline static QString m_proxyUrl;
|
||||
};
|
||||
|
||||
+32
-32
@@ -390,55 +390,55 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
|
||||
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
|
||||
|
||||
if (!obj.value("Jc").isNull()) {
|
||||
config.m_junkPacketCount = obj.value("Jc").toString();
|
||||
if (const auto jc = obj.value("Jc"); !jc.isUndefined()) {
|
||||
config.m_junkPacketCount = jc.toString();
|
||||
}
|
||||
if (!obj.value("Jmin").isNull()) {
|
||||
config.m_junkPacketMinSize = obj.value("Jmin").toString();
|
||||
if (const auto jmin = obj.value("Jmin"); !jmin.isUndefined()) {
|
||||
config.m_junkPacketMinSize = jmin.toString();
|
||||
}
|
||||
if (!obj.value("Jmax").isNull()) {
|
||||
config.m_junkPacketMaxSize = obj.value("Jmax").toString();
|
||||
if (const auto jmax = obj.value("Jmax"); !jmax.isUndefined()) {
|
||||
config.m_junkPacketMaxSize = jmax.toString();
|
||||
}
|
||||
if (!obj.value("S1").isNull()) {
|
||||
config.m_initPacketJunkSize = obj.value("S1").toString();
|
||||
if (const auto s1 = obj.value("S1"); !s1.isUndefined()) {
|
||||
config.m_initPacketJunkSize = s1.toString();
|
||||
}
|
||||
if (!obj.value("S2").isNull()) {
|
||||
config.m_responsePacketJunkSize = obj.value("S2").toString();
|
||||
if (const auto s2 = obj.value("S2"); !s2.isUndefined()) {
|
||||
config.m_responsePacketJunkSize = s2.toString();
|
||||
}
|
||||
if (!obj.value("S3").isNull()) {
|
||||
config.m_cookieReplyPacketJunkSize = obj.value("S3").toString();
|
||||
if (const auto s3 = obj.value("S3"); !s3.isUndefined()) {
|
||||
config.m_cookieReplyPacketJunkSize = s3.toString();
|
||||
}
|
||||
if (!obj.value("S4").isNull()) {
|
||||
config.m_transportPacketJunkSize = obj.value("S4").toString();
|
||||
if (const auto s4 = obj.value("S4"); !s4.isUndefined()) {
|
||||
config.m_transportPacketJunkSize = s4.toString();
|
||||
}
|
||||
|
||||
if (!obj.value("H1").isNull()) {
|
||||
config.m_initPacketMagicHeader = obj.value("H1").toString();
|
||||
if (const auto h1 = obj.value("H1"); !h1.isUndefined()) {
|
||||
config.m_initPacketMagicHeader = h1.toString();
|
||||
}
|
||||
if (!obj.value("H2").isNull()) {
|
||||
config.m_responsePacketMagicHeader = obj.value("H2").toString();
|
||||
if (const auto h2 = obj.value("H2"); !h2.isUndefined()) {
|
||||
config.m_responsePacketMagicHeader = h2.toString();
|
||||
}
|
||||
if (!obj.value("H3").isNull()) {
|
||||
config.m_underloadPacketMagicHeader = obj.value("H3").toString();
|
||||
if (const auto h3 = obj.value("H3"); !h3.isUndefined()) {
|
||||
config.m_underloadPacketMagicHeader = h3.toString();
|
||||
}
|
||||
if (!obj.value("H4").isNull()) {
|
||||
config.m_transportPacketMagicHeader = obj.value("H4").toString();
|
||||
if (const auto h4 = obj.value("H4"); !h4.isUndefined()) {
|
||||
config.m_transportPacketMagicHeader = h4.toString();
|
||||
}
|
||||
|
||||
if (!obj.value("I1").isNull()) {
|
||||
config.m_specialJunk["I1"] = obj.value("I1").toString();
|
||||
if (const auto i1 = obj.value("I1"); !i1.isUndefined()) {
|
||||
config.m_specialJunk["I1"] = i1.toString();
|
||||
}
|
||||
if (!obj.value("I2").isNull()) {
|
||||
config.m_specialJunk["I2"] = obj.value("I2").toString();
|
||||
if (const auto i2 = obj.value("I2"); !i2.isUndefined()) {
|
||||
config.m_specialJunk["I2"] = i2.toString();
|
||||
}
|
||||
if (!obj.value("I3").isNull()) {
|
||||
config.m_specialJunk["I3"] = obj.value("I3").toString();
|
||||
if (const auto i3 = obj.value("I3"); !i3.isUndefined()) {
|
||||
config.m_specialJunk["I3"] = i3.toString();
|
||||
}
|
||||
if (!obj.value("I4").isNull()) {
|
||||
config.m_specialJunk["I4"] = obj.value("I4").toString();
|
||||
if (const auto i4 = obj.value("I4"); !i4.isUndefined()) {
|
||||
config.m_specialJunk["I4"] = i4.toString();
|
||||
}
|
||||
if (!obj.value("I5").isNull()) {
|
||||
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
||||
if (const auto i5 = obj.value("I5"); !i5.isUndefined()) {
|
||||
config.m_specialJunk["I5"] = i5.toString();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -218,16 +218,13 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
||||
|
||||
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||
QString tunnelName;
|
||||
if (configuration.value(config_key::description).toString().isEmpty()) {
|
||||
if (serverDescription.isEmpty()) {
|
||||
tunnelName = ProtocolProps::protoToString(proto);
|
||||
} else {
|
||||
tunnelName = QString("%1 %2")
|
||||
.arg(configuration.value(config_key::hostName).toString())
|
||||
.arg(ProtocolProps::protoToString(proto));
|
||||
}
|
||||
else {
|
||||
tunnelName = QString("%1 (%2) %3")
|
||||
.arg(configuration.value(config_key::description).toString())
|
||||
.arg(configuration.value(config_key::hostName).toString())
|
||||
.arg(serverDescription)
|
||||
.arg(ProtocolProps::protoToString(proto));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace
|
||||
const char cloudFlareNs2[] = "1.0.0.1";
|
||||
|
||||
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
|
||||
constexpr char proxyUrlsKey[] = "Conf/proxyUrls/";
|
||||
}
|
||||
|
||||
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
|
||||
@@ -526,6 +527,24 @@ void Settings::toggleDevGatewayEnv(bool enabled)
|
||||
m_settings.setValue("Conf/devGatewayEnv", enabled);
|
||||
}
|
||||
|
||||
QByteArray Settings::readGatewayProxyUrls(const QString &cacheKey) const
|
||||
{
|
||||
if (cacheKey.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_settings.value(QString(proxyUrlsKey) + cacheKey).toByteArray();
|
||||
}
|
||||
|
||||
void Settings::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
|
||||
{
|
||||
if (cacheKey.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_settings.setValue(QString(proxyUrlsKey) + cacheKey, proxyUrlsEncrypted);
|
||||
}
|
||||
|
||||
bool Settings::isHomeAdLabelVisible()
|
||||
{
|
||||
return m_settings.value("Conf/homeAdLabelVisible", true).toBool();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
@@ -234,6 +235,8 @@ public:
|
||||
QString getGatewayEndpoint(bool isTestPurchase = false);
|
||||
bool isDevGatewayEnv(bool isTestPurchase = false);
|
||||
void toggleDevGatewayEnv(bool enabled);
|
||||
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
|
||||
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
|
||||
|
||||
bool isHomeAdLabelVisible();
|
||||
void disableHomeAdLabel();
|
||||
|
||||
@@ -1027,7 +1027,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
||||
#endif
|
||||
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
|
||||
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||
auto installationUuid = m_settings->getInstallationUuid(true);
|
||||
@@ -1273,6 +1273,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
|
||||
bool isTestPurchase)
|
||||
{
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ void ApiNewsController::fetchNews(bool showError)
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
||||
|
||||
|
||||
@@ -23,6 +23,19 @@ namespace
|
||||
}
|
||||
|
||||
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
|
||||
QString getSubscriptionStatusForRenewal(const QSharedPointer<ApiAccountInfoModel> &accountInfoModel)
|
||||
{
|
||||
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpired")).toBool()) {
|
||||
return QStringLiteral("expired");
|
||||
}
|
||||
|
||||
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpiringSoon")).toBool()) {
|
||||
return QStringLiteral("expire_soon");
|
||||
}
|
||||
|
||||
return QStringLiteral("active");
|
||||
}
|
||||
}
|
||||
|
||||
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
|
||||
@@ -58,7 +71,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
|
||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||
@@ -97,7 +110,7 @@ void ApiSettingsController::getRenewalLink()
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
|
||||
m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
m_settings->isStrictKillSwitchEnabled(), m_settings);
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||
@@ -105,6 +118,7 @@ void ApiSettingsController::getRenewalLink()
|
||||
apiPayload[configKey::authData] = authData;
|
||||
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
|
||||
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
|
||||
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(m_apiAccountInfoModel);
|
||||
|
||||
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
|
||||
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
|
||||
|
||||
@@ -217,6 +217,8 @@ bool ImportController::extractConfigFromData(QString data)
|
||||
|
||||
bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||
{
|
||||
m_configType = checkConfigFormat(QString::fromUtf8(data));
|
||||
|
||||
QJsonObject dataObj = QJsonDocument::fromJson(data).object();
|
||||
if (!dataObj.isEmpty()) {
|
||||
m_config = dataObj;
|
||||
@@ -226,10 +228,13 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||
QByteArray ba_uncompressed = qUncompress(data);
|
||||
if (!ba_uncompressed.isEmpty()) {
|
||||
m_config = QJsonDocument::fromJson(ba_uncompressed).object();
|
||||
if (m_config.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
m_configType = checkConfigFormat(QString::fromUtf8(ba_uncompressed));
|
||||
return true;
|
||||
}
|
||||
|
||||
m_configType = checkConfigFormat(data);
|
||||
if (m_configType == ConfigTypes::Invalid) {
|
||||
QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray baUncompressed = qUncompress(ba);
|
||||
@@ -240,6 +245,10 @@ bool ImportController::extractConfigFromQr(const QByteArray &data)
|
||||
|
||||
if (!ba.isEmpty()) {
|
||||
m_config = QJsonDocument::fromJson(ba).object();
|
||||
if (m_config.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
m_configType = checkConfigFormat(QString::fromUtf8(ba));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,14 +54,11 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case IsComponentVisibleRole: {
|
||||
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
case IsSubscriptionRenewalAvailableRole: {
|
||||
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
|
||||
return m_accountInfoData.isRenewalAvailable;
|
||||
}
|
||||
case HasExpiredWorkerRole: {
|
||||
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
|
||||
@@ -133,6 +130,7 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
|
||||
accountInfoData.isInAppPurchase = apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false);
|
||||
|
||||
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
|
||||
accountInfoData.isRenewalAvailable = accountInfoObject.value(apiDefs::key::isRenewalAvailable).toBool(false);
|
||||
|
||||
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
|
||||
accountInfoData.supportedProtocols.push_back(protocol.toString());
|
||||
|
||||
@@ -61,6 +61,7 @@ private:
|
||||
QString subscriptionDescription;
|
||||
|
||||
bool isInAppPurchase = false;
|
||||
bool isRenewalAvailable = false;
|
||||
};
|
||||
|
||||
AccountInfoData m_accountInfoData;
|
||||
|
||||
@@ -179,6 +179,9 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
case AdEndpointRole: {
|
||||
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
|
||||
}
|
||||
case IsRenewalAvailableRole: {
|
||||
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::isRenewalAvailable).toBool(false);
|
||||
}
|
||||
case IsSubscriptionExpiredRole: {
|
||||
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) {
|
||||
return false;
|
||||
@@ -473,6 +476,7 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
roles[AdHeaderRole] = "adHeader";
|
||||
roles[AdDescriptionRole] = "adDescription";
|
||||
roles[AdEndpointRole] = "adEndpoint";
|
||||
roles[IsRenewalAvailableRole] = "isRenewalAvailable";
|
||||
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
|
||||
@@ -51,6 +51,7 @@ public:
|
||||
AdHeaderRole,
|
||||
AdDescriptionRole,
|
||||
AdEndpointRole,
|
||||
IsRenewalAvailableRole,
|
||||
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
|
||||
@@ -12,11 +12,10 @@ import "../Controls2/TextTypes"
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
property bool isRenewalActionAvailable: false
|
||||
property bool isRenewalAvailable: false
|
||||
|
||||
onOpened: {
|
||||
isRenewalActionAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
|
||||
&& !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
isRenewalAvailable = ServersModel.getProcessedServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
}
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
@@ -44,13 +43,13 @@ DrawerType2 {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
text: qsTr("Amnezia Premium subscription has expired")
|
||||
text: ServersModel.getProcessedServerData("name") + qsTr(" subscription has expired")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
visible: root.isRenewalActionAvailable
|
||||
visible: root.isRenewalAvailable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
@@ -62,7 +61,7 @@ DrawerType2 {
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
visible: root.isRenewalActionAvailable
|
||||
visible: root.isRenewalAvailable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
@@ -96,8 +95,13 @@ DrawerType2 {
|
||||
text: qsTr("Support")
|
||||
|
||||
clickedFunc: function() {
|
||||
root.closeTriggered()
|
||||
PageController.goToPage(PageEnum.PageSettingsApiSupport)
|
||||
PageController.showBusyIndicator(true)
|
||||
let result = ApiSettingsController.getAccountInfo(false)
|
||||
PageController.showBusyIndicator(false)
|
||||
if (result) {
|
||||
root.closeTriggered()
|
||||
PageController.goToPage(PageEnum.PageSettingsApiSupport)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +482,7 @@ PageType {
|
||||
|
||||
headerText: qsTr("I5 - Special junk 5")
|
||||
textField.text: serverSpecialJunk5
|
||||
checkEmptyText: false
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== serverSpecialJunk5) {
|
||||
|
||||
@@ -17,6 +17,8 @@ import "../Controls2/TextTypes"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool isRestoringBackup: false
|
||||
|
||||
Connections {
|
||||
target: SettingsController
|
||||
|
||||
@@ -138,9 +140,14 @@ PageType {
|
||||
textColor: AmneziaStyle.color.paleGray
|
||||
borderWidth: 1
|
||||
|
||||
enabled: !root.isRestoringBackup
|
||||
|
||||
text: qsTr("Restore from backup")
|
||||
|
||||
clickedFunc: function() {
|
||||
if (root.isRestoringBackup) {
|
||||
return
|
||||
}
|
||||
var filePath = SystemController.getFileName(qsTr("Open backup file"),
|
||||
qsTr("Backup files (*.backup)"))
|
||||
if (filePath !== "") {
|
||||
@@ -152,6 +159,10 @@ PageType {
|
||||
}
|
||||
|
||||
function restoreBackup(filePath) {
|
||||
if (root.isRestoringBackup) {
|
||||
return
|
||||
}
|
||||
|
||||
var headerText = qsTr("Import settings from a backup file?")
|
||||
var descriptionText = qsTr("All current settings will be reset");
|
||||
var yesButtonText = qsTr("Continue")
|
||||
@@ -161,9 +172,13 @@ PageType {
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot restore backup settings during active connection"))
|
||||
} else {
|
||||
root.isRestoringBackup = true
|
||||
PageController.showBusyIndicator(true)
|
||||
SettingsController.restoreAppConfig(filePath)
|
||||
PageController.showBusyIndicator(false)
|
||||
Qt.callLater(function() {
|
||||
SettingsController.restoreAppConfig(filePath)
|
||||
PageController.showBusyIndicator(false)
|
||||
root.isRestoringBackup = false
|
||||
})
|
||||
}
|
||||
}
|
||||
var noButtonFunction = function() {
|
||||
|
||||
@@ -16,6 +16,8 @@ import "../Config"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool isRestoringBackup: false
|
||||
|
||||
Connections {
|
||||
target: ImportController
|
||||
|
||||
@@ -228,6 +230,8 @@ PageType {
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
leftImageSource: imageSource
|
||||
|
||||
enabled: !root.isRestoringBackup
|
||||
|
||||
onClicked: { handler() }
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
@@ -314,12 +318,19 @@ PageType {
|
||||
property string imageSource: "qrc:/images/controls/archive-restore.svg"
|
||||
property bool isVisible: PageController.isStartPageVisible()
|
||||
property var handler: function() {
|
||||
if (root.isRestoringBackup) {
|
||||
return
|
||||
}
|
||||
var filePath = SystemController.getFileName(qsTr("Open backup file"),
|
||||
qsTr("Backup files (*.backup)"))
|
||||
if (filePath !== "") {
|
||||
root.isRestoringBackup = true
|
||||
PageController.showBusyIndicator(true)
|
||||
SettingsController.restoreAppConfig(filePath)
|
||||
PageController.showBusyIndicator(false)
|
||||
Qt.callLater(function() {
|
||||
SettingsController.restoreAppConfig(filePath)
|
||||
PageController.showBusyIndicator(false)
|
||||
root.isRestoringBackup = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user