Compare commits

..

21 Commits

Author SHA1 Message Date
lunardunno
707c6ecc26 check only sudoers 2026-05-31 20:00:54 +04:00
lunardunno
644cc0b743 sudo|docker and not found, in one line 2026-05-31 04:47:56 +04:00
lunardunno
59f461ba26 Changing phrases for check stdout check_user_in_sudo.sh‎ 2026-05-31 01:24:07 +04:00
lunardunno
8ea852a71b Changing the phrase for check stdout
"sudo:" with "not found" instead of "command not found"
2026-05-30 17:54:10 +04:00
lunardunno
38287dcd5b suppressing sudo password prompt install_docker.sh 2026-05-30 13:29:38 +04:00
lunardunno
b7c180d889 suppressing sudo password prompt 2026-05-29 23:34:03 +04:00
lunardunno
d503e0f4ee suppressing sudo password prompt 2026-05-28 23:18:42 +04:00
lunardunno
d82b9884c1 which LOCK_CMD with sudo
Run the "which" with sudo to check the $LOCK_CMD variable in case the user's PATH variable has incorrect values ​​if the user is not root and is only a member of the sudo group.
2026-05-28 23:18:42 +04:00
lunardunno
eb6b94cd42 "which" as main, "command" as backup for check user 2026-05-28 23:18:42 +04:00
lunardunno
e740001142 "which" as main, "command" as backup. 2026-05-28 23:18:42 +04:00
lunardunno
1378032670 Attempting to use "command -v"
Switching to using "command -v" instead of "which".
2026-05-28 23:18:42 +04:00
lunardunno
249c6cda29 processed phrases have been changed
The phrases processed by the server controller have been changed.
2026-05-28 19:39:10 +04:00
lunardunno
173ebc861e various changes in the script
The messages output for processing by the server controller have been changed: "Container runtime is not supported" and "Container runtime service is not running."
The redundant check and output of the "Packet manager not found" message, as well as the interruption of script execution, have been eliminated, as this situation is handled by the server controller at an earlier stage (check_server_is_busy.sh) and only there.
Added installation of the whish package if it is missing from the OS, for subsequent re-execution of the install_docker.sh and check_server_is_busy.sh scripts.
Implemented an alternative method for detecting the package manager if the whish package is initially missing from the OS.
The algorithm for setting the $pm variable (package manager) has been changed.
2026-05-28 18:50:03 +04:00
lunardunno
c1f606a18a Changing the names of errors 2026-05-28 17:07:20 +04:00
lunardunno
84c76d75da fix last line in errorStrings.cpp 2026-05-28 15:48:54 +04:00
lunardunno
9cdd4fe8d2 fix last line in errorCodes.h 2026-05-28 15:47:29 +04:00
lunardunno
b43e16d2ec Merge branch 'dev' into feat/implementing_docker_service_checking 2026-05-28 15:35:05 +04:00
lunardunno
d194e01fc0 Adding extended descriptions of new errors 2026-05-27 18:15:08 +04:00
lunardunno
7931317dec Error Codes added
Error Codes added for ServerContainerizationNotSupported & DockerServiceNotActive
2026-05-27 17:26:22 +04:00
lunardunno
66103dac5a adding message handling to install controller
Adding handling for "Containerization app is not supported" and "Service status not active" messages to the controller.
2026-05-27 17:19:42 +04:00
lunardunno
15669b3f98 Updating install_docker.sh script
Implementing a Docker service status check.
The Docker reinstall step has been removed due to the implementation of Docker service checking.
Implementing locale checking and assignment.
Implementation of execution of some actions through commands with sudo, to reduce delays caused by differences in the values ​​of the PATH variable for the root user and the user included in the sudo group.
Implementation of a verification step for the install containerization app to avoid installing unsupported podman-docker applications.
2026-05-27 16:06:54 +04:00
38 changed files with 111 additions and 1328 deletions

View File

