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 8b364488..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 @@ -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 @@ -78,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 { @@ -129,6 +140,8 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda private lateinit var permissionManager: PermissionManager + private lateinit var reviewManager: ReviewManager + private var shouldPromptReview = false override fun onCreate(savedInstanceState: Bundle?) { ///enableEdgeToEdge() @@ -165,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() @@ -196,6 +212,10 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda // You can start the upload service or update the UI accordingly. UploadService.startUploadService(this) } + + reviewManager = ReviewManagerFactory.create(this) + InAppReviewHelper.requestReviewInfo(this) + shouldPromptReview = InAppReviewHelper.onAppLaunched() } override fun onResume() { @@ -212,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) @@ -719,6 +752,8 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda mFolderAdapter.update(projects) } + + 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) + } +}