Compare commits

..

1 Commits

Author SHA1 Message Date
vkamn
4d28f81965 fix: set timeout for MarketplaceUpdateController 2026-07-26 15:20:53 +08:00
11 changed files with 242 additions and 206 deletions

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 5.0.0.2)
set(AMNEZIAVPN_VERSION 5.0.0.5)
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 2135)
set(APP_ANDROID_VERSION_CODE 2136)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -84,5 +84,4 @@ dependencies {
implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
implementation(libs.androidx.biometric)
implementation(libs.google.play.app.update)
}

View File

@@ -12,7 +12,6 @@ androidx-datastore = "1.1.1"
kotlinx-coroutines = "1.8.1"
kotlinx-serialization = "1.6.3"
google-mlkit = "17.3.0"
google-play-app-update = "2.1.0"
[libraries]
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
@@ -29,7 +28,6 @@ androidx-datastore = { module = "androidx.datastore:datastore-preferences", vers
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
google-play-app-update = { module = "com.google.android.play:app-update-ktx", version.ref = "google-play-app-update" }
[bundles]
androidx-camera = [

View File

@@ -3,6 +3,13 @@ package org.amnezia.vpn
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
@@ -48,15 +55,6 @@ import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
import kotlin.text.RegexOption.IGNORE_CASE
import AppListProvider
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.InstallState
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -83,9 +81,6 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val UPDATE_APP_ACTION_CODE = 5
private const val PLAY_UPDATE_UNAVAILABLE = 1
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
@@ -112,23 +107,7 @@ class AmneziaActivity : QtActivity() {
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private var appUpdateManager: AppUpdateManager? = null
private var restartTitle = ""
private var restartMessage = ""
private var restartAction = ""
private var restartLater = ""
private var restartPromptShown = false
private val installStateListener = InstallStateUpdatedListener(::onInstallStateUpdated)
private fun onInstallStateUpdated(state: InstallState) {
when (state.installStatus()) {
InstallStatus.DOWNLOADED -> promptCompleteUpdate()
InstallStatus.INSTALLED, InstallStatus.FAILED, InstallStatus.CANCELED ->
appUpdateManager?.unregisterListener(installStateListener)
else -> {}
}
}
private var updateCoverDialog: Dialog? = null
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -404,12 +383,6 @@ class AmneziaActivity : QtActivity() {
QtAndroidController.onActivityResumed()
}
appUpdateManager?.appUpdateInfo?.addOnSuccessListener { info ->
if (info.installStatus() == InstallStatus.DOWNLOADED) {
promptCompleteUpdate()
}
}
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!!
openFileDeliveryScheduled = true
@@ -523,92 +496,119 @@ class AmneziaActivity : QtActivity() {
super.onDestroy()
}
fun checkPlayUpdate(restartTitle: String, restartMessage: String, restartAction: String, restartLater: String) {
this.restartTitle = restartTitle
this.restartMessage = restartMessage
this.restartAction = restartAction
this.restartLater = restartLater
fun showUpdateCover() {
runOnUiThread {
val manager = appUpdateManager
?: AppUpdateManagerFactory.create(applicationContext).also { appUpdateManager = it }
manager.appUpdateInfo
.addOnSuccessListener { info ->
val available = info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
if (available) {
startPlayFlexibleFlow(info)
} else {
Log.d(TAG, "Play update not available, using fallback")
QtAndroidController.onPlayUpdateResult(PLAY_UPDATE_UNAVAILABLE)
}
}
.addOnFailureListener { e ->
Log.w(TAG, "checkPlayUpdate failed: ${e.message}")
QtAndroidController.onPlayUpdateResult(PLAY_UPDATE_UNAVAILABLE)
}
}
}
private fun startPlayFlexibleFlow(info: AppUpdateInfo) {
val manager = appUpdateManager ?: run {
QtAndroidController.onPlayUpdateResult(PLAY_UPDATE_UNAVAILABLE)
return
}
try {
manager.registerListener(installStateListener)
manager.startUpdateFlowForResult(
info, this, AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(), UPDATE_APP_ACTION_CODE
)
} catch (e: Exception) {
Log.w(TAG, "startPlayUpdateFlow failed: ${e.message}")
manager.unregisterListener(installStateListener)
QtAndroidController.onPlayUpdateResult(PLAY_UPDATE_UNAVAILABLE)
}
}
private fun promptCompleteUpdate() {
if (isFinishing || isDestroyed || restartPromptShown) return
restartPromptShown = true
AlertDialog.Builder(this)
.setTitle(restartTitle)
.setMessage(restartMessage)
.setCancelable(false)
.setPositiveButton(restartAction) { _, _ -> appUpdateManager?.completeUpdate() }
.setNegativeButton(restartLater) { d, _ ->
restartPromptShown = false
d.dismiss()
if (isFinishing || isDestroyed || updateCoverDialog != null) return@runOnUiThread
val dialog = Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
dialog.setCancelable(false)
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(0xFF0E0E11.toInt())
}
.show()
dialog.setContentView(root)
dialog.show()
updateCoverDialog = dialog
}
}
fun hideUpdateCover() {
runOnUiThread {
updateCoverDialog?.dismiss()
updateCoverDialog = null
}
}
fun showUpdatePrompt(title: String, message: String, updateTitle: String, skipTitle: String, storeUrl: String) {
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setCancelable(true)
.setPositiveButton(updateTitle) { _, _ ->
val dialog = updateCoverDialog ?: Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen).also {
it.setCancelable(false)
it.show()
updateCoverDialog = it
}
val density = resources.displayMetrics.density
fun dp(value: Int) = (value * density).toInt()
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(0xFF0E0E11.toInt())
setPadding(dp(32), dp(32), dp(32), dp(32))
}
val titleView = TextView(this).apply {
text = title
textSize = 22f
setTextColor(0xFFFFFFFF.toInt())
gravity = Gravity.CENTER
typeface = Typeface.create(typeface, Typeface.BOLD)
}
val messageView = TextView(this).apply {
text = message
textSize = 16f
setTextColor(0xFFC7C8CB.toInt())
gravity = Gravity.CENTER
setPadding(0, dp(16), 0, dp(28))
}
val updateButton = Button(this).apply {
text = updateTitle
isAllCaps = false
textSize = 17f
setTextColor(0xFF0E0E11.toInt())
stateListAnimator = null
background = GradientDrawable().apply {
cornerRadius = dp(12).toFloat()
setColor(0xFFFBB26A.toInt())
}
setOnClickListener {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)))
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "open store failed: ${e.message}")
}
hideUpdateCover()
}
.setNegativeButton(skipTitle) { d, _ -> d.dismiss() }
.show()
}
val skipButton = Button(this).apply {
text = skipTitle
isAllCaps = false
textSize = 17f
setTextColor(0xFFD7D8DB.toInt())
stateListAnimator = null
background = GradientDrawable().apply {
cornerRadius = dp(12).toFloat()
setColor(0x00000000)
setStroke(dp(1), 0xFF2C2D30.toInt())
}
setOnClickListener { hideUpdateCover() }
}
val updateParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
).apply { topMargin = dp(8) }
val skipParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
).apply { topMargin = dp(12) }
root.addView(titleView)
root.addView(messageView)
root.addView(updateButton, updateParams)
root.addView(skipButton, skipParams)
dialog.setContentView(root)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")
if (requestCode == UPDATE_APP_ACTION_CODE) {
if (resultCode != RESULT_OK) {
appUpdateManager?.unregisterListener(installStateListener)
}
return
}
actionResultHandlers[requestCode]?.let { handler ->
when (resultCode) {
RESULT_OK -> handler.onSuccess(data)
@@ -1312,7 +1312,6 @@ class AmneziaActivity : QtActivity() {
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
UPDATE_APP_ACTION_CODE -> "UPDATE_APP"
else -> actionCode.toString()
}
}

View File

@@ -34,6 +34,4 @@ object QtAndroidController {
external fun onActivityPaused()
external fun onActivityResumed()
external fun onPlayUpdateResult(status: Int)
}

View File

@@ -103,8 +103,7 @@ bool AndroidController::initialize()
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)},
{"onPlayUpdateResult", "(I)V", reinterpret_cast<void *>(onPlayUpdateResult)}
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
};
QJniEnvironment env;
@@ -155,15 +154,14 @@ void AndroidController::resetLastServer(int serverIndex)
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::checkPlayUpdate(const QString &restartTitle, const QString &restartMessage,
const QString &restartAction, const QString &restartLater)
void AndroidController::showUpdateCover()
{
callActivityMethod("checkPlayUpdate",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(restartTitle).object<jstring>(),
QJniObject::fromString(restartMessage).object<jstring>(),
QJniObject::fromString(restartAction).object<jstring>(),
QJniObject::fromString(restartLater).object<jstring>());
callActivityMethod("showUpdateCover", "()V");
}
void AndroidController::hideUpdateCover()
{
callActivityMethod("hideUpdateCover", "()V");
}
void AndroidController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
@@ -602,13 +600,4 @@ void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
emit AndroidController::instance()->activityResumed();
}
// static
void AndroidController::onPlayUpdateResult(JNIEnv *env, jobject thiz, jint status)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->playUpdateResult(static_cast<int>(status));
}