@@ -854,41 +854,13 @@ jobs:
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
(cd deploy/build/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_universal.apk)
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}_oss.aab)
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}.aab)
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
(cd deploy/build/${abi}/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_${abi}.apk)
done
- name: 'Build Play AAB'
env:
QT_INSTALL_DIR: ${{ runner.temp }}
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
shell: bash
run: |
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
deploy/build.sh -t android --sign --aab --play --build ./deploy/build/play
(cd deploy/build/play/client/android-build/build/outputs/bundle/playRelease && mv *.aab AmneziaVPN_${VERSION}_play.aab)
- name: 'Build Play APK'
env:
QT_INSTALL_DIR: ${{ runner.temp }}
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
shell: bash
run: |
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
deploy/build.sh -t android --sign --apk --play --abi arm64-v8a --build ./deploy/build/play-apk
(cd deploy/build/play-apk/client/android-build/build/outputs/apk/play/release && mv *.apk AmneziaVPN_${VERSION}_play.apk)
- name: 'Upload universal APK'
uses: actions/upload-artifact@v7
with:
@@ -896,27 +868,13 @@ jobs:
archive: false
retention-days: 7
- name: 'Upload OSS AAB'
- name: 'Upload AAB'
uses: actions/upload-artifact@v7
with:
path: deploy/build/client/android-build/build/outputs/bundle/release/*.aab
archive: false
retention-days: 7
- name: 'Upload Play AAB'
uses: actions/upload-artifact@v7
with:
path: deploy/build/play/client/android-build/build/outputs/bundle/playRelease/*.aab
archive: false
retention-days: 7
- name: 'Upload Play APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/play-apk/client/android-build/build/outputs/apk/play/release/*.apk
archive: false
retention-days: 7
- name: 'Upload arm64-v8a APK'
uses: actions/upload-artifact@v7
with:

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.16.2)
set(AMNEZIAVPN_VERSION 4.9.0.1)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -28,7 +28,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 2123)
set(APP_ANDROID_VERSION_CODE 2122)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -25,10 +25,6 @@
#include "version.h"
#include "platforms/ios/QRCodeReaderBase.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
bool AmneziaApplication::m_forceQuit = false;
@@ -136,12 +132,6 @@ void AmneziaApplication::init()
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
#endif
#ifdef Q_OS_ANDROID
m_engine->rootContext()->setContextProperty("IsPlayBuild", AndroidController::instance()->isPlay());
#else
m_engine->rootContext()->setContextProperty("IsPlayBuild", false);
#endif
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
m_vpnConnectionThread.start();

View File

@@ -1,19 +0,0 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
id(libs.plugins.kotlin.android.get().pluginId)
}
kotlin {
jvmToolchain(17)
}
android {
namespace = "org.amnezia.vpn.billing"
}
dependencies {
compileOnly(project(":utils"))
implementation(libs.androidx.core)
implementation(libs.kotlinx.coroutines)
implementation(libs.android.billing)
}

View File

@@ -1,65 +0,0 @@
import com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
import com.android.billingclient.api.BillingClient.BillingResponseCode.DEVELOPER_ERROR
import com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR
import com.android.billingclient.api.BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_NOT_OWNED
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE
import com.android.billingclient.api.BillingClient.BillingResponseCode.NETWORK_ERROR
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_DISCONNECTED
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE
import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED
import com.android.billingclient.api.BillingResult
import org.amnezia.vpn.util.ErrorCode
internal class BillingException(
billingResult: BillingResult,
retryable: Boolean = false
) : Exception(billingResult.toString()) {
constructor(msg: String) : this(BillingResult.newBuilder()
.setResponseCode(DEVELOPER_ERROR)
.setDebugMessage(msg)
.build())
val errorCode: Int
val isCanceled = billingResult.responseCode == USER_CANCELED
val isRetryable = retryable || billingResult.responseCode in setOf(
NETWORK_ERROR,
SERVICE_DISCONNECTED,
SERVICE_UNAVAILABLE,
ERROR
)
init {
when (billingResult.responseCode) {
ERROR -> {
errorCode = ErrorCode.BillingGooglePlayError
}
BILLING_UNAVAILABLE, SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE -> {
errorCode = ErrorCode.BillingUnavailable
}
DEVELOPER_ERROR, FEATURE_NOT_SUPPORTED, ITEM_NOT_OWNED -> {
errorCode = ErrorCode.BillingError
}
ITEM_ALREADY_OWNED -> {
errorCode = ErrorCode.SubscriptionAlreadyOwned
}
ITEM_UNAVAILABLE -> {
errorCode = ErrorCode.SubscriptionUnavailable
}
NETWORK_ERROR -> {
errorCode = ErrorCode.BillingNetworkError
}
else -> {
errorCode = ErrorCode.BillingError
}
}
}
}

View File

@@ -1,320 +0,0 @@
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.ProductType
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.GetBillingConfigParams
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryProductDetailsParams.Product
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.acknowledgePurchase
import com.android.billingclient.api.queryProductDetails
import com.android.billingclient.api.queryPurchasesAsync
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import org.amnezia.vpn.util.ErrorCode
import org.amnezia.vpn.util.Log
import org.json.JSONArray
import org.json.JSONObject
private const val TAG = "BillingProvider"
private const val PRODUCT_ID = "premium"
class BillingProvider(context: Context) : AutoCloseable {
private var billingClient: BillingClient
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
Log.v(TAG, "Purchases updated: $billingResult")
subscriptionPurchases.value = billingResult to purchases
}
init {
billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListeners)
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
.build()
}
private suspend fun connect() {
if (billingClient.isReady) return
Log.v(TAG, "Billing client connection")
val connection = CompletableDeferred<Unit>()
withContext(Dispatchers.IO) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.v(TAG, "Billing setup finished: $billingResult")
if (billingResult.isOk) {
connection.complete(Unit)
} else {
Log.e(TAG, "Billing setup failed: $billingResult")
connection.completeExceptionally(BillingException(billingResult))
}
}
override fun onBillingServiceDisconnected() {
Log.w(TAG, "Billing service disconnected")
}
})
}
connection.await()
}
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject {
val numberAttempts = 3
var attemptCount = 0
while (true) {
try {
return block()
} catch (e: BillingException) {
if (e.isCanceled) {
Log.w(TAG, "Billing canceled")
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
} else if (e.isRetryable && attemptCount < numberAttempts) {
Log.d(TAG, "Retryable error: $e")
++attemptCount
delay(1000)
} else {
Log.e(TAG, "Billing error: $e")
return JSONObject().put("responseCode", e.errorCode)
}
} catch (_: CancellationException) {
Log.w(TAG, "Billing coroutine canceled")
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
}
}
}
suspend fun getSubscriptionPlans(): JSONObject {
Log.v(TAG, "Get subscription plans")
val productDetailsList = getProductDetails()
val resultJson = JSONObject().put("responseCode", ErrorCode.NoError)
val productArray = JSONArray().also { resultJson.put("products", it) }
productDetailsList?.forEach { productDetails ->
val product = JSONObject().also { productArray.put(it) }
.put("productId", productDetails.productId)
.put("name", productDetails.name)
val offers = JSONArray().also { product.put("offers", it) }
productDetails.subscriptionOfferDetails?.forEach { offerDetails ->
val offer = JSONObject().also { offers.put(it) }
.put("basePlanId", offerDetails.basePlanId)
.put("offerId", offerDetails.offerId)
.put("offerToken", offerDetails.offerToken)
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
offerDetails.pricingPhases.pricingPhaseList.forEach { phase ->
JSONObject().also { pricingPhases.put(it) }
.put("billingCycleCount", phase.billingCycleCount)
.put("billingPeriod", phase.billingPeriod)
.put("formatedPrice", phase.formattedPrice)
.put("recurrenceMode", phase.recurrenceMode)
}
}
}
return resultJson
}
private suspend fun getProductDetails(): List<ProductDetails>? {
Log.v(TAG, "Get product details")
val productDetailsParams = Product.newBuilder()
.setProductId(PRODUCT_ID)
.setProductType(ProductType.SUBS)
.build()
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(listOf(productDetailsParams))
.build()
val result = withContext(Dispatchers.IO) {
billingClient.queryProductDetails(queryProductDetailsParams)
}
Log.v(TAG, "Query product details result: ${result.billingResult}")
if (!result.billingResult.isOk) {
Log.e(TAG, "Failed to get product details: ${result.billingResult}")
throw BillingException(result.billingResult)
}
return result.productDetailsList
}
suspend fun getCustomerCountryCode(): JSONObject {
Log.v(TAG, "Get customer country code")
val deferred = CompletableDeferred<String>()
withContext(Dispatchers.IO) {
billingClient.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build(),
{ billingResult, billingConfig ->
Log.v(TAG, "Billing config: $billingResult, ${billingConfig?.countryCode}")
if (billingResult.isOk) {
deferred.complete(billingConfig?.countryCode ?: "")
} else {
deferred.completeExceptionally(BillingException(billingResult))
}
})
}
val countryCode = deferred.await()
return JSONObject()
.put("responseCode", ErrorCode.NoError)
.put("countryCode", countryCode)
}
suspend fun purchaseSubscription(
activity: Activity,
offerToken: String,
oldPurchaseToken: String? = null
): JSONObject {
Log.v(TAG, "Purchase subscription")
Log.v(TAG, "Offer token: $offerToken")
oldPurchaseToken?.let { Log.v(TAG, "Old purchase token: $it") }
if (offerToken.isBlank()) throw BillingException("offerToken can not be empty")
val productDetails = getProductDetails()?.let {
it.filter { it.productId == PRODUCT_ID }
}?.firstOrNull() ?: throw BillingException("Product details not found")
Log.v(TAG, "Filtered product details:\n$productDetails")
val productDetail = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
val subscriptionUpdateParams = oldPurchaseToken?.let {
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(oldPurchaseToken)
.setSubscriptionReplacementMode(ReplacementMode.WITHOUT_PRORATION)
.build()
}
val billingResult = billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetail))
.apply { subscriptionUpdateParams?.let { setSubscriptionUpdateParams(it) } }
.build())
Log.v(TAG, "Start billing flow result: $billingResult")
if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
Log.w(TAG, "Attempting to purchase already owned product")
val purchases = queryPurchases()
if (purchases.any { PRODUCT_ID in it.products }) throw BillingException(billingResult)
else throw BillingException(billingResult, retryable = true)
} else if (billingResult.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
Log.w(TAG, "Attempting to replace not owned product")
val purchases = queryPurchases()
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(billingResult)
else throw BillingException(billingResult, retryable = true)
} else if (!billingResult.isOk) throw BillingException(billingResult)
subscriptionPurchases.firstOrNull { it != null }?.let { (billingResult, purchases) ->
if (!billingResult.isOk) throw BillingException(billingResult)
return JSONObject()
.put("responseCode", ErrorCode.NoError)
.put("purchases", processPurchases(purchases))
} ?: throw BillingException("Purchase failed")
}
private fun processPurchases(purchases: List<Purchase>?): JSONArray {
val purchaseArray = JSONArray()
purchases?.forEach { purchase ->
/* val purchaseJson = */ JSONObject().also { purchaseArray.put(it) }
.put("purchaseToken", purchase.purchaseToken)
.put("purchaseTime", purchase.purchaseTime)
.put("purchaseState", purchase.purchaseState)
.put("isAcknowledged", purchase.isAcknowledged)
.put("isAutoRenewing", purchase.isAutoRenewing)
.put("orderId", purchase.orderId)
// .put("productIds", JSONArray(purchase.products))
/* purchase.pendingPurchaseUpdate?.let { purchaseUpdate ->
JSONObject()
.put("purchaseToken", purchaseUpdate.purchaseToken)
// .put("productIds", JSONArray(purchaseUpdate.products))
}.also { purchaseJson.put("pendingPurchaseUpdate", it) } */
}
return purchaseArray
}
suspend fun acknowledge(purchaseToken: String): JSONObject {
Log.v(TAG, "Acknowledge purchase: $purchaseToken")
val result = withContext(Dispatchers.IO) {
billingClient.acknowledgePurchase(
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build()
)
}
Log.v(TAG, "Acknowledge purchase result: $result")
if (result.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
Log.w(TAG, "Attempting to acknowledge not owned product")
val purchases = queryPurchases()
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(result)
else throw BillingException(result, retryable = true)
} else if (!result.isOk && result.responseCode != BillingResponseCode.ITEM_ALREADY_OWNED) {
throw BillingException(result)
}
return JSONObject().put("responseCode", ErrorCode.NoError)
}
suspend fun getPurchases(): JSONObject {
Log.v(TAG, "Get purchases")
val purchases = queryPurchases()
return JSONObject()
.put("responseCode", ErrorCode.NoError)
.put("purchases", processPurchases(purchases))
}
private suspend fun queryPurchases(): List<Purchase> {
Log.v(TAG, "Query purchases")
val result = withContext(Dispatchers.IO) {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
)
}
Log.v(TAG, "Query purchases result: ${result.billingResult}")
if (!result.billingResult.isOk) throw BillingException(result.billingResult)
return result.purchasesList
}
override fun close() {
Log.v(TAG, "Close billing client connection")
billingClient.endConnection()
}
companion object {
suspend fun withBillingProvider(context: Context, block: suspend BillingProvider.() -> JSONObject): String =
BillingProvider(context).use { bp ->
bp.handleBillingApiCall {
bp.connect()
bp.block()
}.toString()
}
}
}
internal val BillingResult.isOk: Boolean
get() = responseCode == BillingResponseCode.OK

