diff --git a/CMakeLists.txt b/CMakeLists.txt index d6cdaac0b..3d741ada2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -set(AMNEZIAVPN_VERSION 4.8.14.5) +set(AMNEZIAVPN_VERSION 4.8.14.6) project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} DESCRIPTION "AmneziaVPN" @@ -12,7 +12,8 @@ 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 2117) + +set(APP_ANDROID_VERSION_CODE 2118) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/android/build.gradle.kts b/client/android/build.gradle.kts index 3c742621c..df67e8e79 100644 --- a/client/android/build.gradle.kts +++ b/client/android/build.gradle.kts @@ -122,4 +122,5 @@ dependencies { implementation(libs.google.mlkit) implementation(libs.androidx.datastore) implementation(libs.androidx.biometric) + implementation(libs.google.play.review) } diff --git a/client/android/gradle/libs.versions.toml b/client/android/gradle/libs.versions.toml index c6fa19073..fd00dd3ee 100644 --- a/client/android/gradle/libs.versions.toml +++ b/client/android/gradle/libs.versions.toml @@ -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-review = "2.0.2" [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-review = { module = "com.google.android.play:review-ktx", version.ref = "google-play-review" } [bundles] androidx-camera = [ diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index daedfda3f..31ddab740 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -297,7 +297,10 @@ class AmneziaActivity : QtActivity() { Log.d(TAG, "Window focus changed: hasFocus=$hasFocus") // Cancel pending operations if window loses focus - if (!hasFocus) { + if (hasFocus) { + ReviewManager.onWindowFocusGained(this, mainScope) + } else { + // Cancel pending operations if window loses focus resumeHandler.removeCallbacksAndMessages(null) } } @@ -350,6 +353,8 @@ class AmneziaActivity : QtActivity() { isActivityResumed = true Log.d(TAG, "Resume Amnezia activity") + ReviewManager.onActivityResumed(mainScope) + if (pendingOpenFileUri != null && !openFileDeliveryScheduled) { val uri = pendingOpenFileUri!! openFileDeliveryScheduled = true diff --git a/client/android/src/org/amnezia/vpn/ReviewManager.kt b/client/android/src/org/amnezia/vpn/ReviewManager.kt new file mode 100644 index 000000000..c6aad20aa --- /dev/null +++ b/client/android/src/org/amnezia/vpn/ReviewManager.kt @@ -0,0 +1,58 @@ +package org.amnezia.vpn + +import android.app.Activity +import com.google.android.play.core.review.ReviewManagerFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.Prefs + +private const val TAG = "ReviewManager" + +private const val PREFS_REVIEW_OPEN_COUNT = "REVIEW_OPEN_COUNT" +private const val REVIEW_REQUEST_OPEN_INTERVAL = 20 + +object ReviewManager { + + @Volatile + private var shouldRequestReviewOnFocus = false + + /** + * Call from onResume + * Increments the open count and arms the flag if the interval is hit. + * I/O runs on the IO dispatcher to avoid blocking the main thread. + */ + fun onActivityResumed(scope: CoroutineScope) { + scope.launch(Dispatchers.IO) { + val openCount = Prefs.load(PREFS_REVIEW_OPEN_COUNT) + 1 + Prefs.save(PREFS_REVIEW_OPEN_COUNT, openCount) + + shouldRequestReviewOnFocus = openCount > 0 && openCount % REVIEW_REQUEST_OPEN_INTERVAL == 0 + Log.i(TAG, "onActivityResumed: openCount=$openCount, shouldRequestReview=$shouldRequestReviewOnFocus") + } + } + + /** + * Call from onWindowFocusChanged (hasFocus=true) + */ + fun onWindowFocusGained(activity: Activity, scope: CoroutineScope) { + if (!shouldRequestReviewOnFocus) return + shouldRequestReviewOnFocus = false + + scope.launch(Dispatchers.Main) { + requestReviewFlow(activity) + } + } + + private fun requestReviewFlow(activity: Activity) { + val reviewManager = ReviewManagerFactory.create(activity) + reviewManager.requestReviewFlow().addOnCompleteListener { request -> + if (request.isSuccessful) { + reviewManager.launchReviewFlow(activity, request.result) + } else { + Log.w(TAG, "Review flow request failed: ${request.exception}") + } + } + } +}