View File

@@ -56,8 +56,8 @@ public:
bool requestAuthentication();
void sendTouch(float x, float y);
void checkPlayUpdate(const QString &restartTitle, const QString &restartMessage, const QString &restartAction,
const QString &restartLater);
void showUpdateCover();
void hideUpdateCover();
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl);
@@ -82,7 +82,6 @@ signals:
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused();
void activityResumed();
void playUpdateResult(int status);
private:
bool isWaitingStatus = true;
@@ -115,7 +114,6 @@ private:
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz);
static void onPlayUpdateResult(JNIEnv *env, jobject thiz, jint status);
template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);

View File

@@ -81,6 +81,8 @@ public:
void requestInetAccess();
bool isTestFlight();
void showUpdateCover();
void hideUpdateCover();
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl);
signals:

View File

@@ -1214,7 +1214,7 @@ bool IosController::isTestFlight() {
}
#if !MACOS_NE
static UIWindow *s_updateAlertWindow = nil;
static UIWindow *s_updateCoverWindow = nil;
static UIWindowScene *activeWindowScene() {
UIWindowScene *fallback = nil;
@@ -1231,6 +1231,46 @@ static UIWindowScene *activeWindowScene() {
}
#endif
void IosController::showUpdateCover() {
#if !MACOS_NE
void (^build)(void) = ^{
if (s_updateCoverWindow) {
return;
}
UIWindowScene *scene = activeWindowScene();
if (!scene) {
return;
}
UIWindow *win = [[UIWindow alloc] initWithWindowScene:scene];
win.windowLevel = UIWindowLevelAlert + 1;
UIViewController *vc = [[[UIViewController alloc] init] autorelease];
vc.view.backgroundColor = [UIColor colorWithRed:0.055 green:0.055 blue:0.063 alpha:1.0];
win.rootViewController = vc;
[win makeKeyAndVisible];
s_updateCoverWindow = win;
};
if ([NSThread isMainThread]) {
build();
} else {
dispatch_sync(dispatch_get_main_queue(), build);
}
#endif
}
void IosController::hideUpdateCover() {
#if !MACOS_NE
dispatch_async(dispatch_get_main_queue(), ^{
if (!s_updateCoverWindow) {
return;
}
s_updateCoverWindow.hidden = YES;
[s_updateCoverWindow release];
s_updateCoverWindow = nil;
});
#endif
}
void IosController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl) {
#if !MACOS_NE
@@ -1241,54 +1281,67 @@ void IosController::showUpdatePrompt(const QString &title, const QString &messag
NSString *nsUrl = storeUrl.toNSString();
dispatch_async(dispatch_get_main_queue(), ^{
if (s_updateAlertWindow) {
return;
}
UIWindowScene *scene = activeWindowScene();
if (!scene) {
if (!s_updateCoverWindow) {
return;
}
UIViewController *vc = s_updateCoverWindow.rootViewController;
UIWindow *win = [[UIWindow alloc] initWithWindowScene:scene];
win.windowLevel = UIWindowLevelAlert + 1;
win.backgroundColor = [UIColor clearColor];
UIViewController *vc = [[[UIViewController alloc] init] autorelease];
vc.view.backgroundColor = [UIColor clearColor];
win.rootViewController = vc;
[win makeKeyAndVisible];
s_updateAlertWindow = win;
void (^dismiss)(void) = ^{
s_updateAlertWindow.hidden = YES;
[s_updateAlertWindow release];
s_updateAlertWindow = nil;
void (^dismissCover)(void) = ^{
s_updateCoverWindow.hidden = YES;
[s_updateCoverWindow release];
s_updateCoverWindow = nil;
};
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nsTitle
message:nsMessage
preferredStyle:UIAlertControllerStyleAlert];
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
titleLabel.text = nsTitle;
titleLabel.font = [UIFont boldSystemFontOfSize:22];
titleLabel.textColor = [UIColor whiteColor];
titleLabel.textAlignment = NSTextAlignmentCenter;
titleLabel.numberOfLines = 0;
UIAlertAction *updateAction = [UIAlertAction actionWithTitle:nsUpdate
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
UILabel *messageLabel = [[[UILabel alloc] init] autorelease];
messageLabel.text = nsMessage;
messageLabel.font = [UIFont systemFontOfSize:16];
messageLabel.textColor = [UIColor colorWithWhite:0.78 alpha:1.0];
messageLabel.textAlignment = NSTextAlignmentCenter;
messageLabel.numberOfLines = 0;
UIButton *updateButton = [UIButton buttonWithType:UIButtonTypeSystem];
[updateButton setTitle:nsUpdate forState:UIControlStateNormal];
[updateButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
updateButton.backgroundColor = [UIColor colorWithRed:1.0 green:0.6 blue:0.0 alpha:1.0];
updateButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
updateButton.layer.cornerRadius = 12;
[updateButton.heightAnchor constraintEqualToConstant:52].active = YES;
[updateButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
NSURL *url = [NSURL URLWithString:nsUrl];
if (url) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
dismiss();
}];
dismissCover();
}] forControlEvents:UIControlEventTouchUpInside];
UIAlertAction *skipAction = [UIAlertAction actionWithTitle:nsSkip
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
dismiss();
}];
UIButton *skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
[skipButton setTitle:nsSkip forState:UIControlStateNormal];
[skipButton setTitleColor:[UIColor colorWithWhite:0.7 alpha:1.0] forState:UIControlStateNormal];
skipButton.titleLabel.font = [UIFont systemFontOfSize:17];
[skipButton.heightAnchor constraintEqualToConstant:44].active = YES;
[skipButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
dismissCover();
}] forControlEvents:UIControlEventTouchUpInside];
[alert addAction:updateAction];
[alert addAction:skipAction];
alert.preferredAction = updateAction;
UIStackView *stack = [[[UIStackView alloc] initWithArrangedSubviews:@[titleLabel, messageLabel, updateButton, skipButton]] autorelease];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 16;
stack.translatesAutoresizingMaskIntoConstraints = NO;
[stack setCustomSpacing:28 afterView:messageLabel];
[vc.view addSubview:stack];
[vc presentViewController:alert animated:YES completion:nil];
[NSLayoutConstraint activateConstraints:@[
[stack.centerYAnchor constraintEqualToAnchor:vc.view.centerYAnchor],
[stack.leadingAnchor constraintEqualToAnchor:vc.view.leadingAnchor constant:32],
[stack.trailingAnchor constraintEqualToAnchor:vc.view.trailingAnchor constant:-32]
]];
});
#else
Q_UNUSED(title) Q_UNUSED(message) Q_UNUSED(updateTitle) Q_UNUSED(skipTitle) Q_UNUSED(storeUrl)