View File

@@ -20,7 +20,6 @@ android {
namespace = "org.amnezia.vpn"
buildFeatures {
buildConfig = true
viewBinding = true
}
@@ -34,56 +33,13 @@ android {
jniLibs.useLegacyPackaging = true
}
val abiList = qtTargetAbiList.split(",")
defaultConfig {
applicationId = "org.amnezia.vpn"
targetSdk = qtTargetSdkVersion.toInt()
// keeps language resources for only the locales specified below
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
// ndk.abiFilters is only used for single-ABI builds; multi-ABI uses splits below
if (abiList.size == 1) {
ndk.abiFilters += abiList
}
}
signingConfigs {
register("release") {
storeFile = providers.environmentVariable("QT_ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
storePassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
keyAlias = providers.environmentVariable("QT_ANDROID_KEYSTORE_ALIAS").orNull
keyPassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
}
}
buildTypes {
release {
// exclude coroutine debug resource from release build
packaging {
resources.excludes += "DebugProbesKt.bin"
}
signingConfig = signingConfigs["release"]
}
create("fdroid") {
initWith(getByName("release"))
signingConfig = null
matchingFallbacks += "release"
}
}
flavorDimensions += "billing"
productFlavors {
create("oss") {
dimension = "billing"
buildConfigField("boolean", "IS_PLAY_BUILD", "false")
}
create("play") {
dimension = "billing"
buildConfigField("boolean", "IS_PLAY_BUILD", "true")
}
ndk.abiFilters += qtTargetAbiList.split(",")
}
sourceSets {
@@ -95,74 +51,13 @@ android {
assets.setSrcDirs(listOf("assets"))
jniLibs.setSrcDirs(listOf("libs"))
}
getByName("oss") {
java.setSrcDirs(listOf("oss"))
}
getByName("play") {
java.setSrcDirs(listOf("play"))
}
}
splits {
abi {
// splits only make sense for multi-ABI builds; single-ABI uses ndk.abiFilters
isEnable = abiList.size > 1
reset()
include(*abiList.toTypedArray())
isUniversalApk = false
}
}
// fix for Qt Creator to allow deploying the application to a device
// to enable this fix, add the line outputBaseName=android-build to local.properties
if (outputBaseName.isNotEmpty()) {
applicationVariants.all {
outputs.map { it as BaseVariantOutputImpl }
.forEach { output ->
if (output.outputFileName.endsWith(".apk")) {
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
}
}
}
}
// androiddeployqt expects:
// APK: build/outputs/apk/{base}-{buildType}[-unsigned].apk (no flavor subdir)
// AAB: build/outputs/bundle/{buildType}/{base}-{buildType}.aab (no flavor subdir)
// where {base} = outputBaseName (set by Qt Creator) or "android-build" (CI fallback).
// Release APK gets -unsigned suffix (Qt cmake signs it); debug does not.
// Copy only oss flavor to the flat output dir that androiddeployqt/Qt Creator expect.
// Play flavor is built via android_play_apk/android_play_aab cmake targets and uses
// its native Gradle output paths directly.
applicationVariants.all {
val flavorName = productFlavors.firstOrNull()?.name ?: ""
val buildTypeName = buildType.name
if (flavorName == "oss") {
val base = outputBaseName.ifEmpty { "android-build" }
val unsignedSuffix = if (buildTypeName == "release") "-unsigned" else ""
packageApplicationProvider.configure {
doLast {
val srcDir = layout.buildDirectory.dir("outputs/apk/oss/$buildTypeName").get().asFile
val dstDir = layout.buildDirectory.dir("outputs/apk").get().asFile
dstDir.mkdirs()
srcDir.listFiles()?.filter { it.name.endsWith(".apk") }?.forEach { apk ->
apk.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
}
}
}
tasks.named("bundle${name.replaceFirstChar { it.uppercase() }}") {
doLast {
val srcDir = layout.buildDirectory.dir("outputs/bundle/ossRelease").get().asFile
val dstDir = layout.buildDirectory.dir("outputs/bundle/$buildTypeName").get().asFile
dstDir.mkdirs()
srcDir.listFiles()?.filter { it.name.endsWith(".aab") }?.forEach { aab ->
aab.copyTo(File(dstDir, "$base-$buildTypeName.aab"), overwrite = true)
}
}
buildTypes {
release {
// exclude coroutine debug resource from release build
packaging {
resources.excludes += "DebugProbesKt.bin"
}
}
}
@@ -189,9 +84,4 @@ dependencies {
implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
implementation(libs.androidx.biometric)
playImplementation(project(":billing"))
}
fun DependencyHandler.playImplementation(dependency: Any): Dependency? =
add("playImplementation", dependency)

View File

@@ -1,7 +1,6 @@
[versions]
agp = "8.6.1"
kotlin = "1.9.24"
android-billing = "7.0.0"
androidx-core = "1.13.1"
androidx-activity = "1.9.1"
androidx-annotation = "1.8.2"
@@ -15,7 +14,6 @@ kotlinx-serialization = "1.6.3"
google-mlkit = "17.3.0"
[libraries]
android-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "android-billing" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }

View File

@@ -1,13 +0,0 @@
package org.amnezia.vpn
import android.app.Activity
import android.content.Context
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
override suspend fun getCountryCode(): String = ""
override suspend fun getSubscriptionPlans(): String = ""
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String = ""
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String = ""
override suspend fun acknowledge(purchaseToken: String): String = ""
override suspend fun queryPurchases(): String = ""
}

View File

@@ -1,34 +0,0 @@
package org.amnezia.vpn
import android.app.Activity
import android.content.Context
import BillingProvider.Companion.withBillingProvider
class BillingPaymentRepository(private val context: Context) : BillingRepository {
override suspend fun getCountryCode(): String = withBillingProvider(context) {
getCustomerCountryCode()
}
override suspend fun getSubscriptionPlans(): String = withBillingProvider(context) {
getSubscriptionPlans()
}
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String =
withBillingProvider(context) {
purchaseSubscription(activity, offerToken)
}
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String =
withBillingProvider(context) {
purchaseSubscription(activity, offerToken, oldPurchaseToken)
}
override suspend fun acknowledge(purchaseToken: String): String = withBillingProvider(context) {
acknowledge(purchaseToken)
}
override suspend fun queryPurchases(): String = withBillingProvider(context) {
getPurchases()
}
}

View File

@@ -30,7 +30,6 @@ rootProject.buildFileName = "build.gradle.kts"
include(":qt")
include(":utils")
include(":billing")
include(":protocolApi")
include(":wireguard")
include(":awg")

View File

@@ -55,6 +55,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
@@ -88,7 +89,6 @@ class AmneziaActivity : QtActivity() {
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var pfd: ParcelFileDescriptor? = null
private lateinit var billingRepository: BillingRepository
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
@@ -205,7 +205,6 @@ class AmneziaActivity : QtActivity() {
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
billingRepository = BillingPaymentRepository(applicationContext)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -984,9 +983,15 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun getAppList(): String {
Log.v(TAG, "Get app list")
return blockingCall(Dispatchers.IO) {
AppListProvider.getAppList(packageManager, packageName)
var appList = ""
runBlocking {
mainScope.launch {
withContext(Dispatchers.IO) {
appList = AppListProvider.getAppList(packageManager, packageName)
}
}.join()
}
return appList
}
@Suppress("unused")
@@ -1056,10 +1061,12 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y")
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))
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))
}
}
}
@@ -1155,59 +1162,11 @@ class AmneziaActivity : QtActivity() {
return super.dispatchTrackballEvent(ev)
}
@Suppress("unused")
fun isPlay(): Boolean = BuildConfig.FLAVOR == "play"
@Suppress("unused")
fun isTestPurchaseEnvironment(): Boolean {
if (BuildConfig.DEBUG) return true
val appInfo = packageManager.getApplicationInfo(packageName, 0)
return (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
}
@Suppress("unused")
fun getCountryCode(): String {
Log.v(TAG, "Get country code")
return blockingCall { billingRepository.getCountryCode() }
}
@Suppress("unused")
fun getSubscriptionPlans(): String {
Log.v(TAG, "Get subscription plans")
return blockingCall { billingRepository.getSubscriptionPlans() }
}
@Suppress("unused")
fun purchaseSubscription(offerToken: String): String {
Log.v(TAG, "Purchase subscription")
return blockingCall { billingRepository.purchaseSubscription(this@AmneziaActivity, offerToken) }
}
@Suppress("unused")
fun upgradeSubscription(offerToken: String, oldPurchaseToken: String): String {
Log.v(TAG, "Upgrade subscription")
return blockingCall {
billingRepository.upgradeSubscription(this@AmneziaActivity, offerToken, oldPurchaseToken)
}
}
@Suppress("unused")
fun acknowledgePurchase(purchaseToken: String): String {
Log.v(TAG, "Acknowledge purchase")
return blockingCall { billingRepository.acknowledge(purchaseToken) }
}
@Suppress("unused")
fun queryPurchases(): String {
Log.v(TAG, "Query purchases")
return blockingCall { billingRepository.queryPurchases() }
}
/**
* Utils methods
*/
private fun <T> blockingCall(
context: CoroutineContext = Dispatchers.Default,
context: CoroutineContext = Dispatchers.Main.immediate,
block: suspend () -> T
) = runBlocking {
mainScope.async(context) { block() }.await()

View File

@@ -1,6 +1,5 @@
package org.amnezia.vpn
import android.system.Os
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
@@ -13,9 +12,6 @@ private const val TAG = "AmneziaApplication"
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
override fun onCreate() {
if (BuildConfig.DEBUG) {
Os.setenv("QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS", "0", true)
}
super.onCreate()
Prefs.init(this)
Log.init(this)

View File

@@ -1,12 +0,0 @@
package org.amnezia.vpn
import android.app.Activity
interface BillingRepository {
suspend fun getCountryCode(): String
suspend fun getSubscriptionPlans(): String
suspend fun purchaseSubscription(activity: Activity, offerToken: String): String
suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String
suspend fun acknowledge(purchaseToken: String): String
suspend fun queryPurchases(): String
}

View File

@@ -1,14 +0,0 @@
package org.amnezia.vpn.util
// keep synchronized with client/core/defs.h error_code_ns::ErrorCode
object ErrorCode {
const val NoError = 0
const val BillingCanceled = 1300
const val BillingError = 1301
const val BillingGooglePlayError = 1302
const val BillingUnavailable = 1303
const val SubscriptionAlreadyOwned = 1304
const val SubscriptionUnavailable = 1305
const val BillingNetworkError = 1306
}

View File

@@ -1,9 +1,5 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
# Option to build Play variant (with Google Play Billing) instead of OSS
# When ON, adds target android_play_apk: cmake --build . --target android_play_apk
option(ANDROID_BUILD_PLAY "Add android_play_apk target for Google Play Billing build" OFF)
set(APP_ANDROID_MIN_SDK 28)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -57,22 +53,3 @@ file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/androi
find_package(openvpn-pt-android REQUIRED)
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
if(ANDROID_BUILD_PLAY)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(_gradle_suffix "Debug")
else()
set(_gradle_suffix "Release")
endif()
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build")
add_custom_target(android_play_apk
COMMAND ./gradlew assemblePlay${_gradle_suffix} WORKING_DIRECTORY "${_android_build_dir}"
COMMENT "Building Android Play APK (assemblePlay${_gradle_suffix})"
DEPENDS ${PROJECT}
)
add_custom_target(android_play_aab
COMMAND ./gradlew bundlePlay${_gradle_suffix} WORKING_DIRECTORY "${_android_build_dir}"
COMMENT "Building Android Play AAB (bundlePlay${_gradle_suffix})"
DEPENDS ${PROJECT}
)
endif()

View File

@@ -54,6 +54,7 @@ target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"

View File

@@ -33,9 +33,6 @@
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h>
#elif defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#include <QtConcurrent>
#endif
using namespace amnezia;
@@ -330,7 +327,7 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
int *duplicateServerIndex)
@@ -789,7 +786,7 @@ ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCou
bool isTestPurchase = IosController::Instance()->isTestFlight();
ProtocolData protocolData = generateProtocolData(serviceProtocol);
return importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
return importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
originalTransactionId, isTestPurchase, duplicateServerIndex);
#else
Q_UNUSED(userCountryCode);
@@ -800,117 +797,6 @@ ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCou
#endif
}
ErrorCode SubscriptionController::processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &productId,
int *duplicateServerIndex)
{
#if defined(Q_OS_ANDROID)
auto androidController = AndroidController::instance();
QString purchaseToken;
bool purchaseOk = false;
QFutureWatcher<QPair<bool, QString>> watcher;
QEventLoop waitLoop;
QObject::connect(&watcher, &QFutureWatcher<QPair<bool, QString>>::finished, &waitLoop, &QEventLoop::quit);
QFuture<QPair<bool, QString>> future = QtConcurrent::run([androidController, productId]() {
QJsonObject plansResult = androidController->getSubscriptionPlans();
int responseCode = plansResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Failed to get subscription plans, responseCode:" << responseCode;
return qMakePair(false, QString());
}
QJsonArray products = plansResult.value("products").toArray();
QString offerToken;
for (const QJsonValue &productValue : products) {
QJsonObject product = productValue.toObject();
if (product.value("productId").toString() == productId) {
QJsonArray offers = product.value("offers").toArray();
if (!offers.isEmpty()) {
offerToken = offers.at(0).toObject().value("offerToken").toString();
qInfo() << "[Billing] Found offer token for product:" << productId;
break;
}
}
}
if (offerToken.isEmpty()) {
qWarning() << "[Billing] No offer token found for product:" << productId;
return qMakePair(false, QString());
}
QJsonObject purchaseResult = androidController->purchaseSubscription(offerToken);
responseCode = purchaseResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Purchase failed, responseCode:" << responseCode;
return qMakePair(false, QString());
}
QJsonArray purchases = purchaseResult.value("purchases").toArray();
if (purchases.isEmpty()) {
qWarning() << "[Billing] Purchase succeeded but no purchases returned";
return qMakePair(false, QString());
}
QJsonObject purchase = purchases.at(0).toObject();
QString token = purchase.value("purchaseToken").toString();
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
qInfo() << "[Billing] Purchase success. purchaseToken:" << token << "isAcknowledged:" << isAcknowledged;
if (!isAcknowledged) {
QJsonObject ackResult = androidController->acknowledgePurchase(token);
if (ackResult.value("responseCode").toInt(-1) != 0) {
qWarning() << "[Billing] Acknowledge failed";
} else {
qInfo() << "[Billing] Purchase acknowledged successfully";
}
}
return qMakePair(true, token);
});
watcher.setFuture(future);
waitLoop.exec();
purchaseOk = watcher.result().first;
purchaseToken = watcher.result().second;
if (!purchaseOk || purchaseToken.isEmpty()) {
return ErrorCode::ApiPurchaseError;
}
// First call: determine if this is a test purchase
GatewayRequestData checkRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject checkPayload = checkRequestData.toJsonObject();
checkPayload[apiDefs::key::transactionId] = purchaseToken;
QByteArray checkResponse;
ErrorCode checkError = executeRequest(QString("%1v1/subscriptions"), checkPayload, checkResponse, false);
if (checkError != ErrorCode::NoError) {
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
return checkError;
}
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
// Second call: import service with correct isTestPurchase flag
ProtocolData protocolData = generateProtocolData(serviceProtocol);
return importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
purchaseToken, isTestPurchase, duplicateServerIndex);
#else
Q_UNUSED(userCountryCode);
Q_UNUSED(serviceType);
Q_UNUSED(serviceProtocol);
Q_UNUSED(productId);
return ErrorCode::ApiPurchaseError;
#endif
}
SubscriptionController::AppStoreRestoreResult SubscriptionController::processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol)
{
@@ -966,7 +852,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
ProtocolData protocolData = generateProtocolData(serviceProtocol);
int currentDuplicateServerIndex = -1;
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
ErrorCode errorCode = importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
originalTransactionId, isTestPurchase,
&currentDuplicateServerIndex);
@@ -999,146 +885,14 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
#endif
}
SubscriptionController::PlayMarketRestoreResult SubscriptionController::processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol)
{
PlayMarketRestoreResult result;
#if defined(Q_OS_ANDROID)
auto androidController = AndroidController::instance();
QJsonObject purchasesResult;
{
QFutureWatcher<QJsonObject> queryWatcher;
QEventLoop queryLoop;
QObject::connect(&queryWatcher, &QFutureWatcher<QJsonObject>::finished, &queryLoop, &QEventLoop::quit);
QFuture<QJsonObject> queryFuture = QtConcurrent::run([androidController]() {
return androidController->queryPurchases();
});
queryWatcher.setFuture(queryFuture);
queryLoop.exec();
purchasesResult = queryWatcher.result();
}
int responseCode = purchasesResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning().noquote() << "[Billing] queryPurchases failed, responseCode =" << responseCode;
result.errorCode = ErrorCode::ApiPurchaseError;
return result;
}
QJsonArray purchases = purchasesResult.value("purchases").toArray();
if (purchases.isEmpty()) {
qInfo().noquote() << "[Billing] Restore completed, but no purchases were found";
result.errorCode = ErrorCode::ApiNoPurchasesToRestore;
return result;
}
QSet<QString> processedTokens;
for (const QJsonValue &purchaseValue : std::as_const(purchases)) {
const QJsonObject purchaseObj = purchaseValue.toObject();
const QString purchaseToken = purchaseObj.value("purchaseToken").toString();
if (purchaseToken.isEmpty()) {
qWarning().noquote() << "[Billing] Skipping purchase without purchaseToken";
continue;
}
if (processedTokens.contains(purchaseToken)) {
result.duplicateCount++;
continue;
}
processedTokens.insert(purchaseToken);
qInfo().noquote() << "[Billing] Restoring subscription with purchaseToken =" << purchaseToken;
{
QFutureWatcher<QJsonObject> ackWatcher;
QEventLoop ackLoop;
QObject::connect(&ackWatcher, &QFutureWatcher<QJsonObject>::finished, &ackLoop, &QEventLoop::quit);
QFuture<QJsonObject> ackFuture = QtConcurrent::run([androidController, purchaseToken]() {
return androidController->acknowledgePurchase(purchaseToken);
});
ackWatcher.setFuture(ackFuture);
ackLoop.exec();
QJsonObject ackResult = ackWatcher.result();
int ackCode = ackResult.value("responseCode").toInt(-1);
if (ackCode != 0) {
qWarning().noquote() << "[Billing] acknowledgePurchase failed, responseCode =" << ackCode;
} else {
qInfo().noquote() << "[Billing] Purchase acknowledged successfully";
}
}
GatewayRequestData checkRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject checkPayload = checkRequestData.toJsonObject();
checkPayload[apiDefs::key::transactionId] = purchaseToken;
QByteArray checkResponse;
ErrorCode checkError = executeRequest(QString("%1v1/subscriptions"), checkPayload, checkResponse, false);
if (checkError != ErrorCode::NoError) {
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
result.errorCode = checkError;
continue;
}
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
ProtocolData protocolData = generateProtocolData(serviceProtocol);
int currentDuplicateServerIndex = -1;
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
purchaseToken, isTestPurchase,
&currentDuplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
result.duplicateConfigAlreadyPresent = true;
if (result.duplicateServerIndex < 0) {
result.duplicateServerIndex = currentDuplicateServerIndex;
}
qInfo().noquote() << "[Billing] Skipping purchase" << purchaseToken
<< "because subscription config with the same vpn_key already exists";
} else if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[Billing] Failed to process restored subscription for purchaseToken =" << purchaseToken;
result.errorCode = errorCode;
} else {
result.hasInstalledConfig = true;
}
}
if (!result.hasInstalledConfig) {
result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiNoPurchasesToRestore;
}
return result;
#else
Q_UNUSED(userCountryCode);
Q_UNUSED(serviceType);
Q_UNUSED(serviceProtocol);
result.errorCode = ErrorCode::ApiPurchaseError;
return result;
#endif
}
ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonObject &accountInfo)
{
auto apiV2Opt = m_serversRepository->apiV2Config(serverId);
if (!apiV2Opt.has_value()) {
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
const ApiV2ServerConfig* apiV2 = &apiV2Opt.value();
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),

View File

@@ -61,7 +61,7 @@ public:
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email);
ErrorCode importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
int *duplicateServerIndex = nullptr);
@@ -99,23 +99,10 @@ public:
ErrorCode errorCode = ErrorCode::NoError;
};
struct PlayMarketRestoreResult
{
bool hasInstalledConfig = false;
bool duplicateConfigAlreadyPresent = false;
int duplicateCount = 0;
int duplicateServerIndex = -1;
ErrorCode errorCode = ErrorCode::NoError;
};
ErrorCode processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &productId,
int *duplicateServerIndex = nullptr);
ErrorCode processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &productId,
int *duplicateServerIndex = nullptr);
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol);
@@ -124,9 +111,6 @@ public:
const QString &captchaId, const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut = nullptr);
PlayMarketRestoreResult processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol);
private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool isApiKeyExpired(const QString &serverId) const;

