mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-27 02:51:31 +03:00
Compare commits
2 Commits
dev
...
feat/force
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5205a564 | ||
|
|
5f31f71267 |
@@ -84,4 +84,5 @@ dependencies {
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.biometric)
|
||||
implementation(libs.google.play.app.update)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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" }
|
||||
@@ -28,6 +29,7 @@ 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 = [
|
||||
|
||||
@@ -3,13 +3,6 @@ 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
|
||||
@@ -55,6 +48,15 @@ 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
|
||||
@@ -81,6 +83,9 @@ 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
|
||||
@@ -107,7 +112,23 @@ class AmneziaActivity : QtActivity() {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private var updateCoverDialog: Dialog? = null
|
||||
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 val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
@@ -383,6 +404,12 @@ class AmneziaActivity : QtActivity() {
|
||||
QtAndroidController.onActivityResumed()
|
||||
}
|
||||
|
||||
appUpdateManager?.appUpdateInfo?.addOnSuccessListener { info ->
|
||||
if (info.installStatus() == InstallStatus.DOWNLOADED) {
|
||||
promptCompleteUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
||||
val uri = pendingOpenFileUri!!
|
||||
openFileDeliveryScheduled = true
|
||||
@@ -496,119 +523,92 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun showUpdateCover() {
|
||||
fun checkPlayUpdate(restartTitle: String, restartMessage: String, restartAction: String, restartLater: String) {
|
||||
this.restartTitle = restartTitle
|
||||
this.restartMessage = restartMessage
|
||||
this.restartAction = restartAction
|
||||
this.restartLater = restartLater
|
||||
runOnUiThread {
|
||||
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())
|
||||
}
|
||||
dialog.setContentView(root)
|
||||
dialog.show()
|
||||
updateCoverDialog = dialog
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hideUpdateCover() {
|
||||
runOnUiThread {
|
||||
updateCoverDialog?.dismiss()
|
||||
updateCoverDialog = null
|
||||
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()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun showUpdatePrompt(title: String, message: String, updateTitle: String, skipTitle: String, storeUrl: String) {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
|
||||
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 {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(updateTitle) { _, _ ->
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.w(TAG, "open store failed: ${e.message}")
|
||||
}
|
||||
hideUpdateCover()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.setNegativeButton(skipTitle) { d, _ -> d.dismiss() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +1312,7 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,6 @@ object QtAndroidController {
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
|
||||
external fun onPlayUpdateResult(status: Int)
|
||||
}
|
||||
@@ -103,7 +103,8 @@ 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)}
|
||||
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)},
|
||||
{"onPlayUpdateResult", "(I)V", reinterpret_cast<void *>(onPlayUpdateResult)}
|
||||
};
|
||||
|
||||
QJniEnvironment env;
|
||||
@@ -154,14 +155,15 @@ void AndroidController::resetLastServer(int serverIndex)
|
||||
callActivityMethod("resetLastServer", "(I)V", serverIndex);
|
||||
}
|
||||
|
||||
void AndroidController::showUpdateCover()
|
||||
void AndroidController::checkPlayUpdate(const QString &restartTitle, const QString &restartMessage,
|
||||
const QString &restartAction, const QString &restartLater)
|
||||
{
|
||||
callActivityMethod("showUpdateCover", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::hideUpdateCover()
|
||||
{
|
||||
callActivityMethod("hideUpdateCover", "()V");
|
||||
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>());
|
||||
}
|
||||
|
||||
void AndroidController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
@@ -600,4 +602,13 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ public:
|
||||
bool requestAuthentication();
|
||||
void sendTouch(float x, float y);
|
||||
|
||||
void showUpdateCover();
|
||||
void hideUpdateCover();
|
||||
void checkPlayUpdate(const QString &restartTitle, const QString &restartMessage, const QString &restartAction,
|
||||
const QString &restartLater);
|
||||
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl);
|
||||
|
||||
@@ -82,6 +82,7 @@ signals:
|
||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||
void activityPaused();
|
||||
void activityResumed();
|
||||
void playUpdateResult(int status);
|
||||
|
||||
private:
|
||||
bool isWaitingStatus = true;
|
||||
@@ -114,6 +115,7 @@ 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);
|
||||
|
||||
@@ -81,8 +81,6 @@ 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:
|
||||
|
||||
@@ -1214,7 +1214,7 @@ bool IosController::isTestFlight() {
|
||||
}
|
||||
|
||||
#if !MACOS_NE
|
||||
static UIWindow *s_updateCoverWindow = nil;
|
||||
static UIWindow *s_updateAlertWindow = nil;
|
||||
|
||||
static UIWindowScene *activeWindowScene() {
|
||||
UIWindowScene *fallback = nil;
|
||||
@@ -1231,46 +1231,6 @@ 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
|
||||
@@ -1281,67 +1241,54 @@ void IosController::showUpdatePrompt(const QString &title, const QString &messag
|
||||
NSString *nsUrl = storeUrl.toNSString();
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!s_updateCoverWindow) {
|
||||
if (s_updateAlertWindow) {
|
||||
return;
|
||||
}
|
||||
UIWindowScene *scene = activeWindowScene();
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
UIViewController *vc = s_updateCoverWindow.rootViewController;
|
||||
|
||||
void (^dismissCover)(void) = ^{
|
||||
s_updateCoverWindow.hidden = YES;
|
||||
[s_updateCoverWindow release];
|
||||
s_updateCoverWindow = nil;
|
||||
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;
|
||||
};
|
||||
|
||||
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
|
||||
titleLabel.text = nsTitle;
|
||||
titleLabel.font = [UIFont boldSystemFontOfSize:22];
|
||||
titleLabel.textColor = [UIColor whiteColor];
|
||||
titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
titleLabel.numberOfLines = 0;
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nsTitle
|
||||
message:nsMessage
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
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) {
|
||||
UIAlertAction *updateAction = [UIAlertAction actionWithTitle:nsUpdate
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
NSURL *url = [NSURL URLWithString:nsUrl];
|
||||
if (url) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
dismissCover();
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
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];
|
||||
UIAlertAction *skipAction = [UIAlertAction actionWithTitle:nsSkip
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *action) {
|
||||
dismiss();
|
||||
}];
|
||||
|
||||
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];
|
||||
[alert addAction:updateAction];
|
||||
[alert addAction:skipAction];
|
||||
alert.preferredAction = updateAction;
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[stack.centerYAnchor constraintEqualToAnchor:vc.view.centerYAnchor],
|
||||
[stack.leadingAnchor constraintEqualToAnchor:vc.view.leadingAnchor constant:32],
|
||||
[stack.trailingAnchor constraintEqualToAnchor:vc.view.trailingAnchor constant:-32]
|
||||
]];
|
||||
[vc presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
#else
|
||||
Q_UNUSED(title) Q_UNUSED(message) Q_UNUSED(updateTitle) Q_UNUSED(skipTitle) Q_UNUSED(storeUrl)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QVersionNumber>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
@@ -31,7 +33,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 kAndroidStoreUrl = "https://play.google.com/store/apps/details?id=org.amnezia.vpn";
|
||||
constexpr auto kGithubReleasesUrl = "https://github.com/amnezia-vpn/amnezia-client/releases/latest";
|
||||
#endif
|
||||
} // namespace
|
||||
#endif
|
||||
@@ -42,28 +44,49 @@ MarketplaceUpdateController::MarketplaceUpdateController(QObject *parent) : QObj
|
||||
|
||||
void MarketplaceUpdateController::start()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
// 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();
|
||||
#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()
|
||||
{
|
||||
const QUrl url = versionSourceUrl();
|
||||
if (!url.isValid()) {
|
||||
hideCover();
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("AmneziaVPN"));
|
||||
request.setTransferTimeout(7000);
|
||||
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[MarketplaceUpdate] network error:" << reply->errorString();
|
||||
hideCover();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +94,6 @@ void MarketplaceUpdateController::start()
|
||||
QString storeUrl;
|
||||
if (!parseStoreVersion(reply->readAll(), version, storeUrl) || version.isEmpty()) {
|
||||
qWarning() << "[MarketplaceUpdate] could not determine store version";
|
||||
hideCover();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,14 +103,10 @@ void MarketplaceUpdateController::start()
|
||||
|
||||
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)
|
||||
@@ -124,29 +142,11 @@ bool MarketplaceUpdateController::parseStoreVersion(const QByteArray &body, QStr
|
||||
if (match.hasMatch()) {
|
||||
version = match.captured(1);
|
||||
}
|
||||
storeUrl = QString::fromLatin1(kAndroidStoreUrl);
|
||||
storeUrl = QString::fromLatin1(kGithubReleasesUrl);
|
||||
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");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef MARKETPLACEUPDATECONTROLLER_H
|
||||
#define MARKETPLACEUPDATECONTROLLER_H
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
@@ -17,13 +16,15 @@ 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
|
||||
|
||||
QNetworkAccessManager m_nam;
|
||||
#if defined(Q_OS_ANDROID)
|
||||
private slots:
|
||||
void onPlayUpdateResult(int status);
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user