From 82c0a26625d48c4caa25debda30de5c10f84eba9 Mon Sep 17 00:00:00 2001 From: Elelan's Macbook Pro Date: Thu, 1 May 2025 14:27:18 +0530 Subject: [PATCH 1/2] app rating poc --- .../openarchive/features/main/MainActivity.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index 5c2ff5ef..4fb7066b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -1,5 +1,6 @@ package net.opendasharchive.openarchive.features.main +import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Point @@ -16,6 +17,7 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.LinearLayout import android.widget.PopupWindow +import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat @@ -27,6 +29,12 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.snackbar.Snackbar +import com.google.android.play.core.review.ReviewException +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.review.model.ReviewErrorCode +import com.google.android.play.core.review.testing.FakeReviewManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.opendasharchive.openarchive.BuildConfig @@ -129,6 +137,10 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda private lateinit var permissionManager: PermissionManager + private lateinit var reviewManager: ReviewManager +// private lateinit var reviewManager: FakeReviewManager + var reviewInfo: ReviewInfo? = null + override fun onCreate(savedInstanceState: Bundle?) { ///enableEdgeToEdge() @@ -188,6 +200,11 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda startActivity(Intent(this, HomeActivity::class.java)) true } + } else { + binding.contentMain.imgLogo.setOnLongClickListener { + validateAndShowReviewFlow() + true + } } supportFragmentManager.setFragmentResultListener("uploadRetry", this) { key, bundle -> @@ -196,6 +213,48 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda // You can start the upload service or update the UI accordingly. UploadService.startUploadService(this) } + + reviewManager = ReviewManagerFactory.create(this) +// reviewManager = FakeReviewManager(this) + + requestReviewInfo() + } + + fun requestReviewInfo() { + val request = reviewManager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Store the ReviewInfo object for later use. + reviewInfo = task.result + AppLogger.d("InAppReview", "ReviewInfo obtained successfully.") + } else { + // Handle the error, possibly log it. Don't show an error message to the user. + task.exception?.printStackTrace() + val reviewErrorCode = (task.exception as? ReviewException)?.errorCode + AppLogger.e("InAppReview", "Error requesting review flow: $reviewErrorCode", task.exception) + reviewInfo = null // Reset if failed + } + } + } + + fun showReviewFlow(activity: Activity) { // Pass the current activity + reviewInfo?.let { info -> + val flow = reviewManager.launchReviewFlow(activity, info) + flow.addOnCompleteListener { _ -> + // The flow has finished. The API does NOT indicate whether the user + // reviewed or not, or even if the dialog was shown. + // Continue your app flow regardless of the outcome. + // It's important NOT to change your app's logic based on this completion. + AppLogger.d("InAppReview", "Review flow finished.") + // Reset reviewInfo for next potential request cycle if desired, + // or rely on requesting it again when needed. + reviewInfo = null + } + } ?: run { + AppLogger.d("InAppReview", "ReviewInfo is null, cannot launch review flow.") + // Optionally, you could attempt to request it again here if appropriate, + // but usually, you'd request it ahead of time. + } } override fun onResume() { @@ -727,6 +786,18 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda mFolderAdapter.update(projects) } + private fun validateAndShowReviewFlow() { + // show the review flow only if there is at least one server available and that server has at least one project/folder + val canShowReviewFlow = Space.current?.projects?.isNotEmpty() == true + + if (canShowReviewFlow) { + showReviewFlow(this@MainActivity) + } else { + AppLogger.d("InAppReview", "Cannot show review flow: No projects available.") + Toast.makeText(this, "Cannot show review flow: No projects available.", Toast.LENGTH_LONG).show() + } + } + private fun refreshCurrentProject() { val project = getSelectedProject() From 24f770642ac2c85155e1256f2f06c4cb14a869be Mon Sep 17 00:00:00 2001 From: Elelan's Macbook Pro Date: Wed, 4 Jun 2025 00:24:08 +0530 Subject: [PATCH 2/2] Integrate in-app review functionality with helper methods for managing review prompts --- .../openarchive/features/main/MainActivity.kt | 80 +++++---------- .../openarchive/util/InAppReviewHelper.kt | 97 +++++++++++++++++++ 2 files changed, 119 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/util/InAppReviewHelper.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index 8b42e289..13215329 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -86,6 +86,9 @@ import net.opendasharchive.openarchive.util.extensions.show import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import java.text.NumberFormat +import androidx.core.content.edit +import kotlinx.coroutines.delay +import net.opendasharchive.openarchive.util.InAppReviewHelper class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAdapterListener { @@ -138,9 +141,7 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda private lateinit var permissionManager: PermissionManager private lateinit var reviewManager: ReviewManager -// private lateinit var reviewManager: FakeReviewManager - var reviewInfo: ReviewInfo? = null - + private var shouldPromptReview = false override fun onCreate(savedInstanceState: Bundle?) { ///enableEdgeToEdge() @@ -177,6 +178,9 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda // Initialize the permission manager with this activity and its dialogManager. permissionManager = PermissionManager(this, dialogManager) + // Initialize In App Ratings Helper + InAppReviewHelper.init(this) + initMediaLaunchers() setupToolbarAndPager() setupNavigationDrawer() @@ -200,11 +204,6 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda startActivity(Intent(this, HomeActivity::class.java)) true } - } else { - binding.contentMain.imgLogo.setOnLongClickListener { - validateAndShowReviewFlow() - true - } } supportFragmentManager.setFragmentResultListener("uploadRetry", this) { key, bundle -> @@ -215,46 +214,8 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda } reviewManager = ReviewManagerFactory.create(this) -// reviewManager = FakeReviewManager(this) - - requestReviewInfo() - } - - fun requestReviewInfo() { - val request = reviewManager.requestReviewFlow() - request.addOnCompleteListener { task -> - if (task.isSuccessful) { - // Store the ReviewInfo object for later use. - reviewInfo = task.result - AppLogger.d("InAppReview", "ReviewInfo obtained successfully.") - } else { - // Handle the error, possibly log it. Don't show an error message to the user. - task.exception?.printStackTrace() - val reviewErrorCode = (task.exception as? ReviewException)?.errorCode - AppLogger.e("InAppReview", "Error requesting review flow: $reviewErrorCode", task.exception) - reviewInfo = null // Reset if failed - } - } - } - - fun showReviewFlow(activity: Activity) { // Pass the current activity - reviewInfo?.let { info -> - val flow = reviewManager.launchReviewFlow(activity, info) - flow.addOnCompleteListener { _ -> - // The flow has finished. The API does NOT indicate whether the user - // reviewed or not, or even if the dialog was shown. - // Continue your app flow regardless of the outcome. - // It's important NOT to change your app's logic based on this completion. - AppLogger.d("InAppReview", "Review flow finished.") - // Reset reviewInfo for next potential request cycle if desired, - // or rely on requesting it again when needed. - reviewInfo = null - } - } ?: run { - AppLogger.d("InAppReview", "ReviewInfo is null, cannot launch review flow.") - // Optionally, you could attempt to request it again here if appropriate, - // but usually, you'd request it ahead of time. - } + InAppReviewHelper.requestReviewInfo(this) + shouldPromptReview = InAppReviewHelper.onAppLaunched() } override fun onResume() { @@ -271,6 +232,19 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda serverListOffset = -dims.second.toFloat() serverListCurOffset = serverListOffset } + + // ───────────────────────────────────────────────────────────────────────── + // Only now, after UI is ready, do we fire the in‐app review if needed. + if (shouldPromptReview) { + lifecycleScope.launch(Dispatchers.Main) { + // Wait a small delay so we don’t interrupt initial load (e.g. 2 seconds). + delay(2_000) + InAppReviewHelper.showReviewIfPossible(this@MainActivity, reviewManager) + InAppReviewHelper.markReviewDone() + shouldPromptReview = false + } + } + // ───────────────────────────────────────────────────────────────────────── } @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @@ -778,17 +752,7 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda mFolderAdapter.update(projects) } - private fun validateAndShowReviewFlow() { - // show the review flow only if there is at least one server available and that server has at least one project/folder - val canShowReviewFlow = Space.current?.projects?.isNotEmpty() == true - if (canShowReviewFlow) { - showReviewFlow(this@MainActivity) - } else { - AppLogger.d("InAppReview", "Cannot show review flow: No projects available.") - Toast.makeText(this, "Cannot show review flow: No projects available.", Toast.LENGTH_LONG).show() - } - } private fun refreshCurrentProject() { val project = getSelectedProject() diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/InAppReviewHelper.kt b/app/src/main/java/net/opendasharchive/openarchive/util/InAppReviewHelper.kt new file mode 100644 index 00000000..fd32a6db --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/util/InAppReviewHelper.kt @@ -0,0 +1,97 @@ +package net.opendasharchive.openarchive.util + +import android.app.Activity +import android.content.Context +import com.google.android.play.core.review.ReviewException +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory +import net.opendasharchive.openarchive.core.logger.AppLogger + +object InAppReviewHelper { + // Keys for our Prefs helper: + private const val KEY_LAUNCH_COUNT = "launch_count" + private const val KEY_LAST_REVIEW_TIME = "last_review_time" + + // After this many launches, we become eligible: + private const val MIN_LAUNCHES = 5 + + // 30 days in ms + private const val THIRTY_DAYS_MS = 30L * 24 * 60 * 60 * 1000 + + // Once requestReviewFlow() succeeds, we cache this: + private var reviewInfo: ReviewInfo? = null + + /** + * Call once (e.g. in Application.onCreate or first Activity) so that Prefs.load(...) runs. + */ + fun init(context: Context) { + Prefs.load(context) + } + + /** + * Call early (e.g. in onCreate of MainActivity) to asynchronously fetch ReviewInfo. + */ + fun requestReviewInfo(context: Context) { + val manager: ReviewManager = ReviewManagerFactory.create(context) + manager.requestReviewFlow() + .addOnCompleteListener { task -> + if (task.isSuccessful) { + reviewInfo = task.result + AppLogger.d("InAppReview", "ReviewInfo obtained successfully.") + } else { + (task.exception as? ReviewException)?.let { ex -> + AppLogger.e("InAppReview", "Error requesting review flow: ${ex.errorCode}", ex) + } + reviewInfo = null + } + } + } + + /** + * Call this immediately on app launch (in onCreate). It increments the stored launch count + * and returns TRUE if we are now eligible to show a review (≥ MIN_LAUNCHES AND ≥ 30 days since last). + * + * It does NOT actually show anything. It only says “yes/no.” + */ + fun onAppLaunched(): Boolean { + val previousCount = Prefs.getInt(KEY_LAUNCH_COUNT, 0) + val newCount = previousCount + 1 + Prefs.putInt(KEY_LAUNCH_COUNT, newCount) + + val lastReviewTime = Prefs.getLong(KEY_LAST_REVIEW_TIME, 0L) + val now = System.currentTimeMillis() + + return if (newCount >= MIN_LAUNCHES && (now - lastReviewTime) >= THIRTY_DAYS_MS) { + true + } else { + false + } + } + + /** + * Once you decide it’s time to actually show the prompt (e.g. in onResume, after UI ready), + * call this. If reviewInfo is non-null it will launch; otherwise it just logs “no Info.” + */ + fun showReviewIfPossible(activity: Activity, reviewManager: ReviewManager) { + reviewInfo?.let { info -> + reviewManager.launchReviewFlow(activity, info) + .addOnCompleteListener { + AppLogger.d("InAppReview", "Review flow finished.") + reviewInfo = null + } + } ?: run { + AppLogger.d("InAppReview", "ReviewInfo was null; cannot launch review flow.") + } + } + + /** + * After you do showReviewIfPossible(...), call this to reset counters. + * That ensures we won’t prompt again for another 30 days. + */ + fun markReviewDone() { + val now = System.currentTimeMillis() + Prefs.putInt(KEY_LAUNCH_COUNT, 0) + Prefs.putLong(KEY_LAST_REVIEW_TIME, now) + } +}