View File

@@ -795,8 +795,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
@@ -809,8 +809,19 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))
if (stdOut.contains("Container runtime is not supported"))
return ErrorCode::ServerContainerRuntimeNotSupported;
QRegularExpression notFoundRegex(
R"(^.*(?:sudo:|docker:).*not found.*$)",
QRegularExpression::MultilineOption);
if (notFoundRegex.match(stdOut).hasMatch()) {
return ErrorCode::ServerDockerFailedError;
}
if (stdOut.contains("Container runtime service not running"))
return ErrorCode::ContainerRuntimeServiceNotRunning;
return error;
}
@@ -847,7 +858,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired;

View File

@@ -38,6 +38,8 @@ namespace amnezia
XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217,
ServerContainerRuntimeNotSupported = 218,
ContainerRuntimeServiceNotRunning = 219,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -105,7 +107,6 @@ namespace amnezia
ApiCaptchaInvalidError = 1118,
ApiCaptchaRefreshError = 1119,
ApiRateLimitError = 1120,
ApiNoPurchasesToRestore = 1121,
// QFile errors
OpenError = 1200,
@@ -113,16 +114,7 @@ namespace amnezia
PermissionsError = 1202,
UnspecifiedError = 1203,
FatalError = 1204,
AbortError = 1205,
// Billing errors
BillingCanceled = 1300,
BillingError = 1301,
BillingGooglePlayError = 1302,
BillingUnavailable = 1303,
SubscriptionAlreadyOwned = 1304,
SubscriptionUnavailable = 1305,
BillingNetworkError = 1306,
AbortError = 1205
};
Q_ENUM_NS(ErrorCode)
}
@@ -133,5 +125,3 @@ namespace amnezia
Q_DECLARE_METATYPE(amnezia::ErrorCode)
#endif // ERRORCODES_H