View File

@@ -13,8 +13,6 @@
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QVersionNumber>
#include "amneziaApplication.h"
#endif
#if defined(Q_OS_IOS)
@@ -33,7 +31,7 @@ constexpr auto kIosBundleId = "org.amnezia.AmneziaVPN";
constexpr auto kIosStoreUrlFallback = "itms-apps://itunes.apple.com/app/id1600529900";
#else
constexpr auto kAndroidPackage = "org.amnezia.vpn";
constexpr auto kGithubReleasesUrl = "https://github.com/amnezia-vpn/amnezia-client/releases/latest";
constexpr auto kAndroidStoreUrl = "https://play.google.com/store/apps/details?id=org.amnezia.vpn";
#endif
} // namespace
#endif
@@ -44,49 +42,29 @@ MarketplaceUpdateController::MarketplaceUpdateController(QObject *parent) : QObj
void MarketplaceUpdateController::start()
{
#if defined(Q_OS_IOS)
startNetworkCheck();
#elif defined(Q_OS_ANDROID)
connect(AndroidController::instance(), &AndroidController::playUpdateResult, this,
&MarketplaceUpdateController::onPlayUpdateResult, Qt::UniqueConnection);
const QString restartTitle = tr("Update downloaded");
const QString restartMessage = tr("An update to AmneziaVPN has been downloaded. Restart to install it.");
const QString restartAction = tr("Restart");
const QString restartLater = tr("Later");
AndroidController::instance()->checkPlayUpdate(restartTitle, restartMessage, restartAction, restartLater);
#endif
}
#if defined(Q_OS_ANDROID)
void MarketplaceUpdateController::onPlayUpdateResult(int status)
{
if (status == 1) {
qInfo() << "[MarketplaceUpdate] Play unavailable, falling back to GitHub";
startNetworkCheck();
}
}
#endif
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
void MarketplaceUpdateController::startNetworkCheck()
{
// Cover the app immediately so the store UI is not visible before the check
// resolves (avoids a flash of the main window before the update screen).
showCover();
const QUrl url = versionSourceUrl();
if (!url.isValid()) {
hideCover();
return;
}
QNetworkRequest request(url);
request.setTransferTimeout(1000);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("AmneziaVPN"));
request.setTransferTimeout(7000);
QNetworkReply *reply = amnApp->networkManager()->get(request);
QNetworkReply *reply = m_nam.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[MarketplaceUpdate] network error:" << reply->errorString();
hideCover();
return;
}
@@ -94,6 +72,7 @@ void MarketplaceUpdateController::startNetworkCheck()
QString storeUrl;
if (!parseStoreVersion(reply->readAll(), version, storeUrl) || version.isEmpty()) {
qWarning() << "[MarketplaceUpdate] could not determine store version";
hideCover();
return;
}
@@ -103,10 +82,14 @@ void MarketplaceUpdateController::startNetworkCheck()
if (store > current) {
showUpdatePrompt(storeUrl);
} else {
hideCover();
}
});
#endif
}
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QUrl MarketplaceUpdateController::versionSourceUrl() const
{
#if defined(Q_OS_IOS)
@@ -142,11 +125,29 @@ bool MarketplaceUpdateController::parseStoreVersion(const QByteArray &body, QStr
if (match.hasMatch()) {
version = match.captured(1);
}
storeUrl = QString::fromLatin1(kGithubReleasesUrl);
storeUrl = QString::fromLatin1(kAndroidStoreUrl);
return !version.isEmpty();
#endif
}
void MarketplaceUpdateController::showCover()
{
#if defined(Q_OS_IOS)
IosController::Instance()->showUpdateCover();
#else
AndroidController::instance()->showUpdateCover();
#endif
}
void MarketplaceUpdateController::hideCover()
{
#if defined(Q_OS_IOS)
IosController::Instance()->hideUpdateCover();
#else
AndroidController::instance()->hideUpdateCover();
#endif
}
void MarketplaceUpdateController::showUpdatePrompt(const QString &storeUrl)
{
const QString title = tr("Update available");

View File

@@ -1,6 +1,7 @@
#ifndef MARKETPLACEUPDATECONTROLLER_H
#define MARKETPLACEUPDATECONTROLLER_H
#include <QNetworkAccessManager>
#include <QObject>
#include <QUrl>
@@ -16,15 +17,13 @@ public slots:
private:
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
void startNetworkCheck();
QUrl versionSourceUrl() const;
bool parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl);
void showCover();
void hideCover();
void showUpdatePrompt(const QString &storeUrl);
#endif
#if defined(Q_OS_ANDROID)
private slots:
void onPlayUpdateResult(int status);
QNetworkAccessManager m_nam;
#endif
};