From 9c50de9adf140d1edb4f158aa7e025cfc1e90eac Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sat, 13 Sep 2025 17:40:32 +0300 Subject: [PATCH 1/3] chore: remove tools namespace from localized strings --- .../java/ui/screens/main/MainActivity.java | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java index 43bab4b9..37f7d153 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java @@ -30,8 +30,11 @@ import androidx.navigation.ui.NavigationUI; import androidx.preference.PreferenceManager; +import com.d4rk.androidtutorials.java.BuildConfig; import com.d4rk.androidtutorials.java.R; import com.d4rk.androidtutorials.java.databinding.ActivityMainBinding; +import com.d4rk.androidtutorials.java.notifications.managers.AppUpdateNotificationsManager; +import com.d4rk.androidtutorials.java.notifications.managers.AppUsageNotificationsManager; import com.d4rk.androidtutorials.java.ui.components.navigation.BottomSheetMenuFragment; import com.d4rk.androidtutorials.java.ui.screens.startup.StartupActivity; import com.d4rk.androidtutorials.java.ui.screens.startup.StartupViewModel; @@ -48,6 +51,7 @@ 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 com.google.android.ump.ConsentInformation; import com.google.android.ump.ConsentRequestParameters; import com.google.android.ump.UserMessagingPlatform; @@ -92,6 +96,7 @@ public void onResume(@NonNull LifecycleOwner owner) { private NavController navController; private int currentNavIndex; private AppUpdateManager appUpdateManager; + private AppUpdateNotificationsManager appUpdateNotificationsManager; private InstallStateUpdatedListener installStateUpdatedListener; private long backPressedTime; @@ -134,6 +139,7 @@ protected void onCreate(Bundle savedInstanceState) { } this.appUpdateManager = mainViewModel.getAppUpdateManager(); + setupUpdateNotifications(); registerInstallStateListener(); getLifecycle().addObserver(lifecycleObserver); @@ -238,17 +244,19 @@ private void observeViewModel() { .build(); if (useRail) { - NavigationUI.setupWithNavController(mBinding.navRail, navController); - mBinding.navRail.setOnItemSelectedListener(item -> { - if (item.getItemId() == navController.getCurrentDestination().getId()) { + if (mBinding.navRail != null) { + NavigationUI.setupWithNavController(mBinding.navRail, navController); + mBinding.navRail.setOnItemSelectedListener(item -> { + if (item.getItemId() == navController.getCurrentDestination().getId()) { + return true; + } + int newIndex = navOrder.get(item.getItemId()); + NavOptions options = newIndex > currentNavIndex ? forwardOptions : backwardOptions; + navController.navigate(item.getItemId(), null, options); + currentNavIndex = newIndex; return true; - } - int newIndex = navOrder.get(item.getItemId()); - NavOptions options = newIndex > currentNavIndex ? forwardOptions : backwardOptions; - navController.navigate(item.getItemId(), null, options); - currentNavIndex = newIndex; - return true; - }); + }); + } } else { NavigationUI.setupWithNavController(navBarView, navController); navBarView.setOnItemSelectedListener(item -> { @@ -299,12 +307,47 @@ public boolean onOptionsItemSelected(android.view.MenuItem item) { return super.onOptionsItemSelected(item); } + @Override + protected void onResume() { + super.onResume(); + AppUsageNotificationsManager appUsageNotificationsManager = new AppUsageNotificationsManager(this); + appUsageNotificationsManager.scheduleAppUsageCheck(); + appUpdateNotificationsManager.checkAndSendUpdateNotification(); + checkForFlexibleOrImmediateUpdate(); + } + + private void checkForFlexibleOrImmediateUpdate() { + appUpdateManager.getAppUpdateInfo().addOnSuccessListener(appUpdateInfo -> { + boolean updateAvailable = appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE; + if (updateAvailable) { + startImmediateUpdate(appUpdateInfo); + } + }) + .addOnFailureListener(e -> { + if (!BuildConfig.DEBUG) { + Snackbar.make( + findViewById(android.R.id.content), + getString(R.string.snack_general_error), + Snackbar.LENGTH_LONG + ).show(); + } + }); + } + private void startImmediateUpdate(AppUpdateInfo appUpdateInfo) { - appUpdateManager.startUpdateFlowForResult( - appUpdateInfo, - updateActivityResultLauncher, - AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build() - ); + try { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + updateActivityResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build() + ); + } catch (Exception e) { + Log.e("MainActivity", "Error starting in-app update", e); + } + } + + private void setupUpdateNotifications() { + appUpdateNotificationsManager = new AppUpdateNotificationsManager(this); } private void registerInstallStateListener() { From 4fd4d9a17f36e2a3b786bfcd8a21d5bea035b462 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sat, 13 Sep 2025 17:49:16 +0300 Subject: [PATCH 2/3] Guard update notifications on Oreo --- .../androidtutorials/java/ui/screens/main/MainActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java index 37f7d153..6d16c9f4 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java @@ -312,7 +312,9 @@ protected void onResume() { super.onResume(); AppUsageNotificationsManager appUsageNotificationsManager = new AppUsageNotificationsManager(this); appUsageNotificationsManager.scheduleAppUsageCheck(); - appUpdateNotificationsManager.checkAndSendUpdateNotification(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + appUpdateNotificationsManager.checkAndSendUpdateNotification(); + } checkForFlexibleOrImmediateUpdate(); } From 135e1ad4ed1e80cfc52be703a458b24ffd069302 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sat, 13 Sep 2025 18:10:10 +0300 Subject: [PATCH 3/3] Add in-app review prompt after session threshold --- .../java/ui/screens/main/MainActivity.java | 20 +++++++- .../java/utils/ReviewHelper.java | 47 +++++++++++++++++++ app/src/main/res/values/keys.xml | 2 + 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/utils/ReviewHelper.java diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java index 6d16c9f4..f5722207 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java @@ -41,6 +41,7 @@ import com.d4rk.androidtutorials.java.ui.screens.support.SupportActivity; import com.d4rk.androidtutorials.java.utils.ConsentUtils; import com.d4rk.androidtutorials.java.utils.EdgeToEdgeDelegate; +import com.d4rk.androidtutorials.java.utils.ReviewHelper; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.MobileAds; import com.google.android.material.navigation.NavigationBarView; @@ -156,6 +157,8 @@ public void handleOnBackPressed() { } } }); + + checkInAppReview(); } private void setupActionBar() { @@ -333,7 +336,22 @@ private void checkForFlexibleOrImmediateUpdate() { Snackbar.LENGTH_LONG ).show(); } - }); + }); + } + + private void checkInAppReview() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + int sessionCount = prefs.getInt(getString(R.string.key_session_count), 0); + boolean hasPrompted = prefs.getBoolean(getString(R.string.key_has_prompted_review), false); + + ReviewHelper.launchInAppReviewIfEligible( + this, + sessionCount, + hasPrompted, + () -> prefs.edit().putBoolean(getString(R.string.key_has_prompted_review), true).apply() + ); + + prefs.edit().putInt(getString(R.string.key_session_count), sessionCount + 1).apply(); } private void startImmediateUpdate(AppUpdateInfo appUpdateInfo) { diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/utils/ReviewHelper.java b/app/src/main/java/com/d4rk/androidtutorials/java/utils/ReviewHelper.java new file mode 100644 index 00000000..53036953 --- /dev/null +++ b/app/src/main/java/com/d4rk/androidtutorials/java/utils/ReviewHelper.java @@ -0,0 +1,47 @@ +package com.d4rk.androidtutorials.java.utils; + +import android.app.Activity; + +import com.google.android.play.core.review.ReviewInfo; +import com.google.android.play.core.review.ReviewManager; +import com.google.android.play.core.review.ReviewManagerFactory; + +/** + * Utility class for launching Google Play in-app reviews. + */ +public final class ReviewHelper { + + private ReviewHelper() { + // Utility class + } + + public static void launchInAppReviewIfEligible(Activity activity, + int sessionCount, + boolean hasPromptedBefore, + Runnable onReviewLaunched) { + if (sessionCount < 3 || hasPromptedBefore) { + return; + } + launchReview(activity, onReviewLaunched); + } + + public static void forceLaunchInAppReview(Activity activity) { + launchReview(activity, null); + } + + private static void launchReview(Activity activity, Runnable onReviewLaunched) { + ReviewManager reviewManager = ReviewManagerFactory.create(activity); + reviewManager.requestReviewFlow() + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + ReviewInfo reviewInfo = task.getResult(); + reviewManager.launchReviewFlow(activity, reviewInfo) + .addOnCompleteListener(flow -> { + if (onReviewLaunched != null) { + onReviewLaunched.run(); + } + }); + } + }); + } +} diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 2ad5e922..1612118c 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -21,4 +21,6 @@ consent_ad_user_data consent_ad_personalization onboarding_complete + session_count + has_prompted_review \ No newline at end of file