View File

@@ -39,6 +39,8 @@ QString errorString(ErrorCode code) {
case(ErrorCode::XrayRealityKeysReadFailed):
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
break;
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -97,15 +99,6 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
case (ErrorCode::ApiNoPurchasesToRestore):
#if defined(Q_OS_ANDROID)
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Google account used for the purchase.");
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Apple ID used for the purchase.");
#else
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same account used for the purchase.");
#endif
break;
// QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
@@ -115,15 +108,6 @@ QString errorString(ErrorCode code) {
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
// Billing errors
case(ErrorCode::BillingCanceled): errorMessage = QObject::tr("Transaction was canceled by the user"); break;
case(ErrorCode::BillingError): errorMessage = QObject::tr("Billing error"); break;
case(ErrorCode::BillingGooglePlayError): errorMessage = QObject::tr("Internal Google Play error, please try again later"); break;
case(ErrorCode::BillingUnavailable): errorMessage = QObject::tr("Billing is unavailable, please try again later"); break;
case(ErrorCode::SubscriptionAlreadyOwned): errorMessage = QObject::tr("You already own this subscription"); break;
case(ErrorCode::SubscriptionUnavailable): errorMessage = QObject::tr("The requested subscription is not available for purchase"); break;
case(ErrorCode::BillingNetworkError): errorMessage = QObject::tr("A network error occurred during the operation, please check the Internet connection"); break;
case(ErrorCode::InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;

View File

@@ -170,7 +170,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes
// - It can be empty, if so, if the key is not in the JSON, or the value is empty, report an error.
// - Else if it contains one thing. if the key is not in the JSON, or the value is empty, use that one.
// - Else if it contains many things, when the key IS in the JSON but not within the THINGS, use the first in the THINGS
// - Else -------------------------------------------- use the JSON value
// - Else -------------------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> use the JSON value
//
#define __vmess_checker__func(key, values) \
{ \

View File

@@ -26,8 +26,6 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
)
if(DEPLOY)
@@ -116,20 +114,10 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
find_package(openvpnadapter REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
find_package(awg-apple REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
find_package(hev-socks5-tunnel REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)

View File

@@ -328,57 +328,6 @@ void AndroidController::sendTouch(float x, float y)
callActivityMethod("sendTouch", "(FF)V", x, y);
}
bool AndroidController::isPlay()
{
return callActivityMethod<jboolean>("isPlay", "()Z");
}
bool AndroidController::isTestPurchaseEnvironment()
{
return callActivityMethod<jboolean>("isTestPurchaseEnvironment", "()Z");
}
QJsonObject AndroidController::getSubscriptionPlans()
{
QJniObject subscriptionPlans = callActivityMethod<jstring>("getSubscriptionPlans", "()Ljava/lang/String;");
QJsonObject json = QJsonDocument::fromJson(subscriptionPlans.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::purchaseSubscription(const QString &offerToken)
{
QJniObject result = callActivityMethod<jstring, jstring>("purchaseSubscription", "(Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(offerToken).object<jstring>());
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken)
{
QJniObject result = callActivityMethod<jstring, jstring, jstring>("upgradeSubscription",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(offerToken).object<jstring>(),
QJniObject::fromString(oldPurchaseToken).object<jstring>());
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::acknowledgePurchase(const QString &purchaseToken)
{
QJniObject result = callActivityMethod<jstring, jstring>("acknowledgePurchase", "(Ljava/lang/String;)Ljava/lang/String;",
QJniObject::fromString(purchaseToken).object<jstring>());
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
QJsonObject AndroidController::queryPurchases()
{
QJniObject result = callActivityMethod<jstring>("queryPurchases", "()Ljava/lang/String;");
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
return json;
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;

View File

@@ -55,13 +55,6 @@ public:
void requestNotificationPermission();
bool requestAuthentication();
void sendTouch(float x, float y);
bool isPlay();
bool isTestPurchaseEnvironment();
QJsonObject getSubscriptionPlans();
QJsonObject purchaseSubscription(const QString &offerToken);
QJsonObject upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken);
QJsonObject acknowledgePurchase(const QString &purchaseToken);
QJsonObject queryPurchases();
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);

View File

@@ -1,7 +1,8 @@
if which apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
elif which dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
elif which yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
elif which zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
elif which pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
else echo "Packet manager not found"; echo "Internal error"; exit 1; fi;\
if command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
if which apt-get > /dev/null 2>&1 || command -v apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
elif which dnf > /dev/null 2>&1 || command -v dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
elif which yum > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
elif which zypper > /dev/null 2>&1 || command -v zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
elif which pacman > /dev/null 2>&1 || command -v pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
else echo "Packet manager not found"; echo "Internal error"; exit 1;\
fi;\
if sudo -n which $LOCK_CMD > /dev/null 2>&1 || command -v $LOCK_CMD > /dev/null 2>&1; then sudo -n $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi

View File

@@ -1,8 +1,8 @@
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); opt="--version";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); opt="--version";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then opt="--version";\
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then opt="--version";\
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then opt="--version";\
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then opt="--version";\
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then opt="--version";\
else pm="uname"; opt="-a";\
fi;\
CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\

View File

@@ -1,25 +1,34 @@
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="opensuse";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
else echo "Packet manager not found"; exit 1; fi;\
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg";\
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then silent_inst="-yq install --install-recommends"; what_pkg="-s install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then silent_inst="-yq install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then silent_inst="-y -q install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then silent_inst="-nq install"; what_pkg="--dry-run install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="suse";\
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then silent_inst="-S --noconfirm --noprogressbar --quiet"; what_pkg="-Sp"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
fi;\
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, What pkg command: $what_pkg, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg, Language: $LANG";\
echo $LANG | grep -qE '^(en_US.UTF-8|C.UTF-8|C)$' || export LC_ALL=C;\
if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\
if ! command -v sudo > /dev/null 2>&1; then $pm $check_pkgs; $pm $silent_inst sudo; fi;\
if ! command -v fuser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst psmisc; fi;\
if ! command -v lsof > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst lsof; fi;\
if ! command -v docker > /dev/null 2>&1; then \
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\
sleep 5; sudo systemctl enable --now docker; sleep 5;\
if ! sudo -n sh -c 'command -v which > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst which; fi;\
if ! sudo -n sh -c 'command -v fuser > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst psmisc; fi;\
if ! sudo -n sh -c 'command -v lsof > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst lsof; fi;\
if ! sudo -n sh -c 'command -v docker > /dev/null 2>&1'; then \
sudo -n $pm $check_pkgs;\
if ! sudo -n $pm $what_pkg $docker_pkg 2>/dev/null | grep -qi podman; then \
sudo -n $pm $silent_inst $docker_pkg;\
sleep 5; sudo -n systemctl enable --now docker; sleep 5;\
else \
echo "Container runtime is not supported";\
exit 1;\
fi;\
fi;\
if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
if ! command -v apparmor_parser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst apparmor; fi;\
if [ "$(sudo -n cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
if ! sudo -n sh -c 'command -v apparmor_parser > /dev/null 2>&1'; then \
sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst apparmor;\
fi;\
fi;\
if [ "$(systemctl is-active docker)" != "active" ]; then \
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\
sleep 5; sudo systemctl start docker; sleep 5;\
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then \
sleep 5; sudo -n systemctl start docker; sleep 5;\
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then echo "Container runtime service not running"; fi;\
fi;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
docker --version;\
sudo -n docker --version || docker --version;\
uname -sr

View File

@@ -227,36 +227,6 @@ bool SubscriptionUiController::importPremiumFromAppStore(const QString &storePro
return true;
}
bool SubscriptionUiController::importPremiumFromPlayMarket(const QString &storeProductId)
{
#if defined(Q_OS_ANDROID)
QString productId = storeProductId.trimmed();
if (productId.isEmpty()) {
productId = QStringLiteral("premium");
}
int duplicateServerIndex = -1;
ErrorCode errorCode = m_subscriptionController->processPlayMarketPurchase(
m_apiServicesModel->getCountryCode(),
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(),
productId,
&duplicateServerIndex);
if (errorCode != ErrorCode::NoError) {
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
return true;
}
emit errorOccurred(errorCode);
return false;
}
emit installServerFromApiFinished(tr("%1 has been added to the app").arg(m_apiServicesModel->getSelectedServiceName()));
#endif
return true;
}
bool SubscriptionUiController::restoreServiceFromAppStore()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
@@ -311,59 +281,6 @@ bool SubscriptionUiController::restoreServiceFromAppStore()
return true;
}
bool SubscriptionUiController::restoreServiceFromPlayMarket()
{
#if defined(Q_OS_ANDROID)
const QString premiumServiceType = QStringLiteral("amnezia-premium");
if (!fillAvailableServices()) {
qWarning().noquote() << "[Billing] Unable to fetch services list before restore";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
if (m_apiServicesModel->rowCount() <= 0) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
premiumSelected = true;
break;
}
}
if (!premiumSelected) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
SubscriptionController::PlayMarketRestoreResult result = m_subscriptionController->processPlayMarketRestore(
m_apiServicesModel->getCountryCode(),
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol());
if (!result.hasInstalledConfig) {
if (result.duplicateConfigAlreadyPresent) {
emit installServerFromApiFinished(tr("This subscription has already been added"), result.duplicateServerIndex);
return true;
}
emit errorOccurred(result.errorCode);
return false;
}
emit installServerFromApiFinished(tr("Subscription restored successfully."));
if (result.duplicateCount > 0) {
qInfo().noquote() << "[Billing] Skipped" << result.duplicateCount
<< "duplicate restored purchases for tokens already processed";
}
#endif
return true;
}
bool SubscriptionUiController::importFreeFromGateway()
{
QString userCountryCode = m_apiServicesModel->getCountryCode();

View File

@@ -45,10 +45,8 @@ public slots:
bool fillAvailableServices();
bool importPremiumFromAppStore(const QString &storeProductId);
bool importPremiumFromPlayMarket(const QString &storeProductId);
bool importFreeFromGateway();
bool restoreServiceFromAppStore();
bool restoreServiceFromPlayMarket();
bool importTrialFromGateway(const QString &email);
bool updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);

View File

@@ -187,13 +187,6 @@ PageType {
PageController.showBusyIndicator(false)
return
}
if (Qt.platform.os === "android") {
PageController.showBusyIndicator(true)
var androidStoreId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
SubscriptionUiController.importPremiumFromPlayMarket(androidStoreId)
PageController.showBusyIndicator(false)
return
}
if (plan.checkoutUrl) {
Qt.openUrlExternally(plan.checkoutUrl)
PageController.closePage()

View File

@@ -366,14 +366,10 @@ PageType {
property string title: qsTr("Restore purchases")
property string description: qsTr("")
property string imageSource: "qrc:/images/controls/refresh-cw.svg"
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild || IsPlayBuild
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild
property var handler: function() {
PageController.showBusyIndicator(true)
if (Qt.platform.os === "android") {
SubscriptionUiController.restoreServiceFromPlayMarket()
} else {
SubscriptionUiController.restoreServiceFromAppStore()
}
SubscriptionUiController.restoreServiceFromAppStore()
PageController.showBusyIndicator(false)
}
}

View File

@@ -33,8 +33,6 @@ while [[ $# -gt 0 ]]; do
--abi) abis+=("$2"); shift 2 ;;
--sign) : ${SIGN:=true}; shift ;;
--aab) : ${BUILD_AAB=true}; shift ;;
--apk) : ${BUILD_APK=true}; shift ;;
--play) : ${BUILD_PLAY=true}; shift ;;
--help|-h|?)
echo "Usage: $0 [options]"
echo " Options:"
@@ -47,8 +45,6 @@ while [[ $# -gt 0 ]]; do
echo " --abi - specify Android ABIs for target to build for. all by default"
echo " --sign - whether to sign the resulting files. only appicable to Android"
echo " --aab - whether to build AAB. only applicable to Android"
echo " --apk - whether to build APK. use with --play. only applicable to Android"
echo " --play - build Play flavor (Google Play Billing). use with --aab or --apk. only applicable to Android"
exit 0
;;
*) echo "Unknown arg \"$1\". Use $0 -h to get help"; exit 1 ;;
@@ -205,7 +201,6 @@ args=()
[[ -n "$QT_ANDROID_SIGN_AAB" ]] && args+=("-DQT_ANDROID_SIGN_AAB=$QT_ANDROID_SIGN_AAB")
[[ -n "$QT_ANDROID_ABIS" ]] && args+=("-DQT_ANDROID_ABIS=$QT_ANDROID_ABIS")
[[ -n "$QT_ANDROID_BUILD_ALL_ABIS" ]] && args+=("-DQT_ANDROID_BUILD_ALL_ABIS=$QT_ANDROID_BUILD_ALL_ABIS")
[[ -n "$BUILD_PLAY" ]] && args+=("-DANDROID_BUILD_PLAY=ON")
if [[ -n "$FORCE" ]]; then
run_traced rm -rf "$BUILD_PATH"
@@ -214,17 +209,7 @@ fi
run_traced cmake -S "$SOURCE_PATH" -B "$BUILD_PATH" "${args[@]}"
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE"
if [[ -n "$BUILD_AAB" ]]; then
if [[ -n "$BUILD_PLAY" ]]; then
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "android_play_aab"
else
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "aab"
fi
fi
if [[ -n "$BUILD_APK" ]] && [[ -n "$BUILD_PLAY" ]]; then
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "android_play_apk"
fi
[[ -n "$BUILD_AAB" ]] && run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "aab"
if [ -z "$no_installers" ]; then
for installer in $INSTALLERS; do

View File

@@ -1,7 +1,6 @@
from conan import ConanFile
from conan.tools.files import get, copy
from conan.tools.layout import basic_layout
from conan.tools.gnu import AutotoolsToolchain
from conan.errors import ConanInvalidConfiguration
from conan.tools.env import Environment
@@ -24,7 +23,7 @@ class AmneziaLibxray(ConanFile):
def build_requirements(self):
self.tool_requires("go/1.26.0")
def validate(self):
if self.settings.os != "Android":
raise ConanInvalidConfiguration(f"{self.name} v{self.version} does not support {self.settings.os}")
@@ -35,20 +34,14 @@ class AmneziaLibxray(ConanFile):
)
def generate(self):
tc = AutotoolsToolchain(self)
env = tc.environment()
env.define("CGO_LDFLAGS", tc.ldflags)
env.define("CGO_CFLAGS", tc.cflags)
tc.generate(env)
android_env = Environment()
env = Environment()
ndk_path_str = self.conf.get("tools.android:ndk_path")
if ndk_path_str:
ndk_path = Path(ndk_path_str)
if len(ndk_path.parts) > 2:
sdk_path = ndk_path.parents[1]
android_env.define("ANDROID_HOME", str(sdk_path))
android_env.vars(self).save_script("conan_provide_androidhome")
env.define("ANDROID_HOME", str(sdk_path))
env.vars(self).save_script("conan_provide_androidhome")
def _patch_sources(self):
build_path = os.path.join(self.build_folder, "build.sh")
@@ -57,15 +50,12 @@ class AmneziaLibxray(ConanFile):
def build(self):
self._patch_sources()
if self.settings_build.os == "Windows":
self.run("bash build.sh android")
else:
self.run("./build.sh android")
self.run("./build.sh android")
def package(self):
copy(self, "libxray.aar", src=self.build_folder, dst=os.path.join(self.package_folder, "aar"))
def package_info(self):
self.cpp_info.set_property("cmake_extra_variables", {
"AMNEZIA_LIBXRAY_PATH": Path(self.package_folder, "aar", "libxray.aar").as_posix(),
"AMNEZIA_LIBXRAY_PATH": os.path.join(self.package_folder, "aar", "libxray.aar"),
})

View File

@@ -1,13 +1,11 @@
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMake, CMakeToolchain
from conan.tools.files import copy, replace_in_file
from conan.tools.env import VirtualBuildEnv, Environment
from conan.errors import ConanInvalidConfiguration
from conan.tools.scm import Git
import os
import platform
from pathlib import Path
class AwgAndroid(ConanFile):
name = "awg-android"
@@ -23,11 +21,6 @@ class AwgAndroid(ConanFile):
def build_requirements(self):
self.tool_requires("cmake/[>=3.4.1 <4]")
if platform.system() == "Windows":
self.tool_requires("ninja/[*]")
self.tool_requires("go/[*]")
if not self.conf.get("tools.microsoft.bash:path", check_type=str):
self.tool_requires("msys2/cci.latest")
def validate(self):
if self.settings.os != "Android":
@@ -42,11 +35,9 @@ class AwgAndroid(ConanFile):
)
def generate(self):
VirtualBuildEnv(self).generate()
tc = CMakeToolchain(self)
tc.variables["GRADLE_USER_HOME"] = Path(os.path.join(self.build_folder, "gradle_user_home")).as_posix()
tc.variables["CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = Path(os.path.join(self.build_folder, "out")).as_posix()
tc.variables["GRADLE_USER_HOME"] = os.path.join(self.build_folder, "gradle_user_home")
tc.variables["CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = os.path.join(self.build_folder, "out")
# not to warn in case of strtok() usage
tc.extra_cflags = ["-Wno-deprecated-declarations"]
tc.generate()
@@ -73,31 +64,6 @@ class AwgAndroid(ConanFile):
'sha256sum -c',
'shasum -a 256 -c'
)
elif platform.system() == 'Windows':
# elf-cleaner uses sys/mman.h (POSIX only) and cannot be built on Windows;
# skip it — DT_FLAGS_1 warnings only affect Android < 6.0
replace_in_file(self,
os.path.join(self.source_folder, "tunnel", "tools", "CMakeLists.txt"),
'# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions\n'
'file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)\n'
'add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc\n'
' -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""\n'
' -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}\n'
')\n'
'add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"\n'
' --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg.so>")\n'
'add_dependencies(libwg.so elf-cleaner)\n'
'add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"\n'
' --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg-quick.so>")\n'
'add_dependencies(libwg-quick.so elf-cleaner)',
'',
)
# patch Makefile: skip Go download, use 'go' already in PATH from tool_requires
replace_in_file(self,
os.path.join(self.source_folder, "tunnel", "tools", "libwg-go", "Makefile"),
'$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)\n$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod',
'$(DESTDIR)/libwg-go.so: go.mod',
)
def build(self):
self._patch_sources()
@@ -115,6 +81,6 @@ class AwgAndroid(ConanFile):
self.cpp_info.set_property("cmake_target_name", "amnezia::awg-android")
self.cpp_info.libs = [ "wg-go" ]
self.cpp_info.set_property("cmake_extra_variables", {
"AMNEZIA_ANDROID_LIBWG_PATH": Path(os.path.join(self.package_folder, "bin", "libwg.so")).as_posix(),
"AMNEZIA_ANDROID_LIBWG_QUICK_PATH": Path(os.path.join(self.package_folder, "bin", "libwg-quick.so")).as_posix(),
"AMNEZIA_ANDROID_LIBWG_PATH": os.path.join(self.package_folder, "bin", "libwg.so"),
"AMNEZIA_ANDROID_LIBWG_QUICK_PATH": os.path.join(self.package_folder, "bin", "libwg-quick.so"),
})

View File

@@ -5,7 +5,6 @@ from conan.tools.scm import Git
from conan.errors import ConanInvalidConfiguration
import os
from pathlib import Path
class OpenvpnPtAndroid(ConanFile):
name = "openvpn-pt-android"
@@ -54,5 +53,5 @@ class OpenvpnPtAndroid(ConanFile):
self.cpp_info.set_property("cmake_target_name", "amnezia::openvpn-pt-android")
self.cpp_info.libs = [ "ovpn3", "ovpnutil", "rsapss" ]
self.cpp_info.set_property("cmake_extra_variables", {
"OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH": Path(self.package_folder, "lib", "libck-ovpn-plugin.so").as_posix()
"OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH": os.path.join(self.package_folder, "lib", "libck-ovpn-plugin.so")
})

View File

@@ -5,7 +5,6 @@ from conan.errors import ConanInvalidConfiguration
from conan.tools.scm import Git
from conan.internal.model.pkg_type import PackageType
from conan.tools.files import chdir
from conan.tools.apple import XCRun
import os
import shutil
@@ -50,10 +49,7 @@ class OpenVPNAdapter(ConanFile):
def build(self):
with chdir(self, self.source_folder):
xcrun = XCRun(self)
xcodebuild = xcrun.find("xcodebuild")
self.run(f"{xcodebuild}"
self.run("xcrun xcodebuild"
" -project OpenVPNAdapter.xcodeproj"
" -scheme OpenVPNAdapter"
" -configuration Release"
@@ -61,20 +57,10 @@ class OpenVPNAdapter(ConanFile):
f" -sdk {self._sdk}"
f' "CONFIGURATION_BUILD_DIR={self.build_folder}"'
f' "BUILT_PRODUCTS_DIR={self.build_folder}"'
" MACH_O_TYPE=staticlib"
" BUILD_LIBRARY_FOR_DISTRIBUTION=YES"
" CODE_SIGNING_ALLOWED=NO"
)
openvpnadapter = os.path.join(self.build_folder, "OpenVPNAdapter.framework", "OpenVPNAdapter")
self.run(f"{xcrun.libtool} -static -o"
f" {openvpnadapter}"
f" {openvpnadapter}"
f' {os.path.join(self.build_folder, "OpenVPNClient.framework", "OpenVPNClient")}'
f' {os.path.join(self.build_folder, "LZ4.framework", "LZ4")}'
f' {os.path.join(self.build_folder, "mbedTLS.framework", "mbedTLS")}'
)
def package(self):
shutil.copytree(os.path.join(self.build_folder, "OpenVPNAdapter.framework"),
os.path.join(self.package_folder, "OpenVPNAdapter.framework"))
@@ -84,4 +70,3 @@ class OpenVPNAdapter(ConanFile):
self.cpp_info.type = PackageType.STATIC
self.cpp_info.package_framework = True
self.cpp_info.location = os.path.join(self.package_folder, "OpenVPNAdapter.framework")
self.cpp_info.frameworks = ["SystemConfiguration"]