From a21ddae8058288ce993819db0fe4a6b9d0568f69 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Tue, 24 Oct 2023 14:53:00 +0300 Subject: [PATCH 01/92] Remove unnecessary logs from domain screen in the site creation flow --- .../ui/sitecreation/domains/SiteCreationDomainsViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt index 9b8b5261fea3..562f34defa1f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt @@ -122,7 +122,6 @@ class SiteCreationDomainsViewModel @Inject constructor( } else -> { - AppLog.d(AppLog.T.DOMAIN_REGISTRATION, result.products.toString()) products = result.products.orEmpty().associateBy { it.productId } } } From d766d210d1837df8078e05f33f639112f5f33f4d Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Fri, 17 May 2024 19:42:48 -0700 Subject: [PATCH 02/92] Add null check --- .../wordpress/android/ui/reader/ReaderActivityLauncher.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index 32b2261e2527..dc7a3210daa9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -234,6 +234,9 @@ public static void showReaderComments( */ public static void showReaderComments(Context context, long blogId, long postId, DirectOperation directOperation, long commentId, String interceptedUri, String source) { + if (context == null) { + return; + } Intent intent = buildShowReaderCommentsIntent( context, blogId, @@ -257,6 +260,9 @@ public static void showReaderCommentsForResult( public static void showReaderCommentsForResult(Fragment fragment, long blogId, long postId, DirectOperation directOperation, long commentId, String interceptedUri, String source) { + if (fragment.getContext() == null) { + return; + } Intent intent = buildShowReaderCommentsIntent( fragment.getContext(), blogId, From fb5fa519c0bfe3e2783205af444d265746c1d455 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 20 May 2024 16:03:24 +0200 Subject: [PATCH 03/92] Gutenberg - Add onVoiceToContent --- .../android/editor/gutenberg/GutenbergContainerFragment.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java index 3084e5ad16ee..42037d098c9b 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java @@ -311,6 +311,10 @@ public void onRedoPressed() { mWPAndroidGlueCode.onRedoPressed(); } + public void onVoiceToContent(String content) { + mWPAndroidGlueCode.onVoiceToContent(content); + } + public void updateCapabilities(GutenbergPropsBuilder gutenbergPropsBuilder) { // We want to make sure that activity isn't null // as it can make this crash to happen: https://github.com/wordpress-mobile/WordPress-Android/issues/13248 From d3a8bf04fb542319398df450094d31cd334ff4f8 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 20 May 2024 16:03:42 +0200 Subject: [PATCH 04/92] GutenbergEditorFragment - Call onVoiceToContent with test content --- .../android/editor/gutenberg/GutenbergEditorFragment.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index c8dc71940370..d7c91960e316 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -407,6 +407,9 @@ public void onEditorDidMount(ArrayList unsupportedBlocks) { @Override public void run() { setEditorProgressBarVisibility(!mEditorDidMount); + + getGutenbergContainerFragment().onVoiceToContent( + "# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n - Duis sagittis ipsum\n## Conclusion\n- Praesent libero\n- Sed cursus ante dapibus diam\n- Suspendisse malesuada lacus ex"); } }); } From e0145b44648e392051644437db7e87345918e8f9 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 20 May 2024 16:22:32 +0200 Subject: [PATCH 05/92] Update Gutenberg ref --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c07a8e74de6a..fda864a197c1 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ ext { automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' automatticTracksVersion = '5.0.0' - gutenbergMobileVersion = 'v1.119.0-alpha2' + gutenbergMobileVersion = '6878-988f4ea8258afce00fb01640d3368bb4117e33f2' wordPressAztecVersion = 'v2.1.3' wordPressFluxCVersion = '2.79.0' wordPressLoginVersion = '1.15.0' From f6a95341c383c9cb0a305a8b11b2790d7dd00fe5 Mon Sep 17 00:00:00 2001 From: Andy Valdez Date: Mon, 20 May 2024 15:57:29 -0400 Subject: [PATCH 06/92] Set targetSdk to 34. --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c07a8e74de6a..018f0f765a69 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ plugins { ext { minSdkVersion = 24 compileSdkVersion = 34 - targetSdkVersion = 33 + targetSdkVersion = 34 } ext { @@ -23,7 +23,7 @@ ext { automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' automatticTracksVersion = '5.0.0' - gutenbergMobileVersion = 'v1.119.0-alpha2' + gutenbergMobileVersion = '6870-76281ef23dfd66f1268e18a4be9003a46967e4f8' wordPressAztecVersion = 'v2.1.3' wordPressFluxCVersion = '2.79.0' wordPressLoginVersion = '1.15.0' From c7f5bceaffbd9281be7c64a6a9d89ba40d155263 Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Tue, 21 May 2024 22:35:24 -0700 Subject: [PATCH 07/92] Add NotNull annotations to ReaderActivityLauncher.java --- .../android/ui/reader/ReaderActivityLauncher.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index dc7a3210daa9..bf08fd042a88 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -13,6 +13,7 @@ import androidx.core.app.ActivityOptionsCompat; import androidx.fragment.app.Fragment; +import org.jetbrains.annotations.NotNull; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.models.ReaderPost; import org.wordpress.android.models.ReaderTag; @@ -193,7 +194,7 @@ public static Intent createReaderSearchIntent(@NonNull final Context context) { /* * show comments for the passed Ids */ - public static void showReaderComments(Context context, + public static void showReaderComments(@NonNull Context context, long blogId, long postId, String source) { @@ -205,7 +206,7 @@ public static void showReaderComments(Context context, * show specific comment for the passed Ids */ public static void showReaderComments( - Context context, + @NonNull Context context, long blogId, long postId, long commentId, @@ -232,11 +233,8 @@ public static void showReaderComments( * @param commentId specific comment id to perform an action on * @param interceptedUri URI to fall back into (i.e. to be able to open in external browser) */ - public static void showReaderComments(Context context, long blogId, long postId, DirectOperation + public static void showReaderComments(@NonNull Context context, long blogId, long postId, DirectOperation directOperation, long commentId, String interceptedUri, String source) { - if (context == null) { - return; - } Intent intent = buildShowReaderCommentsIntent( context, blogId, @@ -258,7 +256,7 @@ public static void showReaderCommentsForResult( showReaderCommentsForResult(fragment, blogId, postId, null, 0, null, source); } - public static void showReaderCommentsForResult(Fragment fragment, long blogId, long postId, DirectOperation + public static void showReaderCommentsForResult(@NotNull Fragment fragment, long blogId, long postId, DirectOperation directOperation, long commentId, String interceptedUri, String source) { if (fragment.getContext() == null) { return; @@ -275,7 +273,7 @@ public static void showReaderCommentsForResult(Fragment fragment, long blogId, l fragment.startActivityForResult(intent, RequestCodes.READER_FOLLOW_CONVERSATION); } - private static Intent buildShowReaderCommentsIntent(Context context, long blogId, long postId, DirectOperation + private static Intent buildShowReaderCommentsIntent(@NonNull Context context, long blogId, long postId, DirectOperation directOperation, long commentId, String interceptedUri, String source) { Intent intent = new Intent( context, From 9c1aabf23197e250e84aa180f7b552d083a171ca Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Tue, 21 May 2024 17:01:46 +0300 Subject: [PATCH 08/92] Call onPublishingPost() function for only the first publishing --- .../wordpress/android/ui/main/WPMainActivity.java | 12 ++++++++---- .../wordpress/android/ui/posts/PostsListActivity.kt | 4 +++- .../wordpress/android/ui/review/ReviewViewModel.kt | 7 +++++-- .../android/ui/review/ReviewViewModelTest.kt | 6 +++--- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index d15191ccd33d..b463f9e68ed9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -40,8 +40,6 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.BuildConfig; -import org.wordpress.android.inappupdate.InAppUpdateListener; -import org.wordpress.android.inappupdate.IInAppUpdateManager; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; @@ -71,6 +69,8 @@ import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged; import org.wordpress.android.fluxc.store.SiteStore.OnSiteEditorsChanged; import org.wordpress.android.fluxc.store.SiteStore.OnSiteRemoved; +import org.wordpress.android.inappupdate.IInAppUpdateManager; +import org.wordpress.android.inappupdate.InAppUpdateListener; import org.wordpress.android.login.LoginAnalyticsListener; import org.wordpress.android.networking.ConnectionChangeReceiver; import org.wordpress.android.push.GCMMessageHandler; @@ -1412,7 +1412,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { v -> UploadUtils.publishPost(WPMainActivity.this, post, site, mDispatcher), isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(site.getId(), isFirstTimePublishing); - mReviewViewModel.onPublishingPost(isFirstTimePublishing); + if (isFirstTimePublishing) { + mReviewViewModel.onPublishingPost(); + } } ); } @@ -1820,7 +1822,9 @@ public void onPostUploaded(OnPostUploaded event) { targetSite, isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing); - mReviewViewModel.onPublishingPost(isFirstTimePublishing); + if (isFirstTimePublishing) { + mReviewViewModel.onPublishingPost(); + } } ); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index c89268b5853a..1fe521200dd6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -381,7 +381,9 @@ class PostsListActivity : LocaleAwareActivity(), ) { isFirstTimePublishing -> changeTabsOnPostUpload() bloggingRemindersViewModel.onPublishingPost(site.id, isFirstTimePublishing) - reviewViewModel.onPublishingPost(isFirstTimePublishing) + if (isFirstTimePublishing) { + reviewViewModel.onPublishingPost() + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt index c54010baac66..8aba13327cfc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt @@ -8,12 +8,15 @@ import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.viewmodel.Event import javax.inject.Inject +/** + * Manages the logic for the flow of in-app reviews prompt. + */ class ReviewViewModel @Inject constructor(private val appPrefsWrapper: AppPrefsWrapper) : ViewModel() { private val _launchReview = MutableLiveData>() val launchReview = _launchReview as LiveData> - fun onPublishingPost(isFirstTimePublishing: Boolean) { - if (!appPrefsWrapper.isInAppReviewsShown() && isFirstTimePublishing) { + fun onPublishingPost() { + if (!appPrefsWrapper.isInAppReviewsShown()) { if (appPrefsWrapper.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { appPrefsWrapper.incrementPublishedPostCount() } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt index dc83739ec55d..4d2c00860c4e 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt @@ -37,7 +37,7 @@ class ReviewViewModelTest { whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(false) whenever(appPrefsWrapper.getPublishedPostCount()).thenReturn(ReviewViewModel.TARGET_COUNT_POST_PUBLISHED - 1) - viewModel.onPublishingPost(true) + viewModel.onPublishingPost() assertEquals(events.size, 0) } @@ -46,7 +46,7 @@ class ReviewViewModelTest { fun onPublishingPost_whenInAppReviewsAlreadyShown_doNotLaunchInAppReviews() { whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(true) - viewModel.onPublishingPost(true) + viewModel.onPublishingPost() assertEquals(events.size, 0) } @@ -56,7 +56,7 @@ class ReviewViewModelTest { whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(false) whenever(appPrefsWrapper.getPublishedPostCount()).thenReturn(ReviewViewModel.TARGET_COUNT_POST_PUBLISHED) - viewModel.onPublishingPost(true) + viewModel.onPublishingPost() // Verify `launchReview` is triggered. assertEquals(Unit, events.last()) From 3f4b66fa584453853452d3ebbd4436b4b4a7d1cb Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Tue, 21 May 2024 17:07:07 +0300 Subject: [PATCH 09/92] Reset in-app reviews flow when positive button of rate dialog tapped --- .../org/wordpress/android/ui/prefs/AppPrefs.java | 4 ++++ .../wordpress/android/ui/prefs/AppPrefsWrapper.kt | 3 +++ .../wordpress/android/widgets/AppRatingDialog.kt | 14 +++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index 5bf4bdb7824b..4fce63f91b96 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -1302,6 +1302,10 @@ public static void incrementPublishedPostCount() { putInt(DeletablePrefKey.PUBLISHED_POST_COUNT, getPublishedPostCount() + 1); } + public static void resetPublishedPostCount() { + putInt(DeletablePrefKey.PUBLISHED_POST_COUNT, 0); + } + public static int getPublishedPostCount() { return prefs().getInt(DeletablePrefKey.PUBLISHED_POST_COUNT.name(), 0); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index 4a835daca578..3a0b4e5afafb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -196,6 +196,9 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra fun incrementPublishedPostCount() { AppPrefs.incrementPublishedPostCount() } + fun resetPublishedPostCount() { + AppPrefs.resetPublishedPostCount() + } fun getPublishedPostCount(): Int { return AppPrefs.getPublishedPostCount() diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index b57e67bccab9..84a50f405091 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import java.util.Date @@ -147,8 +148,11 @@ object AppRatingDialog { ) } - setOptOut(true) + setOptOut() AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_RATED_APP) + + // Reset the published post counter of in-app reviews prompt flow. + AppPrefs.resetPublishedPostCount() } .setNeutralButton(R.string.app_rating_rate_later) { _, _ -> clearSharedPreferences() @@ -156,7 +160,7 @@ object AppRatingDialog { AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_DECIDED_TO_RATE_LATER) } .setNegativeButton(R.string.app_rating_rate_never) { _, _ -> - setOptOut(true) + setOptOut() AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_DECLINED_TO_RATE_APP) } return builder.create() @@ -178,11 +182,11 @@ object AppRatingDialog { } /** - * Set opt out flag - when true, the rate dialog will never be shown unless app data is cleared. + * Set opt out flag - the rate dialog will never be shown unless app data is cleared. */ - private fun setOptOut(optOut: Boolean) { + private fun setOptOut() { preferences.edit().putBoolean(KEY_OPT_OUT, optOut)?.apply() - this.optOut = optOut + this.optOut = true } /** From e179b3fdb434ef6a492cbbf85bd5b57fe801e989 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Tue, 21 May 2024 17:27:29 +0300 Subject: [PATCH 10/92] Reset the in-app reviews counter when the rating dialog is shown --- .../main/java/org/wordpress/android/widgets/AppRatingDialog.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index 84a50f405091..93a5a990ada4 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -114,6 +114,9 @@ object AppRatingDialog { dialog = AppRatingDialog() dialog.show(fragmentManger, AppRatingDialog.TAG_APP_RATING_PROMPT_DIALOG) AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_SAW_PROMPT) + + // Reset the published post counter of in-app reviews prompt flow. + AppPrefs.resetPublishedPostCount() } } From d4c03f5e071b1f271bb838cda0c21db63e0b2f15 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Thu, 23 May 2024 16:39:38 +0300 Subject: [PATCH 11/92] Don't show the in-app reviews prompt if the rating dialog is denied --- .../main/java/org/wordpress/android/widgets/AppRatingDialog.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index 93a5a990ada4..a08332854f9c 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -165,6 +165,8 @@ object AppRatingDialog { .setNegativeButton(R.string.app_rating_rate_never) { _, _ -> setOptOut() AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_DECLINED_TO_RATE_APP) + + AppPrefs.setInAppReviewsShown() } return builder.create() } From 39c535973cc7aa2717c5d953026fd8e458c009fb Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Thu, 23 May 2024 16:53:46 +0300 Subject: [PATCH 12/92] Wait to show in-app reviews prompt if ask later is tapped --- .../java/org/wordpress/android/ui/review/ReviewViewModel.kt | 5 ++++- .../java/org/wordpress/android/widgets/AppRatingDialog.kt | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt index 8aba13327cfc..019d8aefde6a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.viewmodel.Event +import org.wordpress.android.widgets.AppRatingDialog +import java.util.Date import javax.inject.Inject /** @@ -16,7 +18,8 @@ class ReviewViewModel @Inject constructor(private val appPrefsWrapper: AppPrefsW val launchReview = _launchReview as LiveData> fun onPublishingPost() { - if (!appPrefsWrapper.isInAppReviewsShown()) { + val shouldWaitAskLaterTime = Date().time - AppRatingDialog.askLaterDate.time < AppRatingDialog.criteriaInstallMs + if (!appPrefsWrapper.isInAppReviewsShown() && !shouldWaitAskLaterTime) { if (appPrefsWrapper.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { appPrefsWrapper.incrementPublishedPostCount() } diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index a08332854f9c..7f9d40f88dfc 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -30,6 +30,7 @@ object AppRatingDialog { // app must have been installed this long before the rating dialog will appear private const val CRITERIA_INSTALL_DAYS: Int = 7 + val criteriaInstallMs = TimeUnit.DAYS.toMillis(CRITERIA_INSTALL_DAYS.toLong()) // app must have been launched this many times before the rating dialog will appear private const val CRITERIA_LAUNCH_TIMES: Int = 10 @@ -38,7 +39,7 @@ object AppRatingDialog { private const val CRITERIA_INTERACTIONS: Int = 10 private var installDate = Date() - private var askLaterDate = Date() + var askLaterDate = Date() private var launchTimes = 0 private var interactions = 0 private var optOut = false @@ -103,8 +104,7 @@ object AppRatingDialog { return if (optOut or (launchTimes < CRITERIA_LAUNCH_TIMES) or (interactions < CRITERIA_INTERACTIONS)) { false } else { - val thresholdMs = TimeUnit.DAYS.toMillis(CRITERIA_INSTALL_DAYS.toLong()) - Date().time - installDate.time >= thresholdMs && Date().time - askLaterDate.time >= thresholdMs + Date().time - installDate.time >= criteriaInstallMs && Date().time - askLaterDate.time >= criteriaInstallMs } } From 1357c39637d127dc9da5aa8410352ced646bb52a Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Thu, 23 May 2024 18:12:47 +0300 Subject: [PATCH 13/92] Reset counter and wait for 7 days after in-app review prompt is shown --- .../wordpress/android/ui/prefs/AppPrefs.java | 9 -------- .../android/ui/prefs/AppPrefsWrapper.kt | 8 ------- .../android/ui/review/ReviewViewModel.kt | 8 +++++-- .../android/widgets/AppRatingDialog.kt | 22 ++++++++++++++++++- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index 4fce63f91b96..e53b18a38d03 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -164,7 +164,6 @@ public enum DeletablePrefKey implements PrefKey { PINNED_DYNAMIC_CARD, // PUBLISHED_POST_COUNT will increase until it reaches ReviewViewModel.TARGET_COUNT_POST_PUBLISHED PUBLISHED_POST_COUNT, - IN_APP_REVIEW_SHOWN, BLOGGING_REMINDERS_SHOWN, SHOULD_SCHEDULE_CREATE_SITE_NOTIFICATION, SHOULD_SHOW_WEEKLY_ROUNDUP_NOTIFICATION, @@ -1310,14 +1309,6 @@ public static int getPublishedPostCount() { return prefs().getInt(DeletablePrefKey.PUBLISHED_POST_COUNT.name(), 0); } - public static void setInAppReviewsShown() { - putBoolean(DeletablePrefKey.IN_APP_REVIEW_SHOWN, true); - } - - public static boolean isInAppReviewsShown() { - return prefs().getBoolean(DeletablePrefKey.IN_APP_REVIEW_SHOWN.name(), false); - } - public static void setBloggingRemindersShown(int siteId) { prefs().edit().putBoolean(getBloggingRemindersConfigKey(siteId), true).apply(); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index 3a0b4e5afafb..4cf5275385f3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -204,14 +204,6 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra return AppPrefs.getPublishedPostCount() } - fun setInAppReviewsShown() { - AppPrefs.setInAppReviewsShown() - } - - fun isInAppReviewsShown(): Boolean { - return AppPrefs.isInAppReviewsShown() - } - fun setBloggingRemindersShown(siteId: Int) { AppPrefs.setBloggingRemindersShown(siteId) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt index 019d8aefde6a..29150b67dee7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt @@ -18,14 +18,18 @@ class ReviewViewModel @Inject constructor(private val appPrefsWrapper: AppPrefsW val launchReview = _launchReview as LiveData> fun onPublishingPost() { + val shouldWaitForLastShownTime = + Date().time - AppRatingDialog.inAppReviewsShownDate.time < AppRatingDialog.criteriaInstallMs val shouldWaitAskLaterTime = Date().time - AppRatingDialog.askLaterDate.time < AppRatingDialog.criteriaInstallMs - if (!appPrefsWrapper.isInAppReviewsShown() && !shouldWaitAskLaterTime) { + if (!AppRatingDialog.doNotShowInAppReviewsPrompt && !shouldWaitAskLaterTime && !shouldWaitForLastShownTime) { if (appPrefsWrapper.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { appPrefsWrapper.incrementPublishedPostCount() } if (appPrefsWrapper.getPublishedPostCount() == TARGET_COUNT_POST_PUBLISHED) { _launchReview.value = Event(Unit) - appPrefsWrapper.setInAppReviewsShown() + + AppRatingDialog.storeInAppReviewsShownDate() + appPrefsWrapper.resetPublishedPostCount() } } } diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index 7f9d40f88dfc..6d1aa8f94bad 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -27,6 +27,8 @@ object AppRatingDialog { private const val KEY_OPT_OUT = "rate_opt_out" private const val KEY_ASK_LATER_DATE = "rate_ask_later_date" private const val KEY_INTERACTIONS = "rate_interactions" + private const val IN_APP_REVIEWS_SHOWN_DATE = "in_app_reviews_shown_date" + private const val DO_NOT_SHOW_IN_APP_REVIEWS_PROMPT = "do_not_show_in_app_reviews_prompt" // app must have been installed this long before the rating dialog will appear private const val CRITERIA_INSTALL_DAYS: Int = 7 @@ -43,6 +45,8 @@ object AppRatingDialog { private var launchTimes = 0 private var interactions = 0 private var optOut = false + var inAppReviewsShownDate = Date(0) + var doNotShowInAppReviewsPrompt = false private lateinit var preferences: SharedPreferences @@ -68,6 +72,9 @@ object AppRatingDialog { optOut = preferences.getBoolean(KEY_OPT_OUT, false) installDate = Date(preferences.getLong(KEY_INSTALL_DATE, 0)) askLaterDate = Date(preferences.getLong(KEY_ASK_LATER_DATE, 0)) + + inAppReviewsShownDate = Date(preferences.getLong(IN_APP_REVIEWS_SHOWN_DATE, 0)) + doNotShowInAppReviewsPrompt = preferences.getBoolean(DO_NOT_SHOW_IN_APP_REVIEWS_PROMPT, false) } /** @@ -166,7 +173,7 @@ object AppRatingDialog { setOptOut() AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_DECLINED_TO_RATE_APP) - AppPrefs.setInAppReviewsShown() + doNotShowInAppReviewsPromptAgain() } return builder.create() } @@ -194,6 +201,13 @@ object AppRatingDialog { this.optOut = true } + /** + * Set do not show in-app reviews prompt flag - the in-app reviews prompt will never be shown unless app data is + * cleared. + */ + private fun doNotShowInAppReviewsPromptAgain() = + preferences.edit().putBoolean(DO_NOT_SHOW_IN_APP_REVIEWS_PROMPT, optOut)?.apply() + /** * Store install date - retrieved from package manager if possible. */ @@ -216,4 +230,10 @@ object AppRatingDialog { val nextAskDate = System.currentTimeMillis() preferences.edit().putLong(KEY_ASK_LATER_DATE, nextAskDate)?.apply() } + + /** + * Store the date the in-app reviews prompt is attempted to launch. + */ + fun storeInAppReviewsShownDate() = + preferences.edit().putLong(IN_APP_REVIEWS_SHOWN_DATE, System.currentTimeMillis())?.apply() } From 3329d74168b433df69d6e2f9a0438b5dc53f2257 Mon Sep 17 00:00:00 2001 From: Andy Valdez Date: Thu, 23 May 2024 16:04:59 -0400 Subject: [PATCH 14/92] [Bug] Remove flow and use callbacks instead. I know, pain. --- .../ui/barcodescanner/BarcodeScanner.kt | 50 ++++++------- .../ui/barcodescanner/BarcodeScannerScreen.kt | 3 +- .../barcodescanner/BarcodeScanningFragment.kt | 14 +--- .../android/ui/barcodescanner/CodeScanner.kt | 7 +- .../barcodescanner/GoogleMLKitCodeScanner.kt | 74 ++++++++----------- 5 files changed, 64 insertions(+), 84 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt index e5df791ee239..53eaa1b35d01 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt @@ -18,21 +18,20 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf import org.wordpress.android.ui.compose.theme.AppTheme import androidx.camera.core.Preview as CameraPreview @Composable fun BarcodeScanner( codeScanner: CodeScanner, - onScannedResult: (Flow) -> Unit + onScannedResult: CodeScannerCallback ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } + Column( modifier = Modifier.fillMaxSize() ) { @@ -51,30 +50,27 @@ fun BarcodeScanner( .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) .build() imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy -> - onScannedResult(codeScanner.startScan(imageProxy)) + val callback = object : CodeScannerCallback { + override fun run(status: CodeScannerStatus?) { + status?.let { onScannedResult.run(it) } + } + } + codeScanner.startScan(imageProxy, callback) } try { cameraProviderFuture.get().bindToLifecycle(lifecycleOwner, selector, preview, imageAnalysis) } catch (e: IllegalStateException) { - onScannedResult( - flowOf( - CodeScannerStatus.Failure( - e.message - ?: "Illegal state exception while binding camera provider to lifecycle", - CodeScanningErrorType.Other(e) - ) - ) - ) + onScannedResult.run(CodeScannerStatus.Failure( + e.message + ?: "Illegal state exception while binding camera provider to lifecycle", + CodeScanningErrorType.Other(e) + )) } catch (e: IllegalArgumentException) { - onScannedResult( - flowOf( - CodeScannerStatus.Failure( - e.message - ?: "Illegal argument exception while binding camera provider to lifecycle", - CodeScanningErrorType.Other(e) - ) - ) - ) + onScannedResult.run(CodeScannerStatus.Failure( + e.message + ?: "Illegal argument exception while binding camera provider to lifecycle", + CodeScanningErrorType.Other(e) + )) } previewView }, @@ -84,8 +80,8 @@ fun BarcodeScanner( } class DummyCodeScanner : CodeScanner { - override fun startScan(imageProxy: ImageProxy): Flow { - return flowOf(CodeScannerStatus.Success("", GoogleBarcodeFormatMapper.BarcodeFormat.FormatUPCA)) + override fun startScan(imageProxy: ImageProxy, callback: CodeScannerCallback) { + callback.run(CodeScannerStatus.Success("", GoogleBarcodeFormatMapper.BarcodeFormat.FormatUPCA)) } } @@ -94,6 +90,10 @@ class DummyCodeScanner : CodeScanner { @Composable private fun BarcodeScannerScreenPreview() { AppTheme { - BarcodeScanner(codeScanner = DummyCodeScanner(), onScannedResult = {}) + BarcodeScanner(codeScanner = DummyCodeScanner(), onScannedResult = object : CodeScannerCallback { + override fun run(status: CodeScannerStatus?) { + // no-ops + } + }) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt index 01a270db9ece..9960cd675da0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.R -import kotlinx.coroutines.flow.Flow import org.wordpress.android.ui.compose.theme.AppTheme @Composable @@ -23,7 +22,7 @@ fun BarcodeScannerScreen( codeScanner: CodeScanner, permissionState: BarcodeScanningViewModel.PermissionState, onResult: (Boolean) -> Unit, - onScannedResult: (Flow) -> Unit, + onScannedResult: CodeScannerCallback, ) { val cameraPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt index 73b5f70c0068..434a5a4d8286 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt @@ -12,11 +12,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.util.WPPermissionUtils import javax.inject.Inject @@ -52,12 +48,10 @@ class BarcodeScanningFragment : Fragment() { shouldShowRequestPermissionRationale(KEY_CAMERA_PERMISSION) ) }, - onScannedResult = { codeScannerStatus -> - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - codeScannerStatus.collect { status -> - setResultAndPopStack(status) - } + onScannedResult = object : CodeScannerCallback { + override fun run(status: CodeScannerStatus?) { + if (status != null) { + setResultAndPopStack(status) } } }, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt index 6dde760e0bf0..bcecd01735e4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt @@ -2,11 +2,14 @@ package org.wordpress.android.ui.barcodescanner import android.os.Parcelable import androidx.camera.core.ImageProxy -import kotlinx.coroutines.flow.Flow import kotlinx.parcelize.Parcelize interface CodeScanner { - fun startScan(imageProxy: ImageProxy): Flow + fun startScan(imageProxy: ImageProxy, callback: CodeScannerCallback) +} + +interface CodeScannerCallback { + fun run(status: CodeScannerStatus?) } sealed class CodeScannerStatus : Parcelable { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt index 2e0d339c473e..428bbc5abef0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt @@ -3,10 +3,6 @@ package org.wordpress.android.ui.barcodescanner import androidx.camera.core.ImageProxy import com.google.mlkit.vision.barcode.BarcodeScanner import com.google.mlkit.vision.barcode.common.Barcode -import kotlinx.coroutines.channels.ProducerScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import javax.inject.Inject class GoogleMLKitCodeScanner @Inject constructor( @@ -17,53 +13,41 @@ class GoogleMLKitCodeScanner @Inject constructor( ) : CodeScanner { private var barcodeFound = false @androidx.camera.core.ExperimentalGetImage - override fun startScan(imageProxy: ImageProxy): Flow { - return callbackFlow { - val barcodeTask = barcodeScanner.process(inputImageProvider.provideImage(imageProxy)) - barcodeTask.addOnCompleteListener { - // We must call image.close() on received images when finished using them. - // Otherwise, new images may not be received or the camera may stall. - imageProxy.close() - } - barcodeTask.addOnSuccessListener { barcodeList -> - // The check for barcodeFound is done because the startScan method will be called - // continuously by the library as long as we are in the scanning screen. - // There will be a good chance that the same barcode gets identified multiple times and as a result - // success callback will be called multiple times. - if (!barcodeList.isNullOrEmpty() && !barcodeFound) { - barcodeFound = true - handleScanSuccess(barcodeList.firstOrNull()) - this@callbackFlow.close() - } - } - barcodeTask.addOnFailureListener { exception -> - this@callbackFlow.trySend( - CodeScannerStatus.Failure( - error = exception.message, - type = errorMapper.mapGoogleMLKitScanningErrors(exception) - ) - ) - this@callbackFlow.close() + override fun startScan(imageProxy: ImageProxy, callback: CodeScannerCallback) { + val barcodeTask = barcodeScanner.process(inputImageProvider.provideImage(imageProxy)) + barcodeTask.addOnCompleteListener { + // We must call image.close() on received images when finished using them. + // Otherwise, new images may not be received or the camera may stall. + imageProxy.close() + } + barcodeTask.addOnSuccessListener { barcodeList -> + // The check for barcodeFound is done because the startScan method will be called + // continuously by the library as long as we are in the scanning screen. + // There will be a good chance that the same barcode gets identified multiple times and as a result + // success callback will be called multiple times. + if (!barcodeList.isNullOrEmpty() && !barcodeFound) { + barcodeFound = true + callback.run(handleScanSuccess(barcodeList.firstOrNull())) } - - awaitClose() + } + barcodeTask.addOnFailureListener { exception -> + callback.run(CodeScannerStatus.Failure( + error = exception.message, + type = errorMapper.mapGoogleMLKitScanningErrors(exception) + )) } } - private fun ProducerScope.handleScanSuccess(code: Barcode?) { - code?.rawValue?.let { - trySend( - CodeScannerStatus.Success( - it, - barcodeFormatMapper.mapBarcodeFormat(code.format) - ) + private fun handleScanSuccess(code: Barcode?): CodeScannerStatus { + return code?.rawValue?.let { + CodeScannerStatus.Success( + it, + barcodeFormatMapper.mapBarcodeFormat(code.format) ) } ?: run { - trySend( - CodeScannerStatus.Failure( - error = "Failed to find a valid raw value!", - type = CodeScanningErrorType.Other(Throwable("Empty raw value")) - ) + CodeScannerStatus.Failure( + error = "Failed to find a valid raw value!", + type = CodeScanningErrorType.Other(Throwable("Empty raw value")) ) } } From cb616f0259173c00c8b868df19ff96497b6e0030 Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Thu, 23 May 2024 16:33:08 -0700 Subject: [PATCH 15/92] Add null checks to calling functions --- .../NotificationsDetailListFragment.kt | 14 +++++++++----- .../NotificationsListFragmentPage.kt | 16 +++++++++------- .../ui/reader/ReaderPostDetailFragment.kt | 14 +++++++++----- .../ui/reader/discover/ReaderDiscoverFragment.kt | 14 ++++++++------ 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt index 101f2be19bfb..4f11a909ac06 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt @@ -13,6 +13,7 @@ import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.ListView +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.ListFragment import androidx.lifecycle.lifecycleScope import com.airbnb.lottie.LottieAnimationView @@ -255,11 +256,14 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { } requireNotNull(notification).let { note -> - ReaderActivityLauncher.showReaderComments( - activity, note.siteId.toLong(), note.postId.toLong(), - note.commentId, - COMMENT_NOTIFICATION.sourceDescription - ) + val maybeActivity: FragmentActivity? = activity + maybeActivity?.let { fragmentActivity -> + ReaderActivityLauncher.showReaderComments( + fragmentActivity, note.siteId.toLong(), note.postId.toLong(), + note.commentId, + COMMENT_NOTIFICATION.sourceDescription + ) + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt index b237f0669151..3315ff77fe6f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -225,13 +225,15 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l viewModel.openNote( noteId, { siteId, postId, commentId -> - ReaderActivityLauncher.showReaderComments( - activity, - siteId, - postId, - commentId, - ThreadedCommentsActionSource.COMMENT_NOTIFICATION.sourceDescription - ) + activity?.let { + ReaderActivityLauncher.showReaderComments( + it, + siteId, + postId, + commentId, + ThreadedCommentsActionSource.COMMENT_NOTIFICATION.sourceDescription + ) + } }, { // Open the latest version of this note in case it has changed, which can happen if the note was diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index e141e8cec720..9912197e01ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -40,6 +40,7 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.fragment.app.viewModels @@ -1580,11 +1581,14 @@ class ReaderPostDetailFragment : ViewPagerFragment(), private fun handleDirectOperation() = when (directOperation) { DirectOperation.COMMENT_JUMP, DirectOperation.COMMENT_REPLY, DirectOperation.COMMENT_LIKE -> { viewModel.post?.let { - ReaderActivityLauncher.showReaderComments( - activity, it.blogId, it.postId, - directOperation, commentId.toLong(), viewModel.interceptedUri, - DIRECT_OPERATION.sourceDescription - ) + val maybeActivity: FragmentActivity? = activity + maybeActivity?.let { fragmentActivity -> + ReaderActivityLauncher.showReaderComments( + fragmentActivity, it.blogId, it.postId, + directOperation, commentId.toLong(), viewModel.interceptedUri, + DIRECT_OPERATION.sourceDescription + ) + } } activity?.finish() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt index 62a8d22ae378..6bc60f163672 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt @@ -172,12 +172,14 @@ class ReaderDiscoverFragment : ViewPagerFragment(R.layout.reader_discover_fragme is ShowPostDetail -> ReaderActivityLauncher.showReaderPostDetail(context, event.post.blogId, event.post.postId) is SharePost -> ReaderActivityLauncher.sharePost(context, event.post) is OpenPost -> ReaderActivityLauncher.openPost(context, event.post) - is ShowReaderComments -> ReaderActivityLauncher.showReaderComments( - context, - event.blogId, - event.postId, - READER_POST_CARD.sourceDescription - ) + is ShowReaderComments -> context?.let { + ReaderActivityLauncher.showReaderComments( + it, + event.blogId, + event.postId, + READER_POST_CARD.sourceDescription + ) + } is ShowNoSitesToReblog -> ReaderActivityLauncher.showNoSiteToReblog(activity) is ShowSitePickerForResult -> ActivityLauncher.showSitePickerForResult( this@ReaderDiscoverFragment, From 1522d334c1c44124f4edb2288d57775e73a4ded2 Mon Sep 17 00:00:00 2001 From: Andy Valdez Date: Fri, 24 May 2024 12:29:48 -0400 Subject: [PATCH 16/92] Undo uneccessary lib change. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3e4335014ce0..5efd1828685c 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ ext { automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' automatticTracksVersion = '5.0.0' - gutenbergMobileVersion = '6870-76281ef23dfd66f1268e18a4be9003a46967e4f8' + gutenbergMobileVersion = 'v1.119.0' wordPressAztecVersion = 'v2.1.3' wordPressFluxCVersion = 'trunk-a7d04cdb5a26e9c143daf661475dfbd1f3e466a4' wordPressLoginVersion = '1.15.0' From 50800dd735cf16020d0c44b641ffdb65a0ae9391 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 24 May 2024 15:18:27 -0400 Subject: [PATCH 17/92] Update FluxC hash --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a12b43add817..b7681651945e 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.119.0-alpha2' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = 'trunk-a7d04cdb5a26e9c143daf661475dfbd1f3e466a4' + wordPressFluxCVersion = '3019-46175406bbf1ab0ba3a7cb945785dfa82c50fe8a' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From ad7a4aa875e403f36c1419362290e63f838c4485 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 24 May 2024 15:19:00 -0400 Subject: [PATCH 18/92] Add the jetpack ai query call to the voice-to-content workflow --- .../voicetocontent/VoiceToContentUseCase.kt | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 4603a6d88a78..a6aad35e1dd5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -4,6 +4,7 @@ import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIRestClient import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionRestClient import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.viewmodel.ContextProvider @@ -14,36 +15,69 @@ import javax.inject.Inject class VoiceToContentUseCase @Inject constructor( private val jetpackAIStore: JetpackAIStore, - private val contextProvider: ContextProvider + private val fileHelperWrapper: VoiceToContentTempFileHelperWrapper ) { companion object { const val FEATURE = "voice_to_content" - private const val KILO_BYTE = 1024 + const val ROLE = "jetpack-ai" + const val TYPE = "voice-to-content-simple-draft" } suspend fun execute( siteModel: SiteModel, ): VoiceToContentResult = withContext(Dispatchers.IO) { - val file = getAudioFile() ?: return@withContext VoiceToContentResult(isError = true) - val response = jetpackAIStore.fetchJetpackAITranscription( + val file = fileHelperWrapper.getAudioFile() ?: return@withContext VoiceToContentResult(isError = true) + val transcriptionResponse = jetpackAIStore.fetchJetpackAITranscription( siteModel, FEATURE, file ) - when(response) { + val transcribedText: String? = when(transcriptionResponse) { is JetpackAITranscriptionRestClient.JetpackAITranscriptionResponse.Success -> { - return@withContext VoiceToContentResult(content = response.model) + transcriptionResponse.model } is JetpackAITranscriptionRestClient.JetpackAITranscriptionResponse.Error -> { - return@withContext VoiceToContentResult(isError = true) + null } } + + transcribedText?.let { + val response = jetpackAIStore.fetchJetpackAIQuery( + site = siteModel, + feature = FEATURE, + role = ROLE, + message = it, + stream = false, + type = TYPE + ) + + when(response) { + is JetpackAIRestClient.JetpackAIQueryResponse.Success -> { + return@withContext VoiceToContentResult(content = response.choices[0].message.content) + } + + is JetpackAIRestClient.JetpackAIQueryResponse.Error -> { + return@withContext VoiceToContentResult(isError = true) + } + } + + } ?:return@withContext VoiceToContentResult(isError = true) } +} - // todo: The next three methods are temporary to support development - remove when the real impl is in place - private fun getAudioFile(): File? { +// todo: build out the result object +data class VoiceToContentResult( + val content: String? = null, + val isError: Boolean = false +) + +// todo: Remove this class when real impl is in place - it's here so I can start unit tests +class VoiceToContentTempFileHelperWrapper @Inject constructor( + private val contextProvider: ContextProvider +) { + fun getAudioFile(): File? { val result = runCatching { getFileFromAssets(contextProvider.getContext()) } @@ -53,7 +87,7 @@ class VoiceToContentUseCase @Inject constructor( } } - // todo: Do not forget to delete the test file from the asset directory - when the real impl is in place + // todo: Do not forget to delete the test file from the asset directory private fun getFileFromAssets(context: Context): File { val fileName = "jetpack-ai-transcription-test-audio-file.m4a" val file = File(context.filesDir, fileName) @@ -74,10 +108,7 @@ class VoiceToContentUseCase @Inject constructor( } inputStream.close() } + companion object { + const val KILO_BYTE = 1024 + } } - -// todo: build out the result object -data class VoiceToContentResult( - val content: String? = null, - val isError: Boolean = false -) From 27b1e31203ff4476be5b9c78878b48410e5f09db Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 24 May 2024 15:31:07 -0400 Subject: [PATCH 19/92] Capture the custom jetpack_ai_error from within the success response and set as error --- .../android/ui/voicetocontent/VoiceToContentUseCase.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index a6aad35e1dd5..825ebe3efe29 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -21,6 +21,7 @@ class VoiceToContentUseCase @Inject constructor( const val FEATURE = "voice_to_content" const val ROLE = "jetpack-ai" const val TYPE = "voice-to-content-simple-draft" + const val JETPACK_AI_ERROR = "__JETPACK_AI_ERROR__" } suspend fun execute( @@ -55,7 +56,14 @@ class VoiceToContentUseCase @Inject constructor( when(response) { is JetpackAIRestClient.JetpackAIQueryResponse.Success -> { - return@withContext VoiceToContentResult(content = response.choices[0].message.content) + val finalContent: String = response.choices[0].message.content + // __JETPACK_AI_ERROR__ is a special marker we ask GPT to add to the request when it can’t + // understand the request for any reason, so maybe something confused GPT on some requests. + if (finalContent == JETPACK_AI_ERROR) { + return@withContext VoiceToContentResult(isError = true) + } else { + return@withContext VoiceToContentResult(content = response.choices[0].message.content) + } } is JetpackAIRestClient.JetpackAIQueryResponse.Error -> { From 32484c4903fce7d7aed380d7c472f5be2de78518 Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Fri, 24 May 2024 12:40:32 -0700 Subject: [PATCH 20/92] Checkstyle --- .../wordpress/android/ui/reader/ReaderActivityLauncher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index bf08fd042a88..5479c9a88b39 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -273,8 +273,8 @@ public static void showReaderCommentsForResult(@NotNull Fragment fragment, long fragment.startActivityForResult(intent, RequestCodes.READER_FOLLOW_CONVERSATION); } - private static Intent buildShowReaderCommentsIntent(@NonNull Context context, long blogId, long postId, DirectOperation - directOperation, long commentId, String interceptedUri, String source) { + private static Intent buildShowReaderCommentsIntent(@NonNull Context context, long blogId, long postId, + DirectOperation directOperation, long commentId, String interceptedUri, String source) { Intent intent = new Intent( context, ReaderCommentListActivity.class From e73390a6b18379230d0d46d21362b87447afe30c Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Fri, 24 May 2024 12:53:16 -0700 Subject: [PATCH 21/92] Update to NonNull --- .../wordpress/android/ui/reader/ReaderActivityLauncher.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index 5479c9a88b39..d56b986efe55 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -13,7 +13,6 @@ import androidx.core.app.ActivityOptionsCompat; import androidx.fragment.app.Fragment; -import org.jetbrains.annotations.NotNull; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.models.ReaderPost; import org.wordpress.android.models.ReaderTag; @@ -256,7 +255,7 @@ public static void showReaderCommentsForResult( showReaderCommentsForResult(fragment, blogId, postId, null, 0, null, source); } - public static void showReaderCommentsForResult(@NotNull Fragment fragment, long blogId, long postId, DirectOperation + public static void showReaderCommentsForResult(@NonNull Fragment fragment, long blogId, long postId, DirectOperation directOperation, long commentId, String interceptedUri, String source) { if (fragment.getContext() == null) { return; From 12bdeb5185c7cbaf68aea6fe054365ff9b8989de Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sat, 25 May 2024 12:49:13 -0400 Subject: [PATCH 22/92] Address checkstyle blank line --- .../wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 825ebe3efe29..7b328bba0aeb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -70,7 +70,6 @@ class VoiceToContentUseCase @Inject constructor( return@withContext VoiceToContentResult(isError = true) } } - } ?:return@withContext VoiceToContentResult(isError = true) } } From 40d396f48292d677851d6c9f58a1987f2a09b95a Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sat, 25 May 2024 13:05:59 -0400 Subject: [PATCH 23/92] Refactor: Drop the AI suffix from the actionType --- .../java/org/wordpress/android/ui/main/MainActionListItem.kt | 2 +- .../main/java/org/wordpress/android/ui/main/WPMainActivity.java | 2 +- .../wordpress/android/viewmodel/main/WPMainActivityViewModel.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt index c64a2df2e86b..755e674dfc0c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MainActionListItem.kt @@ -14,7 +14,7 @@ sealed class MainActionListItem { CREATE_NEW_PAGE_FROM_PAGES_CARD, CREATE_NEW_POST, ANSWER_BLOGGING_PROMPT, - CREATE_NEW_POST_FROM_AUDIO_AI + CREATE_NEW_POST_FROM_AUDIO } data class CreateAction( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 8b4281d9cb94..3fa7113bc2be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -720,7 +720,7 @@ private void initViewModel() { mViewModel.getCreateAction().observe(this, createAction -> { switch (createAction) { - case CREATE_NEW_POST_FROM_AUDIO_AI: + case CREATE_NEW_POST_FROM_AUDIO: launchVoiceToContent(); break; case CREATE_NEW_POST: diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt index 8c278ea462a5..2a3c932cd678 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt @@ -208,7 +208,7 @@ class WPMainActivityViewModel @Inject constructor( if (voiceToContentFeatureUtils.isVoiceToContentEnabled() && hasFullAccessToContent(site)) { actionsList.add( CreateAction( - actionType = ActionType.CREATE_NEW_POST_FROM_AUDIO_AI, + actionType = ActionType.CREATE_NEW_POST_FROM_AUDIO, iconRes = R.drawable.ic_mic_white_24dp, labelRes = R.string.my_site_bottom_sheet_add_post_from_audio, onClickAction = ::onCreateActionClicked From b91211fb39125808a0b1c732322f277ee0448b0c Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 26 May 2024 15:58:06 -0400 Subject: [PATCH 24/92] Update import statement to reflect JetpackAIQuery location move --- .../android/ui/voicetocontent/VoiceToContentUseCase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 7b328bba0aeb..54061cb50944 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -4,7 +4,7 @@ import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIRestClient +import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIQueryResponse import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionRestClient import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.viewmodel.ContextProvider @@ -55,7 +55,7 @@ class VoiceToContentUseCase @Inject constructor( ) when(response) { - is JetpackAIRestClient.JetpackAIQueryResponse.Success -> { + is JetpackAIQueryResponse.Success -> { val finalContent: String = response.choices[0].message.content // __JETPACK_AI_ERROR__ is a special marker we ask GPT to add to the request when it can’t // understand the request for any reason, so maybe something confused GPT on some requests. @@ -66,7 +66,7 @@ class VoiceToContentUseCase @Inject constructor( } } - is JetpackAIRestClient.JetpackAIQueryResponse.Error -> { + is JetpackAIQueryResponse.Error -> { return@withContext VoiceToContentResult(isError = true) } } From 12ac21be0245f7b8a119aef57ddd82690f2931a9 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 26 May 2024 16:25:18 -0400 Subject: [PATCH 25/92] Adjust import statements for response objects --- .../android/ui/voicetocontent/VoiceToContentUseCase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 54061cb50944..5ce276291b42 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIQueryResponse -import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionRestClient +import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionResponse import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.viewmodel.ContextProvider import java.io.File @@ -36,10 +36,10 @@ class VoiceToContentUseCase @Inject constructor( ) val transcribedText: String? = when(transcriptionResponse) { - is JetpackAITranscriptionRestClient.JetpackAITranscriptionResponse.Success -> { + is JetpackAITranscriptionResponse.Success -> { transcriptionResponse.model } - is JetpackAITranscriptionRestClient.JetpackAITranscriptionResponse.Error -> { + is JetpackAITranscriptionResponse.Error -> { null } } From f98105b9b892edaa789660d87c14e818dc7b34b3 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 26 May 2024 16:25:30 -0400 Subject: [PATCH 26/92] Update FluxC hash --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7681651945e..3c29a6175584 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.119.0-alpha2' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = '3019-46175406bbf1ab0ba3a7cb945785dfa82c50fe8a' + wordPressFluxCVersion = '3019-a0660818a78b84c3bff95e9bf634884d173f506d' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From 662de3eb291ba10dbfe1e314aa72211dc11f1c19 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 26 May 2024 18:06:55 -0400 Subject: [PATCH 27/92] Add call to get ai assistant feature --- .../VoiceToContentDialogFragment.kt | 7 ++++ .../voicetocontent/VoiceToContentViewModel.kt | 32 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 6e93b50a2eab..7e25526d267d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -54,6 +54,7 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { @Composable fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) { val result by viewModel.uiState.observeAsState() + val assistantFeature by viewModel.aiAssistantFeatureState.observeAsState() Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -69,6 +70,11 @@ fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) { Text(text = result?.content!!, fontSize = 20.sp, fontWeight = FontWeight.Bold) } + assistantFeature != null -> { + Text(text = "Assistant Feature Returned Successfully", fontSize = 20.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(16.dp)) + } + else -> { Text(text = "Ready to fake record - tap microphone", fontSize = 20.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(16.dp)) @@ -81,5 +87,6 @@ fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) { ) } } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index e889027ef9cb..949c5388ee0c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -5,7 +5,11 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature +import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse +import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.viewmodel.ScopedViewModel @@ -17,11 +21,15 @@ class VoiceToContentViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils, private val voiceToContentUseCase: VoiceToContentUseCase, - private val selectedSiteRepository: SelectedSiteRepository + private val selectedSiteRepository: SelectedSiteRepository, + private val jetpackAIStore: JetpackAIStore ) : ScopedViewModel(mainDispatcher) { private val _uiState = MutableLiveData() val uiState = _uiState as LiveData + private val _aiAssistantFeatureState = MutableLiveData() + val aiAssistantFeatureState = _aiAssistantFeatureState as LiveData + private fun isVoiceToContentEnabled() = voiceToContentFeatureUtils.isVoiceToContentEnabled() fun execute() { @@ -30,6 +38,28 @@ class VoiceToContentViewModel @Inject constructor( return } + if (isVoiceToContentEnabled()) { + viewModelScope.launch(Dispatchers.IO) { + val result = jetpackAIStore.fetchJetpackAIAssistantFeature(site) + when (result) { + is JetpackAIAssistantFeatureResponse.Success -> { + _aiAssistantFeatureState.postValue(result.model) + startVoiceToContentFlow() + } + is JetpackAIAssistantFeatureResponse.Error -> { + _uiState.postValue(VoiceToContentResult(isError = true)) + } + } + } + } + } + + private fun startVoiceToContentFlow() { + val site = selectedSiteRepository.getSelectedSite() ?: run { + _uiState.postValue(VoiceToContentResult(isError = true)) + return + } + if (isVoiceToContentEnabled()) { viewModelScope.launch { val result = voiceToContentUseCase.execute(site) From bdde56d03e8bfa5e59699e2ea5731b9a0ed4678d Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 26 May 2024 18:12:38 -0400 Subject: [PATCH 28/92] Fix checkstyle empty line --- .../android/ui/voicetocontent/VoiceToContentDialogFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 7e25526d267d..cfeb15196db0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -87,6 +87,5 @@ fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) { ) } } - } } From c3ca7995926c34035d5a24cc6c37428a6d013cae Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 26 May 2024 18:25:40 -0400 Subject: [PATCH 29/92] update FluxC hash --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3c29a6175584..904ada627e54 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.119.0-alpha2' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = '3019-a0660818a78b84c3bff95e9bf634884d173f506d' + wordPressFluxCVersion = '3021-a39b424e986db04ee70d85051f7ee7bdb4a31767' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From d8768acdf71cfd9c65495b8733ff0d43db6e8ffd Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Mon, 27 May 2024 12:09:48 +0300 Subject: [PATCH 30/92] Adds: RECORD_AUDIO permission --- WordPress/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index d7f5650ffd52..56249f15f6a0 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -17,6 +17,8 @@ + + Date: Mon, 27 May 2024 14:14:20 +0300 Subject: [PATCH 31/92] Adds: AudioRecorder basic implementation --- .../android/modules/ApplicationModule.java | 7 + .../ui/voicetocontent/RecordingUseCase.kt | 39 +++++ .../VoiceToContentDialogFragment.kt | 68 +++++++- .../voicetocontent/VoiceToContentUseCase.kt | 23 ++- .../voicetocontent/VoiceToContentViewModel.kt | 41 ++++- .../android/util/audio/AudioRecorder.kt | 153 ++++++++++++++++++ .../android/util/audio/IAudioRecorder.kt | 21 +++ .../android/util/audio/RecordingUpdate.kt | 7 + .../VoiceToContentViewModelTest.kt | 69 ++++++-- 9 files changed, 394 insertions(+), 34 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/util/audio/RecordingUpdate.kt diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index ed712b9d32e3..3221d85c6c93 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -28,6 +28,8 @@ import org.wordpress.android.ui.sitecreation.SiteCreationStep; import org.wordpress.android.ui.sitecreation.SiteCreationStepsProvider; import org.wordpress.android.util.BuildConfigWrapper; +import org.wordpress.android.util.audio.AudioRecorder; +import org.wordpress.android.util.audio.IAudioRecorder; import org.wordpress.android.util.config.InAppUpdatesFeatureConfig; import org.wordpress.android.util.config.RemoteConfigWrapper; import org.wordpress.android.util.wizard.WizardManager; @@ -121,4 +123,9 @@ public static ActivityNavigator provideActivityNavigator(@ApplicationContext Con public static SensorManager provideSensorManager(@ApplicationContext Context context) { return (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); } + + @Provides + public static IAudioRecorder provideAudioRecorder(@ApplicationContext Context context) { + return new AudioRecorder(1000000L * 25, context); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt new file mode 100644 index 000000000000..375de708c52d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt @@ -0,0 +1,39 @@ +package org.wordpress.android.ui.voicetocontent + +import kotlinx.coroutines.flow.Flow +import org.wordpress.android.util.audio.IAudioRecorder +import org.wordpress.android.util.audio.RecordingUpdate +import java.io.File +import javax.inject.Inject + +class RecordingUseCase @Inject constructor( + private val audioRecorder: IAudioRecorder +) { + fun startRecording() { + audioRecorder.startRecording() + } + + @Suppress("ReturnCount") + suspend fun stopRecording(): File? { + val recordingPath = audioRecorder.stopRecording() + // Return null if the recording path is invalid + if (recordingPath.isNullOrEmpty()) return null + val recordingFile = File(recordingPath) + // Return null if the file does not exist, is not a file, or is empty + if (!recordingFile.exists() || !recordingFile.isFile || recordingFile.length() == 0L) return null + return recordingFile + } + + fun pauseRecording() { + audioRecorder.pauseRecording() + } + + fun resumeRecording() { + audioRecorder.resumeRecording() + } + + fun recordingUpdates(): Flow { + return audioRecorder.recordingUpdates() + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 6e93b50a2eab..2ed6fff5daed 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -1,9 +1,13 @@ package org.wordpress.android.ui.voicetocontent +import android.Manifest +import android.content.pm.PackageManager import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,7 +31,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import org.wordpress.android.R +import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS @AndroidEntryPoint class VoiceToContentDialogFragment : BottomSheetDialogFragment() { @@ -38,11 +44,46 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { ): View = ComposeView(requireContext()).apply { setContent { AppTheme { - VoiceToContentScreen(viewModel) + VoiceToContentScreen( + viewModel = viewModel, + onRequestPermission = { requestAllPermissionsForRecording() }, + hasPermission = { hasAllPermissionsForRecording() } + ) } } } + private val requestMultiplePermissionsLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val areAllPermissionsGranted = permissions.entries.all { it.value } + if (areAllPermissionsGranted) { + viewModel.startRecording() + } else { + // Handle permissions denied case + // todo pantelis handle permissions denied case + Toast.makeText(context, "Permissions needed for recording", Toast.LENGTH_SHORT).show() + } + } + + private fun hasAllPermissionsForRecording(): Boolean { + return REQUIRED_RECORDING_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + requireContext(), + it + ) == PackageManager.PERMISSION_GRANTED + } + } + + private fun requestAllPermissionsForRecording() { + requestMultiplePermissionsLauncher.launch( + arrayOf( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + ) + } + companion object { const val TAG = "voice_to_content_fragment_tag" @@ -52,7 +93,11 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { } @Composable -fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) { +fun VoiceToContentScreen( + viewModel: VoiceToContentViewModel, + onRequestPermission: () -> Unit, + hasPermission: () -> Boolean +) { val result by viewModel.uiState.observeAsState() Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -77,7 +122,24 @@ fun VoiceToContentScreen(viewModel: VoiceToContentViewModel) { contentDescription = "Microphone", modifier = Modifier .size(64.dp) - .clickable { viewModel.execute() } + .clickable { + if (hasPermission()) { + viewModel.startRecording() + } else { + onRequestPermission() + } + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + Icon( + painterResource(id = com.google.android.exoplayer2.ui.R.drawable.exo_icon_stop), + contentDescription = "Stop", + modifier = Modifier + .size(64.dp) + .clickable { + viewModel.stopRecording() + } ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 4603a6d88a78..64d467f02541 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -1,31 +1,26 @@ package org.wordpress.android.ui.voicetocontent -import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionRestClient import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore -import org.wordpress.android.viewmodel.ContextProvider import java.io.File -import java.io.FileOutputStream -import java.io.InputStream import javax.inject.Inject class VoiceToContentUseCase @Inject constructor( - private val jetpackAIStore: JetpackAIStore, - private val contextProvider: ContextProvider + private val jetpackAIStore: JetpackAIStore ) { companion object { const val FEATURE = "voice_to_content" - private const val KILO_BYTE = 1024 + // private const val KILO_BYTE = 1024 } suspend fun execute( siteModel: SiteModel, + file: File ): VoiceToContentResult = withContext(Dispatchers.IO) { - val file = getAudioFile() ?: return@withContext VoiceToContentResult(isError = true) val response = jetpackAIStore.fetchJetpackAITranscription( siteModel, FEATURE, @@ -43,7 +38,7 @@ class VoiceToContentUseCase @Inject constructor( } // todo: The next three methods are temporary to support development - remove when the real impl is in place - private fun getAudioFile(): File? { + /* private fun getAudioFile(): File? { val result = runCatching { getFileFromAssets(contextProvider.getContext()) } @@ -51,19 +46,19 @@ class VoiceToContentUseCase @Inject constructor( return result.getOrElse { null } - } + }*/ // todo: Do not forget to delete the test file from the asset directory - when the real impl is in place - private fun getFileFromAssets(context: Context): File { + /*private fun getFileFromAssets(context: Context): File { val fileName = "jetpack-ai-transcription-test-audio-file.m4a" val file = File(context.filesDir, fileName) context.assets.open(fileName).use { inputStream -> copyInputStreamToFile(inputStream, file) } return file - } + }*/ - private fun copyInputStreamToFile(inputStream: InputStream, outputFile: File) { + /*private fun copyInputStreamToFile(inputStream: InputStream, outputFile: File) { FileOutputStream(outputFile).use { outputStream -> val buffer = ByteArray(KILO_BYTE) var length: Int @@ -73,7 +68,7 @@ class VoiceToContentUseCase @Inject constructor( outputStream.flush() } inputStream.close() - } + }*/ } // todo: build out the result object diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index e889027ef9cb..f47ab728caf4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.voicetocontent +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -9,6 +10,7 @@ import kotlinx.coroutines.launch import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.viewmodel.ScopedViewModel +import java.io.File import javax.inject.Inject import javax.inject.Named @@ -17,14 +19,47 @@ class VoiceToContentViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils, private val voiceToContentUseCase: VoiceToContentUseCase, - private val selectedSiteRepository: SelectedSiteRepository + private val selectedSiteRepository: SelectedSiteRepository, + private val recordingUseCase: RecordingUseCase ) : ScopedViewModel(mainDispatcher) { private val _uiState = MutableLiveData() val uiState = _uiState as LiveData private fun isVoiceToContentEnabled() = voiceToContentFeatureUtils.isVoiceToContentEnabled() - fun execute() { + init { + observeRecordingUpdates() + } + + private fun observeRecordingUpdates() { + viewModelScope.launch { + recordingUseCase.recordingUpdates().collect { update -> + if (update.fileSizeLimitExceeded) { + stopRecording() + } else { + // Handle other updates if needed, e.g., elapsed time and file size + Log.d("AudioRecorder", "Recording update: $update") + } + } + } + } + + fun startRecording() { + recordingUseCase.startRecording() + } + + fun stopRecording() { + viewModelScope.launch { + val file = recordingUseCase.stopRecording() + file?.let { + executeVoiceToContent(it) + } ?: run { + _uiState.postValue(VoiceToContentResult(isError = true)) + } + } + } + + fun executeVoiceToContent(file: File) { val site = selectedSiteRepository.getSelectedSite() ?: run { _uiState.postValue(VoiceToContentResult(isError = true)) return @@ -32,7 +67,7 @@ class VoiceToContentViewModel @Inject constructor( if (isVoiceToContentEnabled()) { viewModelScope.launch { - val result = voiceToContentUseCase.execute(site) + val result = voiceToContentUseCase.execute(site, file) _uiState.postValue(result) } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt new file mode 100644 index 000000000000..3686384623c6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -0,0 +1,153 @@ +package org.wordpress.android.util.audio + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.media.MediaRecorder +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.io.File +import java.io.IOException + +class AudioRecorder( + private val fileSizeLimit: Long, + private val applicationContext: Context +) : IAudioRecorder { + private val storeInMemory = true + private val filePath by lazy { + if (storeInMemory) { + applicationContext.cacheDir.absolutePath + "/recording.3gp" + } else { + applicationContext.getExternalFilesDir(null)?.absolutePath + "/recording.3gp" + } + } + + private var recorder: MediaRecorder? = null + private var recordingJob: Job? = null + private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + private var isPausedRecording = false + + private val _recordingUpdates = MutableStateFlow(RecordingUpdate()) + private val recordingUpdates: StateFlow get() = _recordingUpdates.asStateFlow() + + private val _isRecording = MutableStateFlow(false) + val isRecording: StateFlow = _isRecording + + private val _isPaused = MutableStateFlow(false) + val isPaused: StateFlow = _isPaused + + @Suppress("DEPRECATION") + override fun startRecording() { + if (applicationContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED) { + recorder = MediaRecorder().apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + setOutputFile(filePath) + + try { + prepare() + start() + startRecordingUpdates() + _isRecording.value = true + _isPaused.value = false + } catch (e: IOException) { + // Use a logging framework like Timber + Log.e("AudioRecorder", "Error starting recording") + } + } + } else { + // Handle permission not granted case, e.g., throw an exception or show a message + Log.e("AudioRecorder","Permission to record audio not granted") + } + } + + override fun stopRecording(): String { + try { + recorder?.apply { + stop() + release() + } + } catch (e: IllegalStateException) { + Log.e("AudioRecorder", "Error stopping recording") + } finally { + recorder = null + stopRecordingUpdates() + _isPaused.value = false + _isRecording.value = false + } + return filePath + } + + override fun pauseRecording() { + if (recorder != null) { + try { + recorder?.pause() + _isPaused.value = true + stopRecordingUpdates() + } catch (e: IllegalStateException) { + Log.e("AudioRecorder", "Error pausing recording") + } + } else { + Log.e("AudioRecorder","Pause not supported on this device") + } + } + + override fun resumeRecording() { + if (isPausedRecording) { + coroutineScope.launch { + try { + delay(RESUME_DELAY) + recorder?.resume() + _isPaused.value = false + isPausedRecording = false + startRecordingUpdates() + } catch (e: IllegalStateException) { + Log.e("AudioRecorder", "Error resuming recording") + } + } + } + } + + override fun recordingUpdates(): Flow = recordingUpdates + + private fun startRecordingUpdates() { + recordingJob = coroutineScope.launch { + var elapsedTime = 0 + while (recorder != null) { + delay(RECORDING_UPDATE_INTERVAL) + elapsedTime++ + val fileSize = File(filePath).length() + _recordingUpdates.value = RecordingUpdate( + elapsedTime = elapsedTime, + fileSize = fileSize, + fileSizeLimitExceeded = fileSize >= fileSizeLimit + ) + + if (fileSize >= fileSizeLimit) { + stopRecording() + } + } + } + } + + private fun stopRecordingUpdates() { + recordingJob?.cancel() + } + + companion object { + private const val RECORDING_UPDATE_INTERVAL = 1000L + private const val RESUME_DELAY = 500L + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt new file mode 100644 index 000000000000..4f4b35b820ee --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.util.audio + +import android.Manifest +import kotlinx.coroutines.flow.Flow + +interface IAudioRecorder { + fun startRecording() + fun stopRecording(): String + fun pauseRecording() + fun resumeRecording() + fun recordingUpdates(): Flow + + companion object { + val REQUIRED_RECORDING_PERMISSIONS = arrayOf( + Manifest.permission.RECORD_AUDIO, + // todo pantelis: do we need this? + // Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingUpdate.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingUpdate.kt new file mode 100644 index 000000000000..fbd7ceabce38 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingUpdate.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.util.audio + +data class RecordingUpdate( + val elapsedTime: Int = 0, // in seconds + val fileSize: Long = 0L, // in bytes + val fileSizeLimitExceeded: Boolean = false +) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt index 9bcbfb120119..1f3ebfe27ffe 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt @@ -1,6 +1,8 @@ package org.wordpress.android.ui.voicetocontent import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -13,6 +15,8 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.util.audio.RecordingUpdate +import java.io.File @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) @@ -23,19 +27,27 @@ class VoiceToContentViewModelTest : BaseUnitTest() { @Mock lateinit var voiceToContentUseCase: VoiceToContentUseCase + @Mock + lateinit var recordingUseCase: RecordingUseCase + @Mock lateinit var selectedSiteRepository: SelectedSiteRepository private lateinit var viewModel: VoiceToContentViewModel private lateinit var uiState: MutableList + @Before fun setup() { + // Mock the recording updates to return a non-null flow before ViewModel instantiation + whenever(recordingUseCase.recordingUpdates()).thenReturn(createRecordingUpdateFlow()) + viewModel = VoiceToContentViewModel( testDispatcher(), voiceToContentFeatureUtils, voiceToContentUseCase, - selectedSiteRepository + selectedSiteRepository, + recordingUseCase ) uiState = mutableListOf() @@ -46,35 +58,64 @@ class VoiceToContentViewModelTest : BaseUnitTest() { } } - @Test - fun `when site is null, then execute posts error state `() = test { - whenever(selectedSiteRepository.getSelectedSite()).thenReturn(null) - - viewModel.execute() - - val expectedState = VoiceToContentResult(isError = true) - assertThat(uiState.first()).isEqualTo(expectedState) + // Helper function to create a consistent flow + private fun createRecordingUpdateFlow() = flow { + emit(RecordingUpdate(0, 0, false)) + // You can add more emits to simulate different recording states if needed } @Test - fun `when voice to content is enabled, then execute invokes use case `() = test { + fun `when voice to content is enabled, then executeVoiceToContent invokes use case`() = runTest { val site = SiteModel().apply { id = 1 } whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) + val dummyFile = File("dummy_path") - viewModel.execute() + viewModel.executeVoiceToContent(dummyFile) - verify(voiceToContentUseCase).execute(site) + verify(voiceToContentUseCase).execute(site, dummyFile) } @Test - fun `when voice to content is disabled, then execute does not invoke use case `() = test { + fun `when voice to content is disabled, then executeVoiceToContent does not invoke use case`() = runTest { val site = SiteModel().apply { id = 1 } whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(false) + val dummyFile = File("dummy_path") - viewModel.execute() + viewModel.executeVoiceToContent(dummyFile) verifyNoInteractions(voiceToContentUseCase) } + + @Test + fun `when stopRecording is called and file is null, then posts error state`() = runTest { + whenever(recordingUseCase.stopRecording()).thenReturn(null) + + viewModel.stopRecording() + + val expectedState = VoiceToContentResult(isError = true) + assertThat(uiState.first()).isEqualTo(expectedState) + } + + @Test + fun `when startRecording is called, then recordingUseCase starts recording`() { + viewModel.startRecording() + + verify(recordingUseCase).startRecording() + } + + @Test + fun `when stopRecording is called and file is not null, then executeVoiceToContent is called`() = runTest { + val dummyFile = File("dummy_path") + val site = SiteModel().apply { id = 1 } + whenever(recordingUseCase.stopRecording()).thenReturn(dummyFile) + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) + whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) + + viewModel.stopRecording() + + verify(voiceToContentUseCase).execute(site, dummyFile) + } } + From 01c949a6b31a84e1f857296cafd800a595de8832 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Mon, 27 May 2024 17:45:29 +0300 Subject: [PATCH 32/92] Remove ReviewViewModel.kt The functionality in ReviewViewModel has been moved to AppRatingDialog. --- .../android/modules/ViewModelModule.java | 6 -- .../android/ui/main/WPMainActivity.java | 5 -- .../android/ui/posts/PostsListActivity.kt | 11 ---- .../wordpress/android/ui/prefs/AppPrefs.java | 2 +- .../android/ui/review/ReviewViewModel.kt | 41 ------------ .../android/widgets/AppRatingDialog.kt | 42 ++++++++++-- .../android/ui/review/ReviewViewModelTest.kt | 64 ------------------- ...test_instructions_per_dependency_update.md | 2 +- 8 files changed, 37 insertions(+), 136 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt delete mode 100644 WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index 634547141d8a..9989e37ba302 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -54,7 +54,6 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderPostListViewModel; import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel; import org.wordpress.android.ui.reader.viewmodels.SubfilterPageViewModel; -import org.wordpress.android.ui.review.ReviewViewModel; import org.wordpress.android.ui.stats.refresh.lists.DaysListViewModel; import org.wordpress.android.ui.stats.refresh.lists.InsightsDetailListViewModel; import org.wordpress.android.ui.stats.refresh.lists.InsightsListViewModel; @@ -461,11 +460,6 @@ abstract class ViewModelModule { @ViewModelKey(UnifiedCommentListViewModel.class) abstract ViewModel unifiedCommentListViewModel(UnifiedCommentListViewModel viewModel); - @Binds - @IntoMap - @ViewModelKey(ReviewViewModel.class) - abstract ViewModel reviewViewModel(ReviewViewModel viewModel); - @Binds @IntoMap @ViewModelKey(BloggingRemindersViewModel.class) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index b463f9e68ed9..045202605e8d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -139,7 +139,6 @@ import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask; import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter; import org.wordpress.android.ui.reader.tracker.ReaderTracker; -import org.wordpress.android.ui.review.ReviewViewModel; import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource; import org.wordpress.android.ui.stats.StatsTimeframe; import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom; @@ -260,7 +259,6 @@ public class WPMainActivity extends LocaleAwareActivity implements private WPMainActivityViewModel mViewModel; private ModalLayoutPickerViewModel mMLPViewModel; - @NonNull private ReviewViewModel mReviewViewModel; private BloggingRemindersViewModel mBloggingRemindersViewModel; private NotificationsListViewModel mNotificationsViewModel; private FloatingActionButton mFloatingActionButton; @@ -690,7 +688,6 @@ private void initViewModel() { mViewModel = new ViewModelProvider(this, mViewModelFactory).get(WPMainActivityViewModel.class); mMLPViewModel = new ViewModelProvider(this, mViewModelFactory).get(ModalLayoutPickerViewModel.class); - mReviewViewModel = new ViewModelProvider(this, mViewModelFactory).get(ReviewViewModel.class); mBloggingRemindersViewModel = new ViewModelProvider(this, mViewModelFactory).get(BloggingRemindersViewModel.class); @@ -1413,7 +1410,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(site.getId(), isFirstTimePublishing); if (isFirstTimePublishing) { - mReviewViewModel.onPublishingPost(); } } ); @@ -1823,7 +1819,6 @@ public void onPostUploaded(OnPostUploaded event) { isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing); if (isFirstTimePublishing) { - mReviewViewModel.onPublishingPost(); } } ); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index 1fe521200dd6..bf294a521fb0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -52,7 +52,6 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetFrag import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetFragment.Companion.newInstance import org.wordpress.android.ui.posts.prepublishing.home.PublishPost import org.wordpress.android.ui.posts.prepublishing.listeners.PrepublishingBottomSheetListener -import org.wordpress.android.ui.review.ReviewViewModel import org.wordpress.android.ui.uploads.UploadActionUseCase import org.wordpress.android.ui.uploads.UploadUtilsWrapper import org.wordpress.android.ui.utils.UiHelpers @@ -122,9 +121,6 @@ class PostsListActivity : LocaleAwareActivity(), @Inject internal lateinit var bloggingRemindersViewModel: BloggingRemindersViewModel - @Inject - internal lateinit var reviewViewModel: ReviewViewModel - @Inject internal lateinit var blazeFeatureUtils: BlazeFeatureUtils @@ -211,7 +207,6 @@ class PostsListActivity : LocaleAwareActivity(), initViewModel(initPreviewState, currentBottomSheetPostId) initSearchFragment() initBloggingReminders() - initInAppReviews() initTabLayout(tabIndex) loadIntentData(intent) } @@ -337,11 +332,6 @@ class PostsListActivity : LocaleAwareActivity(), } } - private fun initInAppReviews() { - reviewViewModel = ViewModelProvider(this@PostsListActivity, viewModelFactory)[ReviewViewModel::class.java] - reviewViewModel.launchReview.observeEvent(this) { launchInAppReviews() } - } - private fun launchInAppReviews() { val manager = ReviewManagerFactory.create(this) val request = manager.requestReviewFlow() @@ -382,7 +372,6 @@ class PostsListActivity : LocaleAwareActivity(), changeTabsOnPostUpload() bloggingRemindersViewModel.onPublishingPost(site.id, isFirstTimePublishing) if (isFirstTimePublishing) { - reviewViewModel.onPublishingPost() } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index e53b18a38d03..fb099367d74e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -162,7 +162,7 @@ public enum DeletablePrefKey implements PrefKey { SITE_JETPACK_CAPABILITIES, REMOVED_QUICK_START_CARD_TYPE, PINNED_DYNAMIC_CARD, - // PUBLISHED_POST_COUNT will increase until it reaches ReviewViewModel.TARGET_COUNT_POST_PUBLISHED + // PUBLISHED_POST_COUNT will increase until it reaches AppRatingDialog.TARGET_COUNT_POST_PUBLISHED PUBLISHED_POST_COUNT, BLOGGING_REMINDERS_SHOWN, SHOULD_SCHEDULE_CREATE_SITE_NOTIFICATION, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt deleted file mode 100644 index 29150b67dee7..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.wordpress.android.ui.review - -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.viewmodel.Event -import org.wordpress.android.widgets.AppRatingDialog -import java.util.Date -import javax.inject.Inject - -/** - * Manages the logic for the flow of in-app reviews prompt. - */ -class ReviewViewModel @Inject constructor(private val appPrefsWrapper: AppPrefsWrapper) : ViewModel() { - private val _launchReview = MutableLiveData>() - val launchReview = _launchReview as LiveData> - - fun onPublishingPost() { - val shouldWaitForLastShownTime = - Date().time - AppRatingDialog.inAppReviewsShownDate.time < AppRatingDialog.criteriaInstallMs - val shouldWaitAskLaterTime = Date().time - AppRatingDialog.askLaterDate.time < AppRatingDialog.criteriaInstallMs - if (!AppRatingDialog.doNotShowInAppReviewsPrompt && !shouldWaitAskLaterTime && !shouldWaitForLastShownTime) { - if (appPrefsWrapper.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { - appPrefsWrapper.incrementPublishedPostCount() - } - if (appPrefsWrapper.getPublishedPostCount() == TARGET_COUNT_POST_PUBLISHED) { - _launchReview.value = Event(Unit) - - AppRatingDialog.storeInAppReviewsShownDate() - appPrefsWrapper.resetPublishedPostCount() - } - } - } - - companion object { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - const val TARGET_COUNT_POST_PUBLISHED = 2 - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index 6d1aa8f94bad..38ab2d14c4c8 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -29,10 +29,11 @@ object AppRatingDialog { private const val KEY_INTERACTIONS = "rate_interactions" private const val IN_APP_REVIEWS_SHOWN_DATE = "in_app_reviews_shown_date" private const val DO_NOT_SHOW_IN_APP_REVIEWS_PROMPT = "do_not_show_in_app_reviews_prompt" + private const val TARGET_COUNT_POST_PUBLISHED = 2 // app must have been installed this long before the rating dialog will appear private const val CRITERIA_INSTALL_DAYS: Int = 7 - val criteriaInstallMs = TimeUnit.DAYS.toMillis(CRITERIA_INSTALL_DAYS.toLong()) + private val criteriaInstallMs = TimeUnit.DAYS.toMillis(CRITERIA_INSTALL_DAYS.toLong()) // app must have been launched this many times before the rating dialog will appear private const val CRITERIA_LAUNCH_TIMES: Int = 10 @@ -41,12 +42,12 @@ object AppRatingDialog { private const val CRITERIA_INTERACTIONS: Int = 10 private var installDate = Date() - var askLaterDate = Date() + private var askLaterDate = Date() private var launchTimes = 0 private var interactions = 0 private var optOut = false - var inAppReviewsShownDate = Date(0) - var doNotShowInAppReviewsPrompt = false + private var inAppReviewsShownDate = Date(0) + private var doNotShowInAppReviewsPrompt = false private lateinit var preferences: SharedPreferences @@ -81,7 +82,6 @@ object AppRatingDialog { * Show the rate dialog if the criteria is satisfied. * @return true if shown, false otherwise. */ - fun showRateDialogIfNeeded(fragmentManger: FragmentManager): Boolean { return if (shouldShowRateDialog()) { showRateDialog(fragmentManger) @@ -103,6 +103,34 @@ object AppRatingDialog { } } + /** + * Called when a post is published. We use this to determine which users will see the in-app review prompt. + */ + fun onPostPublished() { + if (shouldShowInAppReviewsPrompt()) return + if (AppPrefs.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { + AppPrefs.incrementPublishedPostCount() + } + } + + /** + * Check whether the in-app reviews prompt should be shown or not. + * @return true if the prompt should be shown + */ + fun shouldShowInAppReviewsPrompt(): Boolean { + val shouldWaitAfterLastShown = Date().time - inAppReviewsShownDate.time < criteriaInstallMs + val shouldWaitAfterAskLaterTapped = Date().time - askLaterDate.time < criteriaInstallMs + val publishedPostsGoal = AppPrefs.getPublishedPostCount() == TARGET_COUNT_POST_PUBLISHED + val notificationsGoal = notificationCount == TARGET_COUNT_NOTIFICATIONS + return !doNotShowInAppReviewsPrompt && !shouldWaitAfterLastShown && !shouldWaitAfterAskLaterTapped && + (publishedPostsGoal || notificationsGoal) + } + + fun onInAppReviewsPromptShown() { + storeInAppReviewsShownDate() + AppPrefs.resetPublishedPostCount() + } + /** * Check whether the rate dialog should be shown or not. * @return true if the dialog should be shown @@ -152,7 +180,7 @@ object AppRatingDialog { Intent.ACTION_VIEW, Uri.parse( "http://play.google.com/store/apps/details?id=" + - requireActivity().packageName + requireActivity().packageName ) ) ) @@ -234,6 +262,6 @@ object AppRatingDialog { /** * Store the date the in-app reviews prompt is attempted to launch. */ - fun storeInAppReviewsShownDate() = + private fun storeInAppReviewsShownDate() = preferences.edit().putLong(IN_APP_REVIEWS_SHOWN_DATE, System.currentTimeMillis())?.apply() } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt deleted file mode 100644 index 4d2c00860c4e..000000000000 --- a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.wordpress.android.ui.review - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.whenever -import org.wordpress.android.eventToList -import org.wordpress.android.ui.prefs.AppPrefsWrapper -import kotlin.test.assertEquals - -@RunWith(MockitoJUnitRunner::class) -class ReviewViewModelTest { - @Rule - @JvmField - val rule = InstantTaskExecutorRule() - - @Mock - lateinit var appPrefsWrapper: AppPrefsWrapper - - private lateinit var viewModel: ReviewViewModel - - private lateinit var events: MutableList - - @Before - fun setup() { - viewModel = ReviewViewModel(appPrefsWrapper) - events = mutableListOf() - events = viewModel.launchReview.eventToList() - } - - @Test - fun onPublishingPost_whenPublishedCountIsLow_doNotLaunchInAppReviews() { - whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(false) - whenever(appPrefsWrapper.getPublishedPostCount()).thenReturn(ReviewViewModel.TARGET_COUNT_POST_PUBLISHED - 1) - - viewModel.onPublishingPost() - - assertEquals(events.size, 0) - } - - @Test - fun onPublishingPost_whenInAppReviewsAlreadyShown_doNotLaunchInAppReviews() { - whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(true) - - viewModel.onPublishingPost() - - assertEquals(events.size, 0) - } - - @Test - fun onPublishingPost_whenPublishedCountIsHigh_launchInAppReviews() { - whenever(appPrefsWrapper.isInAppReviewsShown()).thenReturn(false) - whenever(appPrefsWrapper.getPublishedPostCount()).thenReturn(ReviewViewModel.TARGET_COUNT_POST_PUBLISHED) - - viewModel.onPublishingPost() - - // Verify `launchReview` is triggered. - assertEquals(Unit, events.last()) - } -} diff --git a/docs/test_instructions_per_dependency_update.md b/docs/test_instructions_per_dependency_update.md index 7c373357c0cc..8a72a770d0b6 100644 --- a/docs/test_instructions_per_dependency_update.md +++ b/docs/test_instructions_per_dependency_update.md @@ -426,7 +426,7 @@ Step.3: 1. In app reviews - Perform a clean install. -- Publish three (`ReviewViewModel.TARGET_COUNT_POST_PUBLISHED + 1`) new posts or stories. +- Publish three (`AppRatingDialog.TARGET_COUNT_POST_PUBLISHED + 1`) new posts or stories. - Verify that there are no crashes. From 5ab588a542bc45d8a8b1889081953309bcce4266 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Mon, 27 May 2024 18:00:45 +0300 Subject: [PATCH 33/92] Update resetPublishedPostCount() method --- .../src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index fb099367d74e..02e5481eaeb2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -1302,7 +1302,7 @@ public static void incrementPublishedPostCount() { } public static void resetPublishedPostCount() { - putInt(DeletablePrefKey.PUBLISHED_POST_COUNT, 0); + remove(DeletablePrefKey.PUBLISHED_POST_COUNT); } public static int getPublishedPostCount() { From 982e79e891e3009b3c38d44638aafda1e5f5e1d1 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Mon, 27 May 2024 18:04:14 +0300 Subject: [PATCH 34/92] Track notification count for in-app reviews prompt logic --- .../android/ui/main/WPMainActivity.java | 14 ++++---- .../NotificationsListViewModel.kt | 5 ++- .../android/ui/posts/PostsListActivity.kt | 6 ++++ .../wordpress/android/ui/prefs/AppPrefs.java | 14 ++++++++ .../android/widgets/AppRatingDialog.kt | 33 +++++++++++++++---- 5 files changed, 58 insertions(+), 14 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 045202605e8d..f11e7107d17b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -506,7 +506,12 @@ && getIntent().getExtras().getBoolean(ARG_CONTINUE_JETPACK_CONNECT, false)) { } if (canShowAppRatingPrompt) { - AppRatingDialog.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager()); + if (AppRatingDialog.INSTANCE.shouldShowInAppReviewsPrompt()) { + launchInAppReviews(); + AppRatingDialog.INSTANCE.onInAppReviewsPromptShown(); + } else { + AppRatingDialog.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager()); + } } scheduleLocalNotifications(); @@ -783,11 +788,6 @@ private void initViewModel() { }); }); - mReviewViewModel.getLaunchReview().observe(this, event -> event.applyIfNotHandled(unit -> { - launchInAppReviews(); - return null; - })); - BloggingReminderUtils.observeBottomSheet( mBloggingRemindersViewModel.isBottomSheetShowing(), this, @@ -1410,6 +1410,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(site.getId(), isFirstTimePublishing); if (isFirstTimePublishing) { + AppRatingDialog.INSTANCE.onPostPublished(); } } ); @@ -1819,6 +1820,7 @@ public void onPostUploaded(OnPostUploaded event) { isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing); if (isFirstTimePublishing) { + AppRatingDialog.INSTANCE.onPostPublished(); } } ); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt index cf1a761f4d56..f66b9744f18d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt @@ -35,6 +35,7 @@ import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.ToastUtilsWrapper import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.widgets.AppRatingDialog import javax.inject.Inject import javax.inject.Named @@ -141,6 +142,7 @@ class NotificationsListViewModel @Inject constructor( openDetailView: () -> Unit ) { val note = noteId?.let { notificationsUtilsWrapper.getNoteById(noteId) } + note?.let { AppRatingDialog.onNotificationReceived(it) } if (note != null && note.isCommentType && !note.canModerate()) { val readerPost = readerPostTableWrapper.getBlogPost(note.siteId.toLong(), note.postId.toLong(), false) if (readerPost != null) { @@ -158,7 +160,8 @@ class NotificationsListViewModel @Inject constructor( appLogWrapper.w(AppLog.T.NOTIFS, "Failed to fetch post for comment: $statusCode") openDetailView() } - }) + } + ) } } else { openDetailView() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index bf294a521fb0..928910c6b194 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -64,6 +64,7 @@ import org.wordpress.android.util.extensions.logException import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.viewmodel.observeEvent +import org.wordpress.android.widgets.AppRatingDialog import javax.inject.Inject import android.R as AndroidR @@ -372,6 +373,7 @@ class PostsListActivity : LocaleAwareActivity(), changeTabsOnPostUpload() bloggingRemindersViewModel.onPublishingPost(site.id, isFirstTimePublishing) if (isFirstTimePublishing) { + AppRatingDialog.onPostPublished() } } } @@ -443,6 +445,10 @@ class PostsListActivity : LocaleAwareActivity(), override fun onResume() { super.onResume() ActivityId.trackLastActivity(ActivityId.POSTS) + if (AppRatingDialog.shouldShowInAppReviewsPrompt()) { + launchInAppReviews() + AppRatingDialog.onInAppReviewsPromptShown() + } } @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index 02e5481eaeb2..cacf342db357 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -164,6 +164,8 @@ public enum DeletablePrefKey implements PrefKey { PINNED_DYNAMIC_CARD, // PUBLISHED_POST_COUNT will increase until it reaches AppRatingDialog.TARGET_COUNT_POST_PUBLISHED PUBLISHED_POST_COUNT, + // PUBLISHED_POST_COUNT will increase until it reaches AppRatingDialog.TARGET_COUNT_NOTIFICATIONS + IN_APP_REVIEWS_NOTIFICATION_COUNT, BLOGGING_REMINDERS_SHOWN, SHOULD_SCHEDULE_CREATE_SITE_NOTIFICATION, SHOULD_SHOW_WEEKLY_ROUNDUP_NOTIFICATION, @@ -1309,6 +1311,18 @@ public static int getPublishedPostCount() { return prefs().getInt(DeletablePrefKey.PUBLISHED_POST_COUNT.name(), 0); } + public static void incrementInAppReviewsNotificationCount() { + putInt(DeletablePrefKey.IN_APP_REVIEWS_NOTIFICATION_COUNT, getInAppReviewsNotificationCount() + 1); + } + + public static int getInAppReviewsNotificationCount() { + return prefs().getInt(DeletablePrefKey.IN_APP_REVIEWS_NOTIFICATION_COUNT.name(), 0); + } + + public static void resetInAppReviewsNotificationCount() { + remove(DeletablePrefKey.IN_APP_REVIEWS_NOTIFICATION_COUNT); + } + public static void setBloggingRemindersShown(int siteId) { prefs().edit().putBoolean(getBloggingRemindersConfigKey(siteId), true).apply(); } diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt index 38ab2d14c4c8..9e3eb3c0838a 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.models.Note import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T @@ -30,6 +31,7 @@ object AppRatingDialog { private const val IN_APP_REVIEWS_SHOWN_DATE = "in_app_reviews_shown_date" private const val DO_NOT_SHOW_IN_APP_REVIEWS_PROMPT = "do_not_show_in_app_reviews_prompt" private const val TARGET_COUNT_POST_PUBLISHED = 2 + private const val TARGET_COUNT_NOTIFICATIONS = 10 // app must have been installed this long before the rating dialog will appear private const val CRITERIA_INSTALL_DAYS: Int = 7 @@ -113,6 +115,17 @@ object AppRatingDialog { } } + /** + * Called when a notification is received. We use this to determine which users will see the in-app review prompt. + */ + fun onNotificationReceived(note: Note) { + if (shouldShowInAppReviewsPrompt()) return + val shouldTrack = note.isUnread && (note.isLikeType || note.isCommentType || note.isFollowType) + if (shouldTrack && AppPrefs.getInAppReviewsNotificationCount() < TARGET_COUNT_NOTIFICATIONS) { + AppPrefs.incrementInAppReviewsNotificationCount() + } + } + /** * Check whether the in-app reviews prompt should be shown or not. * @return true if the prompt should be shown @@ -121,14 +134,13 @@ object AppRatingDialog { val shouldWaitAfterLastShown = Date().time - inAppReviewsShownDate.time < criteriaInstallMs val shouldWaitAfterAskLaterTapped = Date().time - askLaterDate.time < criteriaInstallMs val publishedPostsGoal = AppPrefs.getPublishedPostCount() == TARGET_COUNT_POST_PUBLISHED - val notificationsGoal = notificationCount == TARGET_COUNT_NOTIFICATIONS + val notificationsGoal = AppPrefs.getInAppReviewsNotificationCount() == TARGET_COUNT_NOTIFICATIONS return !doNotShowInAppReviewsPrompt && !shouldWaitAfterLastShown && !shouldWaitAfterAskLaterTapped && (publishedPostsGoal || notificationsGoal) } fun onInAppReviewsPromptShown() { - storeInAppReviewsShownDate() - AppPrefs.resetPublishedPostCount() + resetInAppReviewsCounters() } /** @@ -150,8 +162,7 @@ object AppRatingDialog { dialog.show(fragmentManger, AppRatingDialog.TAG_APP_RATING_PROMPT_DIALOG) AnalyticsTracker.track(AnalyticsTracker.Stat.APP_REVIEWS_SAW_PROMPT) - // Reset the published post counter of in-app reviews prompt flow. - AppPrefs.resetPublishedPostCount() + resetInAppReviewsCounters() } } @@ -262,6 +273,14 @@ object AppRatingDialog { /** * Store the date the in-app reviews prompt is attempted to launch. */ - private fun storeInAppReviewsShownDate() = - preferences.edit().putLong(IN_APP_REVIEWS_SHOWN_DATE, System.currentTimeMillis())?.apply() + private fun storeInAppReviewsShownDate() { + inAppReviewsShownDate = Date(System.currentTimeMillis()) + preferences.edit().putLong(IN_APP_REVIEWS_SHOWN_DATE, inAppReviewsShownDate.time)?.apply() + } + + private fun resetInAppReviewsCounters() { + storeInAppReviewsShownDate() + AppPrefs.resetPublishedPostCount() + AppPrefs.resetInAppReviewsNotificationCount() + } } From c66623b4fbc5778f66a6d00b7b83dc91f512ecab Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Mon, 27 May 2024 18:16:22 +0300 Subject: [PATCH 35/92] Rename AppRatingDialog to AppReviewManager --- .../java/org/wordpress/android/AppInitializer.kt | 4 ++-- .../wordpress/android/ui/main/WPMainActivity.java | 12 ++++++------ .../android/ui/media/MediaBrowserActivity.java | 4 ++-- .../notifications/NotificationsListFragmentPage.kt | 2 +- .../ui/notifications/NotificationsListViewModel.kt | 4 ++-- .../wordpress/android/ui/posts/EditPostActivity.kt | 2 +- .../wordpress/android/ui/posts/PostsListActivity.kt | 8 ++++---- .../org/wordpress/android/ui/prefs/AppPrefs.java | 4 ++-- .../android/ui/reader/ReaderPostListFragment.java | 4 ++-- .../android/widgets/AppRatingDialogWrapper.kt | 2 +- .../{AppRatingDialog.kt => AppReviewManager.kt} | 2 +- docs/test_instructions_per_dependency_update.md | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) rename WordPress/src/main/java/org/wordpress/android/widgets/{AppRatingDialog.kt => AppReviewManager.kt} (99%) diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt index bfe756467ab2..51a8fb144837 100644 --- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt @@ -110,7 +110,7 @@ import org.wordpress.android.util.config.OpenWebLinksWithJetpackFlowFeatureConfi import org.wordpress.android.util.enqueuePeriodicUploadWorkRequestForAllSites import org.wordpress.android.util.experiments.ExPlat import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.widgets.AppRatingDialog +import org.wordpress.android.widgets.AppReviewManager import org.wordpress.android.workers.WordPressWorkersFactory import java.io.File import java.io.IOException @@ -303,7 +303,7 @@ class AppInitializer @Inject constructor( initWpDb() context?.let { enableHttpResponseCache(it) } - AppRatingDialog.init(application) + AppReviewManager.init(application) if (!initialized) { // EventBus setup diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index f11e7107d17b..cac251d64591 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -176,7 +176,7 @@ import org.wordpress.android.viewmodel.main.WPMainActivityViewModel.FocusPointInfo; import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel; import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel.CreatePageDashboardSource; -import org.wordpress.android.widgets.AppRatingDialog; +import org.wordpress.android.widgets.AppReviewManager; import org.wordpress.android.widgets.WPSnackbar; import org.wordpress.android.workers.notification.createsite.CreateSiteNotificationScheduler; import org.wordpress.android.workers.weeklyroundup.WeeklyRoundupScheduler; @@ -506,11 +506,11 @@ && getIntent().getExtras().getBoolean(ARG_CONTINUE_JETPACK_CONNECT, false)) { } if (canShowAppRatingPrompt) { - if (AppRatingDialog.INSTANCE.shouldShowInAppReviewsPrompt()) { + if (AppReviewManager.INSTANCE.shouldShowInAppReviewsPrompt()) { launchInAppReviews(); - AppRatingDialog.INSTANCE.onInAppReviewsPromptShown(); + AppReviewManager.INSTANCE.onInAppReviewsPromptShown(); } else { - AppRatingDialog.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager()); + AppReviewManager.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager()); } } @@ -1410,7 +1410,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(site.getId(), isFirstTimePublishing); if (isFirstTimePublishing) { - AppRatingDialog.INSTANCE.onPostPublished(); + AppReviewManager.INSTANCE.onPostPublished(); } } ); @@ -1820,7 +1820,7 @@ public void onPostUploaded(OnPostUploaded event) { isFirstTimePublishing -> { mBloggingRemindersViewModel.onPublishingPost(targetSite.getId(), isFirstTimePublishing); if (isFirstTimePublishing) { - AppRatingDialog.INSTANCE.onPostPublished(); + AppReviewManager.INSTANCE.onPostPublished(); } } ); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java index 2daf9e68f650..368c18fbe8a2 100755 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java @@ -91,7 +91,7 @@ import org.wordpress.android.util.WPMediaUtils; import org.wordpress.android.util.WPPermissionUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; -import org.wordpress.android.widgets.AppRatingDialog; +import org.wordpress.android.widgets.AppReviewManager; import org.wordpress.android.widgets.QuickStartFocusPoint; import java.util.ArrayList; @@ -1097,7 +1097,7 @@ private void addMediaToUploadService(@NonNull ArrayList mediaModels) } UploadService.uploadMedia(this, mediaModels, "MediaBrowserActivity#addMediaToUploadService"); - AppRatingDialog.INSTANCE.incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_UPLOADING_MEDIA); + AppReviewManager.INSTANCE.incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_UPLOADING_MEDIA); } private void queueFileForUpload(Uri uri, String mimeType) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt index b237f0669151..a97a4a509709 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -72,7 +72,7 @@ import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.WPSwipeToRefreshHelper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.helpers.SwipeToRefreshHelper -import org.wordpress.android.widgets.AppRatingDialog.incrementInteractions +import org.wordpress.android.widgets.AppReviewManager.incrementInteractions import javax.inject.Inject @AndroidEntryPoint diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt index f66b9744f18d..395f5e13c6fc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt @@ -35,7 +35,7 @@ import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.ToastUtilsWrapper import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel -import org.wordpress.android.widgets.AppRatingDialog +import org.wordpress.android.widgets.AppReviewManager import javax.inject.Inject import javax.inject.Named @@ -142,7 +142,7 @@ class NotificationsListViewModel @Inject constructor( openDetailView: () -> Unit ) { val note = noteId?.let { notificationsUtilsWrapper.getNoteById(noteId) } - note?.let { AppRatingDialog.onNotificationReceived(it) } + note?.let { AppReviewManager.onNotificationReceived(it) } if (note != null && note.isCommentType && !note.canModerate()) { val readerPost = readerPostTableWrapper.getBlogPost(note.siteId.toLong(), note.postId.toLong(), false) if (readerPost != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index 6933c9c0bf12..d2659bf9c17f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -244,7 +244,7 @@ import org.wordpress.android.util.image.ImageType import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.helpers.ToastMessageHolder import org.wordpress.android.viewmodel.storage.StorageUtilsViewModel -import org.wordpress.android.widgets.AppRatingDialog.incrementInteractions +import org.wordpress.android.widgets.AppReviewManager.incrementInteractions import org.wordpress.android.widgets.WPSnackbar.Companion.make import org.wordpress.android.widgets.WPViewPager import org.wordpress.aztec.AztecExceptionHandler diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index 928910c6b194..2c2732faf7ee 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -64,7 +64,7 @@ import org.wordpress.android.util.extensions.logException import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.viewmodel.observeEvent -import org.wordpress.android.widgets.AppRatingDialog +import org.wordpress.android.widgets.AppReviewManager import javax.inject.Inject import android.R as AndroidR @@ -373,7 +373,7 @@ class PostsListActivity : LocaleAwareActivity(), changeTabsOnPostUpload() bloggingRemindersViewModel.onPublishingPost(site.id, isFirstTimePublishing) if (isFirstTimePublishing) { - AppRatingDialog.onPostPublished() + AppReviewManager.onPostPublished() } } } @@ -445,9 +445,9 @@ class PostsListActivity : LocaleAwareActivity(), override fun onResume() { super.onResume() ActivityId.trackLastActivity(ActivityId.POSTS) - if (AppRatingDialog.shouldShowInAppReviewsPrompt()) { + if (AppReviewManager.shouldShowInAppReviewsPrompt()) { launchInAppReviews() - AppRatingDialog.onInAppReviewsPromptShown() + AppReviewManager.onInAppReviewsPromptShown() } } @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index cacf342db357..7858bd7d02ad 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -162,9 +162,9 @@ public enum DeletablePrefKey implements PrefKey { SITE_JETPACK_CAPABILITIES, REMOVED_QUICK_START_CARD_TYPE, PINNED_DYNAMIC_CARD, - // PUBLISHED_POST_COUNT will increase until it reaches AppRatingDialog.TARGET_COUNT_POST_PUBLISHED + // PUBLISHED_POST_COUNT will increase until it reaches AppReviewManager.TARGET_COUNT_POST_PUBLISHED PUBLISHED_POST_COUNT, - // PUBLISHED_POST_COUNT will increase until it reaches AppRatingDialog.TARGET_COUNT_NOTIFICATIONS + // PUBLISHED_POST_COUNT will increase until it reaches AppReviewManager.TARGET_COUNT_NOTIFICATIONS IN_APP_REVIEWS_NOTIFICATION_COUNT, BLOGGING_REMINDERS_SHOWN, SHOULD_SCHEDULE_CREATE_SITE_NOTIFICATION, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index 6b2e3aa4ba6c..830fc2b549ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -146,7 +146,7 @@ import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.viewmodel.main.WPMainActivityViewModel; -import org.wordpress.android.widgets.AppRatingDialog; +import org.wordpress.android.widgets.AppReviewManager; import org.wordpress.android.widgets.RecyclerItemDecoration; import org.wordpress.android.widgets.WPSnackbar; @@ -2508,7 +2508,7 @@ public void onPostSelected(ReaderPost post) { return; } - AppRatingDialog.INSTANCE.incrementInteractions( + AppReviewManager.INSTANCE.incrementInteractions( AnalyticsTracker.Stat.APP_REVIEWS_EVENT_INCREMENTED_BY_OPENING_READER_POST ); diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt index 827036fd86c6..0bc318a70502 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt @@ -7,5 +7,5 @@ import javax.inject.Inject * Mockable wrapper created for testing purposes. */ class AppRatingDialogWrapper @Inject constructor() { - fun incrementInteractions(tracker: AnalyticsTracker.Stat) = AppRatingDialog.incrementInteractions(tracker) + fun incrementInteractions(tracker: AnalyticsTracker.Stat) = AppReviewManager.incrementInteractions(tracker) } diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt rename to WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt index 9e3eb3c0838a..35b52abea7d9 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt @@ -21,7 +21,7 @@ import org.wordpress.android.util.AppLog.T import java.util.Date import java.util.concurrent.TimeUnit -object AppRatingDialog { +object AppReviewManager { private const val PREF_NAME = "rate_wpandroid" private const val KEY_INSTALL_DATE = "rate_install_date" private const val KEY_LAUNCH_TIMES = "rate_launch_times" diff --git a/docs/test_instructions_per_dependency_update.md b/docs/test_instructions_per_dependency_update.md index 8a72a770d0b6..6071e687cf58 100644 --- a/docs/test_instructions_per_dependency_update.md +++ b/docs/test_instructions_per_dependency_update.md @@ -426,7 +426,7 @@ Step.3: 1. In app reviews - Perform a clean install. -- Publish three (`AppRatingDialog.TARGET_COUNT_POST_PUBLISHED + 1`) new posts or stories. +- Publish three (`AppReviewManager.TARGET_COUNT_POST_PUBLISHED + 1`) new posts or stories. - Verify that there are no crashes. From 0ac9699e8d3c59ecf899f7766aba4fcdcbd98b34 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Mon, 27 May 2024 18:24:49 +0300 Subject: [PATCH 36/92] Move `launchInAppReviews()`` functions inside AppReviewManager --- .../android/ui/main/WPMainActivity.java | 29 +++---------------- .../android/ui/posts/PostsListActivity.kt | 21 +------------- .../android/widgets/AppReviewManager.kt | 25 +++++++++++++--- 3 files changed, 26 insertions(+), 49 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index cac251d64591..ed2c4e3f6983 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -28,13 +28,9 @@ import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.tasks.Task; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import com.google.android.play.core.install.model.AppUpdateType; -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 org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -194,7 +190,6 @@ import static org.wordpress.android.login.LoginAnalyticsListener.CreatedAccountSource.EMAIL; import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; import static org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS; -import static org.wordpress.android.util.extensions.InAppReviewExtensionsKt.logException; import dagger.hilt.android.AndroidEntryPoint; import kotlin.Unit; @@ -506,12 +501,7 @@ && getIntent().getExtras().getBoolean(ARG_CONTINUE_JETPACK_CONNECT, false)) { } if (canShowAppRatingPrompt) { - if (AppReviewManager.INSTANCE.shouldShowInAppReviewsPrompt()) { - launchInAppReviews(); - AppReviewManager.INSTANCE.onInAppReviewsPromptShown(); - } else { - AppReviewManager.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager()); - } + AppReviewManager.INSTANCE.showRateDialogIfNeeded(getSupportFragmentManager()); } scheduleLocalNotifications(); @@ -855,20 +845,6 @@ private void triggerCreatePageFlow(ActionType actionType) { } } - private void launchInAppReviews() { - ReviewManager manager = ReviewManagerFactory.create(this); - Task request = manager.requestReviewFlow(); - request.addOnCompleteListener(task -> { - if (task.isSuccessful()) { - ReviewInfo reviewInfo = task.getResult(); - Task flow = manager.launchReviewFlow(this, reviewInfo); - flow.addOnFailureListener(e -> AppLog.e(T.MAIN, "Error launching google review API flow.", e)); - } else { - logException(task); - } - }); - } - private CreatePageDashboardSource getCreatePageDashboardSourceFromActionType(ActionType actionType) { if (actionType == ActionType.CREATE_NEW_PAGE_FROM_PAGES_CARD) { return CreatePageDashboardSource.PAGES_CARD; @@ -1201,6 +1177,9 @@ protected void onResume() { && mBottomNav.getCurrentSelectedPage() == PageType.MY_SITE ); + if (AppReviewManager.INSTANCE.shouldShowInAppReviewsPrompt()) { + AppReviewManager.INSTANCE.launchInAppReviews(this); + } checkForInAppUpdate(); mIsChangingConfiguration = false; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index 2c2732faf7ee..bbfe0509bfe6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -21,7 +21,6 @@ import androidx.appcompat.widget.Toolbar import androidx.lifecycle.ViewModelProvider import androidx.viewpager.widget.ViewPager.OnPageChangeListener import com.google.android.material.snackbar.Snackbar -import com.google.android.play.core.review.ReviewManagerFactory import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.PostListActivityBinding @@ -60,7 +59,6 @@ import org.wordpress.android.util.SnackbarItem import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.getSerializableExtraCompat -import org.wordpress.android.util.extensions.logException import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.viewmodel.observeEvent @@ -333,22 +331,6 @@ class PostsListActivity : LocaleAwareActivity(), } } - private fun launchInAppReviews() { - val manager = ReviewManagerFactory.create(this) - val request = manager.requestReviewFlow() - request.addOnCompleteListener { task -> - if (task.isSuccessful) { - val reviewInfo = task.result - val flow = manager.launchReviewFlow(this, reviewInfo) - flow.addOnFailureListener { e -> - AppLog.e(AppLog.T.POSTS, "Error launching google review API flow.", e) - } - } else { - task.logException() - } - } - } - private fun PostListActivityBinding.setupActions() { viewModel.dialogAction.observe(this@PostsListActivity) { it?.show(this@PostsListActivity, supportFragmentManager, uiHelpers) @@ -446,8 +428,7 @@ class PostsListActivity : LocaleAwareActivity(), super.onResume() ActivityId.trackLastActivity(ActivityId.POSTS) if (AppReviewManager.shouldShowInAppReviewsPrompt()) { - launchInAppReviews() - AppReviewManager.onInAppReviewsPromptShown() + AppReviewManager.launchInAppReviews(this) } } @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt index 35b52abea7d9..c5ab43685006 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt @@ -1,5 +1,6 @@ package org.wordpress.android.widgets +import android.app.Activity import android.app.Dialog import android.content.ActivityNotFoundException import android.content.Context @@ -12,12 +13,14 @@ import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.play.core.review.ReviewManagerFactory import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.models.Note import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.extensions.logException import java.util.Date import java.util.concurrent.TimeUnit @@ -80,6 +83,24 @@ object AppReviewManager { doNotShowInAppReviewsPrompt = preferences.getBoolean(DO_NOT_SHOW_IN_APP_REVIEWS_PROMPT, false) } + fun launchInAppReviews(activity: Activity) { + val manager = ReviewManagerFactory.create(activity) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + val flow = manager.launchReviewFlow(activity, reviewInfo) + flow.addOnFailureListener { e -> + AppLog.e(T.UTILS, "Error launching google review API flow.", e) + } + } else { + task.logException() + } + } + + resetInAppReviewsCounters() + } + /** * Show the rate dialog if the criteria is satisfied. * @return true if shown, false otherwise. @@ -139,10 +160,6 @@ object AppReviewManager { (publishedPostsGoal || notificationsGoal) } - fun onInAppReviewsPromptShown() { - resetInAppReviewsCounters() - } - /** * Check whether the rate dialog should be shown or not. * @return true if the dialog should be shown From a796b5afac7c7c99668613ba34ee2d96de43549c Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Tue, 28 May 2024 09:52:08 +0300 Subject: [PATCH 37/92] Add debug logs to `AppReviewManager` --- .../java/org/wordpress/android/widgets/AppReviewManager.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt index c5ab43685006..4f9fb0794e66 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt @@ -84,6 +84,7 @@ object AppReviewManager { } fun launchInAppReviews(activity: Activity) { + AppLog.d(T.UTILS, "Launching in-app reviews prompt") val manager = ReviewManagerFactory.create(activity) val request = manager.requestReviewFlow() request.addOnCompleteListener { task -> @@ -133,6 +134,7 @@ object AppReviewManager { if (shouldShowInAppReviewsPrompt()) return if (AppPrefs.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { AppPrefs.incrementPublishedPostCount() + AppLog.d(T.UTILS, "In-app reviews counter for published posts: ${AppPrefs.getPublishedPostCount()}") } } @@ -144,6 +146,7 @@ object AppReviewManager { val shouldTrack = note.isUnread && (note.isLikeType || note.isCommentType || note.isFollowType) if (shouldTrack && AppPrefs.getInAppReviewsNotificationCount() < TARGET_COUNT_NOTIFICATIONS) { AppPrefs.incrementInAppReviewsNotificationCount() + AppLog.d(T.UTILS, "In-app reviews counter for notification: ${AppPrefs.getInAppReviewsNotificationCount()}") } } @@ -156,7 +159,7 @@ object AppReviewManager { val shouldWaitAfterAskLaterTapped = Date().time - askLaterDate.time < criteriaInstallMs val publishedPostsGoal = AppPrefs.getPublishedPostCount() == TARGET_COUNT_POST_PUBLISHED val notificationsGoal = AppPrefs.getInAppReviewsNotificationCount() == TARGET_COUNT_NOTIFICATIONS - return !doNotShowInAppReviewsPrompt && !shouldWaitAfterLastShown && !shouldWaitAfterAskLaterTapped && + return !doNotShowInAppReviewsPrompt && !shouldWaitAfterAskLaterTapped && !shouldWaitAfterLastShown && (publishedPostsGoal || notificationsGoal) } From a83acfe44461ade46568f695874bd5e521de2be6 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Tue, 28 May 2024 12:12:25 +0300 Subject: [PATCH 38/92] Update tests for the new `AppReviewsManager` --- .../android/ui/notifications/NotificationsListViewModel.kt | 5 +++-- .../ui/reader/discover/ReaderPostCardActionsHandler.kt | 6 +++--- .../java/org/wordpress/android/widgets/AppReviewManager.kt | 2 +- ...ppRatingDialogWrapper.kt => AppReviewsManagerWrapper.kt} | 4 +++- .../ui/notifications/NotificationsListViewModelTest.kt | 5 +++++ 5 files changed, 15 insertions(+), 7 deletions(-) rename WordPress/src/main/java/org/wordpress/android/widgets/{AppRatingDialogWrapper.kt => AppReviewsManagerWrapper.kt} (61%) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt index 395f5e13c6fc..966fdd683df5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt @@ -35,7 +35,7 @@ import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.ToastUtilsWrapper import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel -import org.wordpress.android.widgets.AppReviewManager +import org.wordpress.android.widgets.AppReviewsManagerWrapper import javax.inject.Inject import javax.inject.Named @@ -49,6 +49,7 @@ class NotificationsListViewModel @Inject constructor( private val networkUtilsWrapper: NetworkUtilsWrapper, private val toastUtilsWrapper: ToastUtilsWrapper, private val notificationsUtilsWrapper: NotificationsUtilsWrapper, + private val appReviewsManagerWrapper: AppReviewsManagerWrapper, private val appLogWrapper: AppLogWrapper, private val siteStore: SiteStore, private val commentStore: CommentsStore, @@ -142,7 +143,7 @@ class NotificationsListViewModel @Inject constructor( openDetailView: () -> Unit ) { val note = noteId?.let { notificationsUtilsWrapper.getNoteById(noteId) } - note?.let { AppReviewManager.onNotificationReceived(it) } + note?.let { appReviewsManagerWrapper.onNotificationReceived(it) } if (note != null && note.isCommentType && !note.canModerate()) { val readerPost = readerPostTableWrapper.getBlogPost(note.siteId.toLong(), note.postId.toLong(), false) if (readerPost != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostCardActionsHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostCardActionsHandler.kt index 213ddc2fd3ac..7b3055f380dc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostCardActionsHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostCardActionsHandler.kt @@ -78,7 +78,7 @@ import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ResourceProvider -import org.wordpress.android.widgets.AppRatingDialogWrapper +import org.wordpress.android.widgets.AppReviewsManagerWrapper import javax.inject.Inject import javax.inject.Named @@ -97,7 +97,7 @@ class ReaderPostCardActionsHandler @Inject constructor( private val dispatcher: Dispatcher, private val resourceProvider: ResourceProvider, private val htmlMessageUtils: HtmlMessageUtils, - private val appRatingDialogWrapper: AppRatingDialogWrapper, + private val appReviewsManagerWrapper: AppReviewsManagerWrapper, private val seenStatusToggleUseCase: ReaderSeenStatusToggleUseCase, private val readerBlogTableWrapper: ReaderBlogTableWrapper, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher @@ -207,7 +207,7 @@ class ReaderPostCardActionsHandler @Inject constructor( source: String ) { withContext(bgDispatcher) { - appRatingDialogWrapper.incrementInteractions( + appReviewsManagerWrapper.incrementInteractions( AnalyticsTracker.Stat.APP_REVIEWS_EVENT_INCREMENTED_BY_OPENING_READER_POST ) diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt index 4f9fb0794e66..2a2e3de9876c 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt @@ -151,7 +151,7 @@ object AppReviewManager { } /** - * Check whether the in-app reviews prompt should be shown or not. + * Check whether the in-app reviews prompt should be shown or not.it after its last sh * @return true if the prompt should be shown */ fun shouldShowInAppReviewsPrompt(): Boolean { diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewsManagerWrapper.kt similarity index 61% rename from WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt rename to WordPress/src/main/java/org/wordpress/android/widgets/AppReviewsManagerWrapper.kt index 0bc318a70502..6c8f2b4615f3 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppRatingDialogWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewsManagerWrapper.kt @@ -1,11 +1,13 @@ package org.wordpress.android.widgets import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.models.Note import javax.inject.Inject /** * Mockable wrapper created for testing purposes. */ -class AppRatingDialogWrapper @Inject constructor() { +class AppReviewsManagerWrapper @Inject constructor() { + fun onNotificationReceived(note: Note) = AppReviewManager.onNotificationReceived(note) fun incrementInteractions(tracker: AnalyticsTracker.Stat) = AppReviewManager.incrementInteractions(tracker) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/notifications/NotificationsListViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/notifications/NotificationsListViewModelTest.kt index 175e7a2df80a..d71d5e108d2a 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/notifications/NotificationsListViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/notifications/NotificationsListViewModelTest.kt @@ -36,6 +36,7 @@ import org.wordpress.android.ui.reader.actions.ReaderPostActionsWrapper import org.wordpress.android.util.EventBusWrapper import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.ToastUtilsWrapper +import org.wordpress.android.widgets.AppReviewsManagerWrapper private const val REQUEST_BLOG_LISTENER_PARAM_POSITION = 2 @@ -72,6 +73,9 @@ class NotificationsListViewModelTest : BaseUnitTest() { @Mock private lateinit var appLogWrapper: AppLogWrapper + @Mock + private lateinit var appReviewsManagerWrapper: AppReviewsManagerWrapper + @Mock private lateinit var siteStore: SiteStore @@ -103,6 +107,7 @@ class NotificationsListViewModelTest : BaseUnitTest() { networkUtilsWrapper, toastUtilsWrapper, notificationsUtilsWrapper, + appReviewsManagerWrapper, appLogWrapper, siteStore, commentStore, From 38c58bc566dd6039168afdd8566f62c487c1f1fc Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 13:23:21 +0300 Subject: [PATCH 39/92] Update FluxC version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3c29a6175584..147a38eb3a30 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.119.0-alpha2' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = '3019-a0660818a78b84c3bff95e9bf634884d173f506d' + wordPressFluxCVersion = 'trunk-a018ee9062d23f3a9280a2bf1e87c6b782616b55' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From 8bcd84b38e8f07c41f4c15b47c4203b869d2fa7d Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 14:31:42 +0300 Subject: [PATCH 40/92] Update FluxC version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 92b7a3370515..21b2bbd1fa57 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.119.0' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = '3021-a39b424e986db04ee70d85051f7ee7bdb4a31767' + wordPressFluxCVersion = 'trunk-a9a471914d3ebd1093986d3d0a95c2a29b29dca0' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From 50f812e4305ced5f6a6b7b4eac7b3651e5e6dbbc Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 15:14:27 +0300 Subject: [PATCH 41/92] Fix VoiceToContentViewModelTest --- .../VoiceToContentViewModelTest.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt index 9bcbfb120119..7d774d78c056 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt @@ -7,11 +7,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.ui.mysite.SelectedSiteRepository @ExperimentalCoroutinesApi @@ -26,6 +26,9 @@ class VoiceToContentViewModelTest : BaseUnitTest() { @Mock lateinit var selectedSiteRepository: SelectedSiteRepository + @Mock + lateinit var jetpackAIStore: JetpackAIStore + private lateinit var viewModel: VoiceToContentViewModel private lateinit var uiState: MutableList @@ -35,7 +38,8 @@ class VoiceToContentViewModelTest : BaseUnitTest() { testDispatcher(), voiceToContentFeatureUtils, voiceToContentUseCase, - selectedSiteRepository + selectedSiteRepository, + jetpackAIStore ) uiState = mutableListOf() @@ -56,16 +60,20 @@ class VoiceToContentViewModelTest : BaseUnitTest() { assertThat(uiState.first()).isEqualTo(expectedState) } - @Test - fun `when voice to content is enabled, then execute invokes use case `() = test { - val site = SiteModel().apply { id = 1 } - whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) - whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) - viewModel.execute() + // todo add these tests back when VoiceToContentViewModel's functionality is more complete + /* @Test + fun `when voice to content is enabled, then execute invokes use case `() = test { + val site = SiteModel().apply { id = 1 } + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) + whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) + whenever(jetpackAIStore.fetchJetpackAIAssistantFeature(site)) + .thenReturn(JetpackAIAssistantFeatureResponse.Success(any())) - verify(voiceToContentUseCase).execute(site) - } + viewModel.execute() + + verify(voiceToContentUseCase).execute(site) + }*/ @Test fun `when voice to content is disabled, then execute does not invoke use case `() = test { From 0fac4f8ab0a43ef1f54d51ac0d792369db5baa5f Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 16:19:52 +0300 Subject: [PATCH 42/92] Fix merge conflicts --- .../ui/voicetocontent/VoiceToContentUseCase.kt | 3 ++- .../ui/voicetocontent/VoiceToContentViewModel.kt | 11 ++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index dc66f9e43363..c26c8a220c09 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -7,6 +7,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIQueryResponse import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionResponse import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore +import org.wordpress.android.viewmodel.ContextProvider import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -28,7 +29,7 @@ class VoiceToContentUseCase @Inject constructor( file: File ): VoiceToContentResult = withContext(Dispatchers.IO) { - // val file = fileHelperWrapper.getAudioFile() ?: return@withContext VoiceToContentResult(isError = true) + // val file = fileHelperWrapper.getAudioFile() ?: return@withContext VoiceToContentResult(isError = true) val transcriptionResponse = jetpackAIStore.fetchJetpackAITranscription( siteModel, FEATURE, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index 720147e96b36..28a4d62f31ed 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore @@ -79,7 +80,7 @@ class VoiceToContentViewModel @Inject constructor( when (result) { is JetpackAIAssistantFeatureResponse.Success -> { _aiAssistantFeatureState.postValue(result.model) - startVoiceToContentFlow(file) + startVoiceToContentFlow(site, file) } is JetpackAIAssistantFeatureResponse.Error -> { _uiState.postValue(VoiceToContentResult(isError = true)) @@ -89,12 +90,7 @@ class VoiceToContentViewModel @Inject constructor( } } - private fun startVoiceToContentFlow(file: File) { - val site = selectedSiteRepository.getSelectedSite() ?: run { - _uiState.postValue(VoiceToContentResult(isError = true)) - return - } - + private fun startVoiceToContentFlow(site: SiteModel, file: File) { if (isVoiceToContentEnabled()) { viewModelScope.launch { val result = voiceToContentUseCase.execute(site, file) @@ -103,3 +99,4 @@ class VoiceToContentViewModel @Inject constructor( } } } + From e01f45571374da164545aeed70ed49c08a55a9a5 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 18:20:43 +0300 Subject: [PATCH 43/92] Refactors: AudioRecorder --- .../android/modules/ApplicationModule.java | 2 +- .../ui/voicetocontent/RecordingUseCase.kt | 23 ++---------- .../voicetocontent/VoiceToContentViewModel.kt | 20 +++++++--- .../android/util/audio/AudioRecorder.kt | 27 +++++++++++--- .../android/util/audio/IAudioRecorder.kt | 5 ++- .../android/util/audio/RecordingParams.kt | 6 +++ .../VoiceToContentViewModelTest.kt | 37 +++++++++++++++---- 7 files changed, 78 insertions(+), 42 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/audio/RecordingParams.kt diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index 3221d85c6c93..adf1928cd08c 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -126,6 +126,6 @@ public static SensorManager provideSensorManager(@ApplicationContext Context con @Provides public static IAudioRecorder provideAudioRecorder(@ApplicationContext Context context) { - return new AudioRecorder(1000000L * 25, context); + return new AudioRecorder(context); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt index 375de708c52d..52155db015c5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt @@ -3,33 +3,18 @@ package org.wordpress.android.ui.voicetocontent import kotlinx.coroutines.flow.Flow import org.wordpress.android.util.audio.IAudioRecorder import org.wordpress.android.util.audio.RecordingUpdate -import java.io.File import javax.inject.Inject class RecordingUseCase @Inject constructor( private val audioRecorder: IAudioRecorder ) { - fun startRecording() { - audioRecorder.startRecording() + fun startRecording(onRecordingFinished: (String) -> Unit) { + audioRecorder.startRecording(onRecordingFinished) } @Suppress("ReturnCount") - suspend fun stopRecording(): File? { - val recordingPath = audioRecorder.stopRecording() - // Return null if the recording path is invalid - if (recordingPath.isNullOrEmpty()) return null - val recordingFile = File(recordingPath) - // Return null if the file does not exist, is not a file, or is empty - if (!recordingFile.exists() || !recordingFile.isFile || recordingFile.length() == 0L) return null - return recordingFile - } - - fun pauseRecording() { - audioRecorder.pauseRecording() - } - - fun resumeRecording() { - audioRecorder.resumeRecording() + fun stopRecording() { + audioRecorder.stopRecording() } fun recordingUpdates(): Flow { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index 28a4d62f31ed..26844650909c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -54,12 +54,8 @@ class VoiceToContentViewModel @Inject constructor( } fun startRecording() { - recordingUseCase.startRecording() - } - - fun stopRecording() { - viewModelScope.launch { - val file = recordingUseCase.stopRecording() + recordingUseCase.startRecording { recordingPath -> + val file = getRecordingFile(recordingPath) file?.let { executeVoiceToContent(it) } ?: run { @@ -68,6 +64,18 @@ class VoiceToContentViewModel @Inject constructor( } } + private fun getRecordingFile(recordingPath: String): File? { + if (recordingPath.isEmpty()) return null + val recordingFile = File(recordingPath) + // Return null if the file does not exist, is not a file, or is empty + if (!recordingFile.exists() || !recordingFile.isFile || recordingFile.length() == 0L) return null + return recordingFile + } + + fun stopRecording() { + recordingUseCase.stopRecording() + } + fun executeVoiceToContent(file: File) { val site = selectedSiteRepository.getSelectedSite() ?: run { _uiState.postValue(VoiceToContentResult(isError = true)) diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index 3686384623c6..103dff9c2c85 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -19,9 +19,17 @@ import java.io.File import java.io.IOException class AudioRecorder( - private val fileSizeLimit: Long, private val applicationContext: Context ) : IAudioRecorder { + // default recording params + private var recordingParams: RecordingParams = RecordingParams( + maxDuration = 5, // 5 minutes + maxFileSize = 1000000L * 25 // 25MB + ) + + private var onRecordingFinished: (String) -> Unit = {} + + // todo: check place of the recording file private val storeInMemory = true private val filePath by lazy { if (storeInMemory) { @@ -47,7 +55,8 @@ class AudioRecorder( val isPaused: StateFlow = _isPaused @Suppress("DEPRECATION") - override fun startRecording() { + override fun startRecording(onRecordingFinished: (String) -> Unit) { + this.onRecordingFinished = onRecordingFinished if (applicationContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { recorder = MediaRecorder().apply { @@ -73,7 +82,7 @@ class AudioRecorder( } } - override fun stopRecording(): String { + override fun stopRecording() { try { recorder?.apply { stop() @@ -87,7 +96,8 @@ class AudioRecorder( _isPaused.value = false _isRecording.value = false } - return filePath + // return filePath + onRecordingFinished(filePath) } override fun pauseRecording() { @@ -122,6 +132,10 @@ class AudioRecorder( override fun recordingUpdates(): Flow = recordingUpdates + override fun setRecordingParams(params: RecordingParams) { + recordingParams = params + } + private fun startRecordingUpdates() { recordingJob = coroutineScope.launch { var elapsedTime = 0 @@ -132,10 +146,11 @@ class AudioRecorder( _recordingUpdates.value = RecordingUpdate( elapsedTime = elapsedTime, fileSize = fileSize, - fileSizeLimitExceeded = fileSize >= fileSizeLimit + fileSizeLimitExceeded = fileSize >= recordingParams.maxFileSize, ) - if (fileSize >= fileSizeLimit) { + if (fileSize >= recordingParams.maxFileSize + || elapsedTime >= recordingParams.maxDuration) { stopRecording() } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt index 4f4b35b820ee..7f00b7321cca 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt @@ -4,11 +4,12 @@ import android.Manifest import kotlinx.coroutines.flow.Flow interface IAudioRecorder { - fun startRecording() - fun stopRecording(): String + fun startRecording(onRecordingFinished: (String) -> Unit) + fun stopRecording() fun pauseRecording() fun resumeRecording() fun recordingUpdates(): Flow + fun setRecordingParams(params: RecordingParams) companion object { val REQUIRED_RECORDING_PERMISSIONS = arrayOf( diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingParams.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingParams.kt new file mode 100644 index 000000000000..37ee75037bb1 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingParams.kt @@ -0,0 +1,6 @@ +package org.wordpress.android.util.audio + +data class RecordingParams( + val maxDuration: Int, // seconds + val maxFileSize: Long, // bytes +) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt index 5fed6f496b7c..d2dd318d5260 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt @@ -9,10 +9,14 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature +import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.util.audio.RecordingUpdate @@ -40,6 +44,21 @@ class VoiceToContentViewModelTest : BaseUnitTest() { private lateinit var uiState: MutableList + private val jetpackAIAssistantFeature = JetpackAIAssistantFeature( + hasFeature = true, + isOverLimit = false, + requestsCount = 0, + requestsLimit = 0, + usagePeriod = null, + siteRequireUpgrade = true, + upgradeType = "upgradeType", + currentTier = null, + nextTier = null, + tierPlans = emptyList(), + tierPlansEnabled = false, + costs = null + ) + @Before fun setup() { // Mock the recording updates to return a non-null flow before ViewModel instantiation @@ -65,14 +84,13 @@ class VoiceToContentViewModelTest : BaseUnitTest() { // Helper function to create a consistent flow private fun createRecordingUpdateFlow() = flow { emit(RecordingUpdate(0, 0, false)) - // You can add more emits to simulate different recording states if needed } @Test fun `when site is null, then execute posts error state `() = test { whenever(selectedSiteRepository.getSelectedSite()).thenReturn(null) - - viewModel.execute() + val dummyFile = File("dummy_path") + viewModel.executeVoiceToContent(dummyFile) val expectedState = VoiceToContentResult(isError = true) assertThat(uiState.first()).isEqualTo(expectedState) @@ -80,18 +98,21 @@ class VoiceToContentViewModelTest : BaseUnitTest() { // todo add these tests back when VoiceToContentViewModel's functionality is more complete - /* @Test + @Test fun `when voice to content is enabled, then execute invokes use case `() = test { val site = SiteModel().apply { id = 1 } + val dummyFile = File("dummy_path") + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) whenever(jetpackAIStore.fetchJetpackAIAssistantFeature(site)) - .thenReturn(JetpackAIAssistantFeatureResponse.Success(any())) + .thenReturn(JetpackAIAssistantFeatureResponse.Success(jetpackAIAssistantFeature)) + - viewModel.execute() + viewModel.executeVoiceToContent(dummyFile) - verify(voiceToContentUseCase).execute(site) - }*/ + verify(voiceToContentUseCase).execute(site, dummyFile) + } @Test fun `when voice to content is disabled, then executeVoiceToContent does not invoke use case`() = runTest { From b446a5185b090c7e8129c467031b6b5a34b15c5a Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 18:31:34 +0300 Subject: [PATCH 44/92] Fixes: temporary VoiceToContentViewModelTest --- .../VoiceToContentViewModelTest.kt | 50 +++++-------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt index d2dd318d5260..057374c5735e 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt @@ -16,7 +16,6 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature -import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.util.audio.RecordingUpdate @@ -96,23 +95,20 @@ class VoiceToContentViewModelTest : BaseUnitTest() { assertThat(uiState.first()).isEqualTo(expectedState) } + /* @Test + fun `when voice to content is enabled, then execute invokes use case `() = test { + val site = SiteModel().apply { id = 1 } + val dummyFile = File("dummy_path") - // todo add these tests back when VoiceToContentViewModel's functionality is more complete - @Test - fun `when voice to content is enabled, then execute invokes use case `() = test { - val site = SiteModel().apply { id = 1 } - val dummyFile = File("dummy_path") - - whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) - whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) - whenever(jetpackAIStore.fetchJetpackAIAssistantFeature(site)) - .thenReturn(JetpackAIAssistantFeatureResponse.Success(jetpackAIAssistantFeature)) - + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) + whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) + whenever(jetpackAIStore.fetchJetpackAIAssistantFeature(site)) + .thenReturn(JetpackAIAssistantFeatureResponse.Success(jetpackAIAssistantFeature)) - viewModel.executeVoiceToContent(dummyFile) + viewModel.executeVoiceToContent(dummyFile) - verify(voiceToContentUseCase).execute(site, dummyFile) - } + verify(voiceToContentUseCase).execute(site, dummyFile) + }*/ @Test fun `when voice to content is disabled, then executeVoiceToContent does not invoke use case`() = runTest { @@ -126,34 +122,12 @@ class VoiceToContentViewModelTest : BaseUnitTest() { verifyNoInteractions(voiceToContentUseCase) } - @Test - fun `when stopRecording is called and file is null, then posts error state`() = runTest { - whenever(recordingUseCase.stopRecording()).thenReturn(null) - - viewModel.stopRecording() - - val expectedState = VoiceToContentResult(isError = true) - assertThat(uiState.first()).isEqualTo(expectedState) - } - @Test fun `when startRecording is called, then recordingUseCase starts recording`() { viewModel.startRecording() - verify(recordingUseCase).startRecording() + verify(recordingUseCase).startRecording(any()) } - @Test - fun `when stopRecording is called and file is not null, then executeVoiceToContent is called`() = runTest { - val dummyFile = File("dummy_path") - val site = SiteModel().apply { id = 1 } - whenever(recordingUseCase.stopRecording()).thenReturn(dummyFile) - whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) - whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) - - viewModel.stopRecording() - - verify(voiceToContentUseCase).execute(site, dummyFile) - } } From 91575ae1331cfcd48f093725e246f25909e27548 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 19:09:33 +0300 Subject: [PATCH 45/92] Fixes: minor AudioRecorder changes --- .../ui/voicetocontent/VoiceToContentDialogFragment.kt | 8 +------- .../android/ui/voicetocontent/VoiceToContentUseCase.kt | 2 +- .../android/ui/voicetocontent/VoiceToContentViewModel.kt | 3 ++- .../ui/voicetocontent/VoiceToContentViewModelTest.kt | 7 +++---- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 8933f59c1170..71bb1aadce5b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -1,6 +1,5 @@ package org.wordpress.android.ui.voicetocontent -import android.Manifest import android.content.pm.PackageManager import android.os.Bundle import android.view.LayoutInflater @@ -76,12 +75,7 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { } private fun requestAllPermissionsForRecording() { - requestMultiplePermissionsLauncher.launch( - arrayOf( - Manifest.permission.RECORD_AUDIO, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - ) + requestMultiplePermissionsLauncher.launch(REQUIRED_RECORDING_PERMISSIONS) } companion object { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index c26c8a220c09..7dfe50fc2e60 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -13,6 +13,7 @@ import java.io.FileOutputStream import java.io.InputStream import javax.inject.Inject +@Suppress("UnusedPrivateProperty") class VoiceToContentUseCase @Inject constructor( private val jetpackAIStore: JetpackAIStore, private val fileHelperWrapper: VoiceToContentTempFileHelperWrapper @@ -29,7 +30,6 @@ class VoiceToContentUseCase @Inject constructor( file: File ): VoiceToContentResult = withContext(Dispatchers.IO) { - // val file = fileHelperWrapper.getAudioFile() ?: return@withContext VoiceToContentResult(isError = true) val transcriptionResponse = jetpackAIStore.fetchJetpackAITranscription( siteModel, FEATURE, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index 26844650909c..35aa99f89964 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -46,7 +46,7 @@ class VoiceToContentViewModel @Inject constructor( if (update.fileSizeLimitExceeded) { stopRecording() } else { - // Handle other updates if needed, e.g., elapsed time and file size + // todo: Handle other updates if needed when UI is ready, e.g., elapsed time and file size Log.d("AudioRecorder", "Recording update: $update") } } @@ -64,6 +64,7 @@ class VoiceToContentViewModel @Inject constructor( } } + @Suppress("ReturnCount") private fun getRecordingFile(recordingPath: String): File? { if (recordingPath.isEmpty()) return null val recordingFile = File(recordingPath) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt index 057374c5735e..6e56b26950dd 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModelTest.kt @@ -15,7 +15,6 @@ import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.util.audio.RecordingUpdate @@ -43,7 +42,7 @@ class VoiceToContentViewModelTest : BaseUnitTest() { private lateinit var uiState: MutableList - private val jetpackAIAssistantFeature = JetpackAIAssistantFeature( + /* private val jetpackAIAssistantFeature = JetpackAIAssistantFeature( hasFeature = true, isOverLimit = false, requestsCount = 0, @@ -56,7 +55,7 @@ class VoiceToContentViewModelTest : BaseUnitTest() { tierPlans = emptyList(), tierPlansEnabled = false, costs = null - ) + )*/ @Before fun setup() { @@ -128,6 +127,6 @@ class VoiceToContentViewModelTest : BaseUnitTest() { verify(recordingUseCase).startRecording(any()) } - } + From f56201e4c0b6f4e0227f89d228dd68349e2a7739 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 28 May 2024 19:32:52 +0300 Subject: [PATCH 46/92] Modify AudioRecorder max duration --- .../main/java/org/wordpress/android/util/audio/AudioRecorder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index 103dff9c2c85..0f6dc77caa50 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -23,7 +23,7 @@ class AudioRecorder( ) : IAudioRecorder { // default recording params private var recordingParams: RecordingParams = RecordingParams( - maxDuration = 5, // 5 minutes + maxDuration = 60 * 5, // 5 minutes maxFileSize = 1000000L * 25 // 25MB ) From 8a6c9434722fb62f8500ecfc8b2d413be9245289 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Tue, 28 May 2024 16:00:27 -0400 Subject: [PATCH 47/92] Add eligibility and limit checks --- .../VoiceToContentFeatureUtils.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt index 9c2e1b428682..3ba608ddd349 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.voicetocontent +import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.util.config.VoiceToContentFeatureConfig import javax.inject.Inject @@ -9,4 +10,28 @@ class VoiceToContentFeatureUtils @Inject constructor( private val voiceToContentFeatureConfig: VoiceToContentFeatureConfig ) { fun isVoiceToContentEnabled() = buildConfigWrapper.isJetpackApp && voiceToContentFeatureConfig.isEnabled() + + fun isEligibleForVoiceToContent(jetpackFeatureAIAssistantFeature: JetpackAIAssistantFeature) = + !jetpackFeatureAIAssistantFeature.siteRequireUpgrade + + fun getRequestLimit(jetpackFeatureAIAssistantFeature: JetpackAIAssistantFeature): Int { + with (jetpackFeatureAIAssistantFeature) { + if (currentTier?.slug == JETPACK_AI_FREE) { + return maxOf(0, requestsLimit - requestsCount) + } + // The backend uses `1` as an indicator of unlimited requests. + if (currentTier?.value == 1) { + return Int.MAX_VALUE + } + // The `usage-period.requests-count` is only valid for paid plans with + // a limited number of requests. + val requestsLimit = currentTier?.limit ?: requestsLimit + val requestsCount = usagePeriod?.requestsCount ?: requestsCount + return maxOf(0, requestsLimit - requestsCount) + } + } + + companion object { + private const val JETPACK_AI_FREE = "jetpack_ai_free" + } } From 83d9c79b2e8c9a59bcdaa4db9de0b18c488e8934 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Tue, 28 May 2024 16:00:44 -0400 Subject: [PATCH 48/92] Add tests for eligibility and limit checks --- .../VoiceToContentFeatureUtilsTest.kt | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt index 79cf10520b58..de094475ec69 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt @@ -2,12 +2,17 @@ package org.wordpress.android.ui.voicetocontent import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature +import org.wordpress.android.fluxc.model.jetpackai.Tier +import org.wordpress.android.fluxc.model.jetpackai.UsagePeriod import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.util.config.VoiceToContentFeatureConfig @@ -64,4 +69,111 @@ class VoiceToContentFeatureUtilsTest { // Assert assertEquals(false, result) } + + @Test + fun `when site requires an upgrade, then is not eligible for voiceToContent`() { + val feature = JetpackAIAssistantFeature( + hasFeature = false, + isOverLimit = false, + requestsCount = 0, + requestsLimit = 0, + usagePeriod = null, + siteRequireUpgrade = true, + upgradeType = "", + currentTier = null, + nextTier = null, + tierPlans = emptyList(), + tierPlansEnabled = false, + costs = null + ) + assertFalse(utils.isEligibleForVoiceToContent(feature)) + } + + @Test + fun `when site does not require an upgrade, then is eligible for voiceToContent`() { + val feature = JetpackAIAssistantFeature( + hasFeature = false, + isOverLimit = false, + requestsCount = 0, + requestsLimit = 0, + usagePeriod = null, + siteRequireUpgrade = false, + upgradeType = "", + currentTier = null, + nextTier = null, + tierPlans = emptyList(), + tierPlansEnabled = false, + costs = null + ) + assertTrue(utils.isEligibleForVoiceToContent(feature)) + } + + @Test + fun `when is free plan, then request limit is calculate for free plan`() { + val freePlanFeature = JetpackAIAssistantFeature( + hasFeature = false, + isOverLimit = false, + requestsCount = 50, + requestsLimit = 100, + usagePeriod = null, + siteRequireUpgrade = false, + upgradeType = "", + currentTier = Tier(JETPACK_AI_FREE, 0, 0, null), + nextTier = null, + tierPlans = emptyList(), + tierPlansEnabled = false, + costs = null + ) + assertEquals(50, utils.getRequestLimit(freePlanFeature)) + + val freePlanFeatureExceed = freePlanFeature.copy(requestsCount = 150) + assertEquals(0, utils.getRequestLimit(freePlanFeatureExceed)) + } + + @Test + fun `when unlimited plan, then request limit is calculated for unlimited plan`() { + val unlimitedPlanFeature = JetpackAIAssistantFeature( + hasFeature = false, + isOverLimit = false, + requestsCount = 0, + requestsLimit = 0, + usagePeriod = null, + siteRequireUpgrade = false, + upgradeType = "", + currentTier = Tier("", 0, 1, null), + nextTier = null, + tierPlans = emptyList(), + tierPlansEnabled = false, + costs = null + ) + assertEquals(Int.MAX_VALUE, utils.getRequestLimit(unlimitedPlanFeature)) + } + + @Test + fun `when limited plan, then request limit is calculated for limited plan`() { + val limitedPlanFeature = JetpackAIAssistantFeature( + hasFeature = false, + isOverLimit = false, + requestsCount = 0, + requestsLimit = 0, + usagePeriod = UsagePeriod("2024-01-01", "2024-02-01", 100), + siteRequireUpgrade = false, + upgradeType = "", + currentTier = Tier("", 200, 0, null), + nextTier = null, + tierPlans = emptyList(), + tierPlansEnabled = false, + costs = null + ) + assertEquals(100, utils.getRequestLimit(limitedPlanFeature)) + + val limitedPlanFeatureExceed = limitedPlanFeature.copy( + usagePeriod = UsagePeriod("2024-01-01", "2024-02-01", 250) + ) + assertEquals(0, utils.getRequestLimit(limitedPlanFeatureExceed)) + } + + companion object { + private const val JETPACK_AI_FREE = "jetpack_ai_free" + } } From aecf60696fa36e559fb3d6a64928ed0c579a54d7 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Tue, 28 May 2024 16:20:27 -0400 Subject: [PATCH 49/92] Refactor to address detekt ReturnCount --- .../VoiceToContentFeatureUtils.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt index 3ba608ddd349..50872b8bc1ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt @@ -15,19 +15,17 @@ class VoiceToContentFeatureUtils @Inject constructor( !jetpackFeatureAIAssistantFeature.siteRequireUpgrade fun getRequestLimit(jetpackFeatureAIAssistantFeature: JetpackAIAssistantFeature): Int { - with (jetpackFeatureAIAssistantFeature) { - if (currentTier?.slug == JETPACK_AI_FREE) { - return maxOf(0, requestsLimit - requestsCount) + return with(jetpackFeatureAIAssistantFeature) { + val calculatedLimit = if (currentTier?.slug == JETPACK_AI_FREE) { + maxOf(0, requestsLimit - requestsCount) + } else if (currentTier?.value == 1) { + Int.MAX_VALUE + } else { + val requestsLimit = currentTier?.limit ?: requestsLimit + val requestsCount = usagePeriod?.requestsCount ?: requestsCount + maxOf(0, requestsLimit - requestsCount) } - // The backend uses `1` as an indicator of unlimited requests. - if (currentTier?.value == 1) { - return Int.MAX_VALUE - } - // The `usage-period.requests-count` is only valid for paid plans with - // a limited number of requests. - val requestsLimit = currentTier?.limit ?: requestsLimit - val requestsCount = usagePeriod?.requestsCount ?: requestsCount - return maxOf(0, requestsLimit - requestsCount) + calculatedLimit } } From f100c30de6904fd9a803dd59e1d5f90eebbec23e Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Tue, 28 May 2024 16:36:09 -0700 Subject: [PATCH 50/92] Update naming --- .../ui/notifications/NotificationsDetailListFragment.kt | 5 ++--- .../wordpress/android/ui/reader/ReaderPostDetailFragment.kt | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt index 4f11a909ac06..7eae078ac84f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt @@ -256,10 +256,9 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { } requireNotNull(notification).let { note -> - val maybeActivity: FragmentActivity? = activity - maybeActivity?.let { fragmentActivity -> + context?.let { nonNullContext -> ReaderActivityLauncher.showReaderComments( - fragmentActivity, note.siteId.toLong(), note.postId.toLong(), + nonNullContext, note.siteId.toLong(), note.postId.toLong(), note.commentId, COMMENT_NOTIFICATION.sourceDescription ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index 9912197e01ef..e2aa05e8057c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -1581,10 +1581,9 @@ class ReaderPostDetailFragment : ViewPagerFragment(), private fun handleDirectOperation() = when (directOperation) { DirectOperation.COMMENT_JUMP, DirectOperation.COMMENT_REPLY, DirectOperation.COMMENT_LIKE -> { viewModel.post?.let { - val maybeActivity: FragmentActivity? = activity - maybeActivity?.let { fragmentActivity -> + context?.let { nonNullContext -> ReaderActivityLauncher.showReaderComments( - fragmentActivity, it.blogId, it.postId, + nonNullContext, it.blogId, it.postId, directOperation, commentId.toLong(), viewModel.interceptedUri, DIRECT_OPERATION.sourceDescription ) From c79a3fc9c9aaddb0740dc17d5e917b33357e12da Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Tue, 28 May 2024 16:47:23 -0700 Subject: [PATCH 51/92] Add annotation back --- .../org/wordpress/android/ui/reader/ReaderActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index d56b986efe55..6fa08eabcca1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -247,7 +247,7 @@ public static void showReaderComments(@NonNull Context context, long blogId, lon } public static void showReaderCommentsForResult( - Fragment fragment, + @NonNull Fragment fragment, long blogId, long postId, String source From 915584c756dc3f2fd311be49cc5ddbc77ddf76e0 Mon Sep 17 00:00:00 2001 From: Aditi Bhatia Date: Tue, 28 May 2024 17:28:30 -0700 Subject: [PATCH 52/92] Detekt --- .../android/ui/notifications/NotificationsDetailListFragment.kt | 1 - .../org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt index 7eae078ac84f..b96defd836b5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt @@ -13,7 +13,6 @@ import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.ListView -import androidx.fragment.app.FragmentActivity import androidx.fragment.app.ListFragment import androidx.lifecycle.lifecycleScope import com.airbnb.lottie.LottieAnimationView diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index e2aa05e8057c..56fa4dcf5df6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -40,7 +40,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.fragment.app.viewModels From b801a3342d518f9e1100536e1fbfef9d84b310e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 07:47:12 +0000 Subject: [PATCH 53/92] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000000..5db72dd6a94f --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} From 89a6d1339bef1300975b316acf3bc341f7ab0e51 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Wed, 29 May 2024 09:53:19 +0200 Subject: [PATCH 54/92] Add Renovate configuration for a8c dependencies only - separateMajorMinor to not create seperate PRs for a single dependency - versioning=semver-coerced to not create PRs for intermediate created for each PR/commit. We only care about released versions --- renovate.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/renovate.json b/renovate.json index 5db72dd6a94f..5cbd45e07c62 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,31 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" + ], + "packageRules": [ + { + "enabled": false, + "packagePatterns": [ + "*" + ] + }, + { + "enabled": true, + "matchDepPatterns": [ + "automattic" + ], + "separateMajorMinor": false + }, + { + "enabled": true, + "enabledManagers": [ + "gradle" + ], + "matchDepPatterns": [ + "automattic" + ], + "separateMajorMinor": false, + "versioning": "semver-coerced" + } ] } From ddab08a922a39ce129efd7415aa58aabde6b4678 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Wed, 29 May 2024 10:10:01 +0200 Subject: [PATCH 55/92] Add wpreleasetoolkit to renovate whitelist configuration --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 5cbd45e07c62..977c9e6392ff 100644 --- a/renovate.json +++ b/renovate.json @@ -13,7 +13,7 @@ { "enabled": true, "matchDepPatterns": [ - "automattic" + "automattic|wpmreleasetoolkit" ], "separateMajorMinor": false }, From e4689656c17e6da20cd20b908155f4df1250e524 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Wed, 29 May 2024 12:03:32 +0300 Subject: [PATCH 56/92] Fix a typo in a comment in the `AppReviewManager` --- .../main/java/org/wordpress/android/widgets/AppReviewManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt index 2a2e3de9876c..4f9fb0794e66 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/AppReviewManager.kt @@ -151,7 +151,7 @@ object AppReviewManager { } /** - * Check whether the in-app reviews prompt should be shown or not.it after its last sh + * Check whether the in-app reviews prompt should be shown or not. * @return true if the prompt should be shown */ fun shouldShowInAppReviewsPrompt(): Boolean { From 489074adfc5b47ac5d6b244a272f90ea4f7b38b7 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Wed, 29 May 2024 13:22:32 +0300 Subject: [PATCH 57/92] Fixes: AudioRecorder clean up --- .../android/ui/voicetocontent/VoiceToContentDialogFragment.kt | 1 - .../java/org/wordpress/android/util/audio/IAudioRecorder.kt | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 71bb1aadce5b..573f3e10c1bf 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -59,7 +59,6 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { if (areAllPermissionsGranted) { viewModel.startRecording() } else { - // Handle permissions denied case // todo pantelis handle permissions denied case Toast.makeText(context, "Permissions needed for recording", Toast.LENGTH_SHORT).show() } diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt index 7f00b7321cca..696416bd8372 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt @@ -13,9 +13,7 @@ interface IAudioRecorder { companion object { val REQUIRED_RECORDING_PERMISSIONS = arrayOf( - Manifest.permission.RECORD_AUDIO, - // todo pantelis: do we need this? - // Manifest.permission.WRITE_EXTERNAL_STORAGE + Manifest.permission.RECORD_AUDIO ) } } From b78b43d670e1f2e621c627fefc60d2fd00b19925 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Wed, 29 May 2024 13:23:00 +0300 Subject: [PATCH 58/92] Modifies: AudioRecorder recording output --- .../org/wordpress/android/util/audio/AudioRecorder.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index 0f6dc77caa50..d80ae2801819 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -29,13 +29,12 @@ class AudioRecorder( private var onRecordingFinished: (String) -> Unit = {} - // todo: check place of the recording file private val storeInMemory = true private val filePath by lazy { if (storeInMemory) { - applicationContext.cacheDir.absolutePath + "/recording.3gp" + applicationContext.cacheDir.absolutePath + "/recording.mp4" } else { - applicationContext.getExternalFilesDir(null)?.absolutePath + "/recording.3gp" + applicationContext.getExternalFilesDir(null)?.absolutePath + "/recording.mp4" } } @@ -61,8 +60,8 @@ class AudioRecorder( == PackageManager.PERMISSION_GRANTED) { recorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) - setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setOutputFile(filePath) try { From b54aeeb7bb7767296156059497ecd0deff1b76fa Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Wed, 29 May 2024 13:29:56 +0300 Subject: [PATCH 59/92] VoiceToContentUseCase cleanup --- .../voicetocontent/VoiceToContentUseCase.kt | 48 +------------------ 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 7dfe50fc2e60..58f8e5854080 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -1,22 +1,16 @@ package org.wordpress.android.ui.voicetocontent -import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIQueryResponse import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionResponse import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore -import org.wordpress.android.viewmodel.ContextProvider import java.io.File -import java.io.FileOutputStream -import java.io.InputStream import javax.inject.Inject -@Suppress("UnusedPrivateProperty") class VoiceToContentUseCase @Inject constructor( - private val jetpackAIStore: JetpackAIStore, - private val fileHelperWrapper: VoiceToContentTempFileHelperWrapper + private val jetpackAIStore: JetpackAIStore ) { companion object { const val FEATURE = "voice_to_content" @@ -80,43 +74,3 @@ data class VoiceToContentResult( val content: String? = null, val isError: Boolean = false ) - -// todo: Remove this class when real impl is in place - it's here so I can start unit tests -class VoiceToContentTempFileHelperWrapper @Inject constructor( - private val contextProvider: ContextProvider -) { - fun getAudioFile(): File? { - val result = runCatching { - getFileFromAssets(contextProvider.getContext()) - } - - return result.getOrElse { - null - } - } - - // todo: Do not forget to delete the test file from the asset directory - private fun getFileFromAssets(context: Context): File { - val fileName = "jetpack-ai-transcription-test-audio-file.m4a" - val file = File(context.filesDir, fileName) - context.assets.open(fileName).use { inputStream -> - copyInputStreamToFile(inputStream, file) - } - return file - } - - private fun copyInputStreamToFile(inputStream: InputStream, outputFile: File) { - FileOutputStream(outputFile).use { outputStream -> - val buffer = ByteArray(KILO_BYTE) - var length: Int - while (inputStream.read(buffer).also { length = it } > 0) { - outputStream.write(buffer, 0, length) - } - outputStream.flush() - } - inputStream.close() - } - companion object { - const val KILO_BYTE = 1024 - } -} From bdc0acdd099f883474e6b7337e3b792c0333bf22 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Wed, 29 May 2024 14:16:03 +0300 Subject: [PATCH 60/92] Handles: permission denial for Audio recording --- .../VoiceToContentDialogFragment.kt | 26 ++++++++++++++++--- WordPress/src/main/res/values/strings.xml | 5 ++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt index 573f3e10c1bf..25b39aefa519 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentDialogFragment.kt @@ -1,12 +1,14 @@ package org.wordpress.android.ui.voicetocontent +import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.compose.foundation.clickable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -33,6 +35,7 @@ import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import org.wordpress.android.R import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS +import android.provider.Settings @AndroidEntryPoint class VoiceToContentDialogFragment : BottomSheetDialogFragment() { @@ -59,8 +62,10 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { if (areAllPermissionsGranted) { viewModel.startRecording() } else { - // todo pantelis handle permissions denied case - Toast.makeText(context, "Permissions needed for recording", Toast.LENGTH_SHORT).show() + // Check if any permissions were denied permanently + if (permissions.entries.any { !it.value }) { + showPermissionDeniedDialog() + } } } @@ -77,6 +82,21 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() { requestMultiplePermissionsLauncher.launch(REQUIRED_RECORDING_PERMISSIONS) } + private fun showPermissionDeniedDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.voice_to_content_permissions_required_title) + .setMessage(R.string.voice_to_content_permissions_required_msg) + .setPositiveButton("Settings") { _, _ -> + // Open the app's settings + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", requireContext().packageName, null) + } + startActivity(intent) + } + .setNegativeButton("Cancel", null) + .show() + } + companion object { const val TAG = "voice_to_content_fragment_tag" diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index fc1aad108df8..3ca5cf0a2a55 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -4898,4 +4898,9 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> You can copy your post text in case your content is impacted. Copy error details to debug and share with support. Clear selected color Link label + + + Audio Recording Permission Required + To record audio, this app needs permission to access your microphone. You have previously denied this permission. Please enable the microphone permission in the app settings to use this feature. + From 957e3a9b7d899f99e874dcadb420a46129879fcb Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Wed, 29 May 2024 18:56:33 +0300 Subject: [PATCH 61/92] Moves: RECORD_AUDIO permission to debug manifest --- WordPress/src/debug/AndroidManifest.xml | 3 +++ WordPress/src/main/AndroidManifest.xml | 1 - .../android/ui/voicetocontent/VoiceToContentFeatureUtils.kt | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/WordPress/src/debug/AndroidManifest.xml b/WordPress/src/debug/AndroidManifest.xml index f8c9e84b0392..08c726b25fcd 100644 --- a/WordPress/src/debug/AndroidManifest.xml +++ b/WordPress/src/debug/AndroidManifest.xml @@ -26,6 +26,9 @@ android:name="android.permission.DUMP" tools:ignore="ProtectedPermissions" /> + + + - Date: Wed, 29 May 2024 14:47:05 -0400 Subject: [PATCH 62/92] Properly resolve conflict. --- .../android/ui/voicetocontent/VoiceToContentFeatureUtils.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt index aa0cc83e4855..4455ff7752c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtils.kt @@ -14,8 +14,6 @@ class VoiceToContentFeatureUtils @Inject constructor( && voiceToContentFeatureConfig.isEnabled() && buildConfigWrapper.isDebug() - fun isVoiceToContentEnabled() = buildConfigWrapper.isJetpackApp && voiceToContentFeatureConfig.isEnabled() - fun isEligibleForVoiceToContent(jetpackFeatureAIAssistantFeature: JetpackAIAssistantFeature) = !jetpackFeatureAIAssistantFeature.siteRequireUpgrade From 29cc555245b1098116b99cf4390df27dee135d5b Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Wed, 29 May 2024 15:30:47 -0400 Subject: [PATCH 63/92] Update unit tests to check for debug build --- .../android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt index de094475ec69..b9b4fb433897 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/voicetocontent/VoiceToContentFeatureUtilsTest.kt @@ -37,6 +37,7 @@ class VoiceToContentFeatureUtilsTest { // Arrange whenever(buildConfigWrapper.isJetpackApp).thenReturn(true) whenever(voiceToContentFeatureConfig.isEnabled()).thenReturn(true) + whenever(buildConfigWrapper.isDebug()).thenReturn(true) // Act val result = utils.isVoiceToContentEnabled() From a1f3c73365408635e8bbd2d2d1681f4c0f4caa8f Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 29 May 2024 17:48:12 -0300 Subject: [PATCH 64/92] Increase Cursor Window Size to 20mb to load larger posts --- .../org/wordpress/android/WellSqlInitializer.kt | 0 .../java/org/wordpress/android/WPWellSqlConfig.kt | 14 ++++++++++++++ .../org/wordpress/android/WellSqlInitializer.kt | 15 --------------- 3 files changed, 14 insertions(+), 15 deletions(-) rename WordPress/src/{debug => main}/java/org/wordpress/android/WellSqlInitializer.kt (100%) create mode 100644 WordPress/src/release/java/org/wordpress/android/WPWellSqlConfig.kt delete mode 100644 WordPress/src/release/java/org/wordpress/android/WellSqlInitializer.kt diff --git a/WordPress/src/debug/java/org/wordpress/android/WellSqlInitializer.kt b/WordPress/src/main/java/org/wordpress/android/WellSqlInitializer.kt similarity index 100% rename from WordPress/src/debug/java/org/wordpress/android/WellSqlInitializer.kt rename to WordPress/src/main/java/org/wordpress/android/WellSqlInitializer.kt diff --git a/WordPress/src/release/java/org/wordpress/android/WPWellSqlConfig.kt b/WordPress/src/release/java/org/wordpress/android/WPWellSqlConfig.kt new file mode 100644 index 000000000000..0c9b384f50b2 --- /dev/null +++ b/WordPress/src/release/java/org/wordpress/android/WPWellSqlConfig.kt @@ -0,0 +1,14 @@ +package org.wordpress.android + +import android.content.Context +import org.wordpress.android.fluxc.persistence.WellSqlConfig + +class WPWellSqlConfig(context: Context) : WellSqlConfig(context) { + /** + * Increase the cursor window size to 20MB for devices running API 28 and above. This should reduce the + * number of SQLiteBlobTooBigExceptions. Note that this is only called on API 28 and + * above since earlier versions don't allow adjusting the cursor window size. + */ + @Suppress("MagicNumber") + override fun getCursorWindowSize() = (1024L * 1024L * 20L) +} diff --git a/WordPress/src/release/java/org/wordpress/android/WellSqlInitializer.kt b/WordPress/src/release/java/org/wordpress/android/WellSqlInitializer.kt deleted file mode 100644 index d33fcb9d1de3..000000000000 --- a/WordPress/src/release/java/org/wordpress/android/WellSqlInitializer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.wordpress.android - -import android.content.Context -import com.yarolegovich.wellsql.WellSql -import org.wordpress.android.fluxc.persistence.WellSqlConfig -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class WellSqlInitializer @Inject constructor(private val context: Context) { - fun init() { - val wellSqlConfig = WellSqlConfig(context) - WellSql.init(wellSqlConfig) - } -} From c8c3aada869a4dcffeb154fbd791b1a479173642 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 11:35:28 +0200 Subject: [PATCH 65/92] Configure renovate to prepare PRs for all wordpress Gradle dependencies --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 977c9e6392ff..ff50c7cd497c 100644 --- a/renovate.json +++ b/renovate.json @@ -23,7 +23,7 @@ "gradle" ], "matchDepPatterns": [ - "automattic" + "automattic|wordpress" ], "separateMajorMinor": false, "versioning": "semver-coerced" From c5a0f96f9c1ebf4cba0c83fa76a7880d3787e4c4 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 11:49:55 +0200 Subject: [PATCH 66/92] Whitelist `gravatar` dependencies as well --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index ff50c7cd497c..7f073e3a3ff2 100644 --- a/renovate.json +++ b/renovate.json @@ -23,7 +23,7 @@ "gradle" ], "matchDepPatterns": [ - "automattic|wordpress" + "automattic|wordpress|gravatar" ], "separateMajorMinor": false, "versioning": "semver-coerced" From bb64d966861dce8c2a66c7e535126c4da9c85164 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 12:00:20 +0200 Subject: [PATCH 67/92] Ignore case in regex for matching dependencies So, e.g. `Automattic/dangermattic` will match now --- renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 7f073e3a3ff2..7598c418af4b 100644 --- a/renovate.json +++ b/renovate.json @@ -13,7 +13,7 @@ { "enabled": true, "matchDepPatterns": [ - "automattic|wpmreleasetoolkit" + "/automattic|wpmreleasetoolkit/i" ], "separateMajorMinor": false }, @@ -23,7 +23,7 @@ "gradle" ], "matchDepPatterns": [ - "automattic|wordpress|gravatar" + "/automattic|wordpress|gravatar/i" ], "separateMajorMinor": false, "versioning": "semver-coerced" From b0a3cce38743a92a07d1ed288dcdd28f36c3db13 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 12:08:04 +0200 Subject: [PATCH 68/92] Fix invalid regex --- renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 7598c418af4b..4e9780c9563f 100644 --- a/renovate.json +++ b/renovate.json @@ -13,7 +13,7 @@ { "enabled": true, "matchDepPatterns": [ - "/automattic|wpmreleasetoolkit/i" + "automattic|wpmreleasetoolkit/i" ], "separateMajorMinor": false }, @@ -23,7 +23,7 @@ "gradle" ], "matchDepPatterns": [ - "/automattic|wordpress|gravatar/i" + "automattic|wordpress|gravatar/i" ], "separateMajorMinor": false, "versioning": "semver-coerced" From 28cc7fa3489fb71f9a3fc99325522bf557264213 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 12:42:36 +0200 Subject: [PATCH 69/92] Add `dangermattic` to the whitelist. --- renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 4e9780c9563f..1a19f259875d 100644 --- a/renovate.json +++ b/renovate.json @@ -13,7 +13,7 @@ { "enabled": true, "matchDepPatterns": [ - "automattic|wpmreleasetoolkit/i" + "automattic|wpmreleasetoolkit|dangermattic" ], "separateMajorMinor": false }, @@ -23,7 +23,7 @@ "gradle" ], "matchDepPatterns": [ - "automattic|wordpress|gravatar/i" + "automattic|wordpress|gravatar" ], "separateMajorMinor": false, "versioning": "semver-coerced" From 6d992db32b8720a358aabd2eec2f7e46a012a740 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 13:43:59 +0200 Subject: [PATCH 70/92] Specify version of dependencies in addition to version.strictly configuration This setting should not change how dependencies are resolved (strictly constraint is still applied). This change is for Renovate to correctly resolve versions of dependencies --- WordPress/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/build.gradle b/WordPress/build.gradle index ffb8fa889646..dac9c075fd0f 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -364,7 +364,7 @@ dependencies { implementation (project(path:':libs:editor')) { exclude group: 'org.wordpress', module: 'utils' } - implementation("$gradle.ext.fluxCBinaryPath") { + implementation("$gradle.ext.fluxCBinaryPath:$wordPressFluxCVersion") { version { strictly wordPressFluxCVersion } @@ -372,7 +372,7 @@ dependencies { exclude group: 'org.wordpress', module: 'utils' exclude group: 'com.android.support', module: 'support-annotations' } - implementation ("$gradle.ext.wputilsBinaryPath") { + implementation ("$gradle.ext.wputilsBinaryPath$wordPressUtilsVersion") { version { strictly wordPressUtilsVersion } @@ -383,7 +383,7 @@ dependencies { } implementation "$gradle.ext.aboutAutomatticBinaryPath:$automatticAboutVersion" - implementation("$gradle.ext.tracksBinaryPath") { + implementation("$gradle.ext.tracksBinaryPath:$automatticTracksVersion") { version { strictly automatticTracksVersion } From f7ef046b22d11fc5ef78b61ab8071a9b15cac16a Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 30 May 2024 13:47:59 +0200 Subject: [PATCH 71/92] add missing coma --- WordPress/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/build.gradle b/WordPress/build.gradle index dac9c075fd0f..0ad75722b44b 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -372,7 +372,7 @@ dependencies { exclude group: 'org.wordpress', module: 'utils' exclude group: 'com.android.support', module: 'support-annotations' } - implementation ("$gradle.ext.wputilsBinaryPath$wordPressUtilsVersion") { + implementation ("$gradle.ext.wputilsBinaryPath:$wordPressUtilsVersion") { version { strictly wordPressUtilsVersion } From 5fda76bb15641ba675667db03f67508e07bc5b60 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Thu, 30 May 2024 13:58:59 +0300 Subject: [PATCH 72/92] AudioRecorder improvements --- .../android/util/audio/AudioRecorder.kt | 100 ++++++++++++------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index d80ae2801819..321328d1c3a7 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.media.MediaRecorder +import android.os.Build import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -53,31 +54,40 @@ class AudioRecorder( private val _isPaused = MutableStateFlow(false) val isPaused: StateFlow = _isPaused - @Suppress("DEPRECATION") override fun startRecording(onRecordingFinished: (String) -> Unit) { this.onRecordingFinished = onRecordingFinished if (applicationContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { - recorder = MediaRecorder().apply { - setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) - setAudioEncoder(MediaRecorder.AudioEncoder.AAC) - setOutputFile(filePath) + try { + recorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaRecorder(applicationContext) + } else { + MediaRecorder() + }.apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + setOutputFile(filePath) - try { prepare() start() startRecordingUpdates() _isRecording.value = true _isPaused.value = false - } catch (e: IOException) { - // Use a logging framework like Timber - Log.e("AudioRecorder", "Error starting recording") } + } catch (e: IOException) { + Log.e(TAG, "Error starting recording: ${e.message}") + onRecordingFinished("") + } catch (e: IllegalStateException) { + Log.e(TAG, "Illegal state when starting recording: ${e.message}") + onRecordingFinished("") + } catch (e: SecurityException) { + Log.e(TAG, "Security exception: ${e.message}") + onRecordingFinished("") } } else { - // Handle permission not granted case, e.g., throw an exception or show a message - Log.e("AudioRecorder","Permission to record audio not granted") + Log.e(TAG, "Permission to record audio not granted") + onRecordingFinished("") } } @@ -88,7 +98,7 @@ class AudioRecorder( release() } } catch (e: IllegalStateException) { - Log.e("AudioRecorder", "Error stopping recording") + Log.e(TAG, "Error stopping recording: ${e.message}") } finally { recorder = null stopRecordingUpdates() @@ -100,16 +110,14 @@ class AudioRecorder( } override fun pauseRecording() { - if (recorder != null) { - try { - recorder?.pause() - _isPaused.value = true - stopRecordingUpdates() - } catch (e: IllegalStateException) { - Log.e("AudioRecorder", "Error pausing recording") - } - } else { - Log.e("AudioRecorder","Pause not supported on this device") + try { + recorder?.pause() + _isPaused.value = true + stopRecordingUpdates() + } catch (e: IllegalStateException) { + Log.e(TAG, "Error pausing recording: ${e.message}") + } catch (e: UnsupportedOperationException) { + Log.e(TAG, "Pause not supported on this device: ${e.message}") } } @@ -123,7 +131,7 @@ class AudioRecorder( isPausedRecording = false startRecordingUpdates() } catch (e: IllegalStateException) { - Log.e("AudioRecorder", "Error resuming recording") + Log.e(TAG, "Error resuming recording") } } } @@ -135,33 +143,63 @@ class AudioRecorder( recordingParams = params } + @Suppress("MagicNumber") private fun startRecordingUpdates() { recordingJob = coroutineScope.launch { - var elapsedTime = 0 + var elapsedTimeInSeconds = 0 while (recorder != null) { delay(RECORDING_UPDATE_INTERVAL) - elapsedTime++ + elapsedTimeInSeconds += (RECORDING_UPDATE_INTERVAL / 1000).toInt() val fileSize = File(filePath).length() _recordingUpdates.value = RecordingUpdate( - elapsedTime = elapsedTime, + elapsedTime = elapsedTimeInSeconds, fileSize = fileSize, fileSizeLimitExceeded = fileSize >= recordingParams.maxFileSize, ) - if (fileSize >= recordingParams.maxFileSize - || elapsedTime >= recordingParams.maxDuration) { + if ( maxFileSizeExceeded(fileSize) || maxDurationExceeded(elapsedTimeInSeconds) ) { stopRecording() + break } } } } + /** + * Checks if the recorded file size has exceeded the specified maximum file size. + * + * @param fileSize The current size of the recorded file in bytes. + * @return `true` if the file size has exceeded the maximum file size minus the threshold, `false` otherwise. + * If `recordingParams.maxFileSize` is set to `-1L`, this function always returns `false` indicating + * no limit. + */ + private fun maxFileSizeExceeded(fileSize: Long): Boolean = when { + recordingParams.maxFileSize == -1L -> false + else -> fileSize >= recordingParams.maxFileSize - FILE_SIZE_THRESHOLD + } + + /** + * Checks if the recording duration has exceeded the specified maximum duration. + * + * @param elapsedTimeInSeconds The elapsed recording time in seconds. + * @return `true` if the elapsed time has exceeded the maximum duration minus the threshold, `false` otherwise. + * If `recordingParams.maxDuration` is set to `-1`, this function always returns `false` indicating + * no limit. + */ + private fun maxDurationExceeded(elapsedTimeInSeconds: Int): Boolean = when { + recordingParams.maxDuration == -1 -> false + else -> elapsedTimeInSeconds >= recordingParams.maxDuration - DURATION_THRESHOLD + } + private fun stopRecordingUpdates() { recordingJob?.cancel() } companion object { - private const val RECORDING_UPDATE_INTERVAL = 1000L - private const val RESUME_DELAY = 500L + private const val TAG = "AudioRecorder" + private const val RECORDING_UPDATE_INTERVAL = 1000L // in milliseconds + private const val RESUME_DELAY = 500L // in milliseconds + private const val FILE_SIZE_THRESHOLD = 100000L + private const val DURATION_THRESHOLD = 1 } } From 11dca7c01ec96fb88fbdf6445c8d346c92327ede Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Thu, 30 May 2024 16:56:08 +0300 Subject: [PATCH 73/92] Fixes: Deprecation error --- .../main/java/org/wordpress/android/util/audio/AudioRecorder.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index 321328d1c3a7..d543ecf64d53 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -54,6 +54,7 @@ class AudioRecorder( private val _isPaused = MutableStateFlow(false) val isPaused: StateFlow = _isPaused + @Suppress("DEPRECATION") override fun startRecording(onRecordingFinished: (String) -> Unit) { this.onRecordingFinished = onRecordingFinished if (applicationContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) From a1f2a5c0f1326550b7c2cd9930bf407d916789d1 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 30 May 2024 17:16:23 +0200 Subject: [PATCH 74/92] Rename onVoiceToContent to onContentUpdate --- .../android/editor/gutenberg/GutenbergContainerFragment.java | 5 +++-- .../android/editor/gutenberg/GutenbergEditorFragment.java | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java index 42037d098c9b..c3242ad2d50c 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java @@ -6,6 +6,7 @@ import android.view.ViewGroup; import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Consumer; import androidx.core.util.Pair; @@ -311,8 +312,8 @@ public void onRedoPressed() { mWPAndroidGlueCode.onRedoPressed(); } - public void onVoiceToContent(String content) { - mWPAndroidGlueCode.onVoiceToContent(content); + public void onContentUpdate(@NonNull String content) { + mWPAndroidGlueCode.onContentUpdate(content); } public void updateCapabilities(GutenbergPropsBuilder gutenbergPropsBuilder) { diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index d7c91960e316..41263b5caf60 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -408,8 +408,7 @@ public void onEditorDidMount(ArrayList unsupportedBlocks) { public void run() { setEditorProgressBarVisibility(!mEditorDidMount); - getGutenbergContainerFragment().onVoiceToContent( - "# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n - Duis sagittis ipsum\n## Conclusion\n- Praesent libero\n- Sed cursus ante dapibus diam\n- Suspendisse malesuada lacus ex"); + getGutenbergContainerFragment().onContentUpdate("# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n - Duis sagittis ipsum\n## Conclusion\n- Praesent libero\n- Sed cursus ante dapibus diam\n- Suspendisse malesuada lacus ex"); } }); } From c42e9dba81b5c12466198fcaf127abe228792ea8 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Thu, 30 May 2024 19:06:34 +0300 Subject: [PATCH 75/92] Adds: RecordingStrategy for AudioRecorder --- .../android/modules/ApplicationModule.java | 17 +++++++++++-- .../ui/voicetocontent/RecordingUseCase.kt | 3 ++- .../android/util/audio/AudioRecorder.kt | 23 ++++++----------- .../android/util/audio/IAudioRecorder.kt | 1 - .../android/util/audio/RecordingStrategy.kt | 25 +++++++++++++++++++ 5 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/audio/RecordingStrategy.kt diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index adf1928cd08c..ddd839347355 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -30,6 +30,9 @@ import org.wordpress.android.util.BuildConfigWrapper; import org.wordpress.android.util.audio.AudioRecorder; import org.wordpress.android.util.audio.IAudioRecorder; +import org.wordpress.android.util.audio.RecordingStrategy; +import org.wordpress.android.util.audio.RecordingStrategy.VoiceToContentRecordingStrategy; +import org.wordpress.android.util.audio.VoiceToContentStrategy; import org.wordpress.android.util.config.InAppUpdatesFeatureConfig; import org.wordpress.android.util.config.RemoteConfigWrapper; import org.wordpress.android.util.wizard.WizardManager; @@ -124,8 +127,18 @@ public static SensorManager provideSensorManager(@ApplicationContext Context con return (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); } + @VoiceToContentStrategy @Provides - public static IAudioRecorder provideAudioRecorder(@ApplicationContext Context context) { - return new AudioRecorder(context); + public static IAudioRecorder provideAudioRecorder( + @ApplicationContext Context context, + @VoiceToContentStrategy RecordingStrategy recordingStrategy + ) { + return new AudioRecorder(context, recordingStrategy); + } + + @VoiceToContentStrategy + @Provides + public static RecordingStrategy provideVoiceToContentRecordingStrategy() { + return new VoiceToContentRecordingStrategy(); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt index 52155db015c5..43d827b4c9b9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt @@ -3,10 +3,11 @@ package org.wordpress.android.ui.voicetocontent import kotlinx.coroutines.flow.Flow import org.wordpress.android.util.audio.IAudioRecorder import org.wordpress.android.util.audio.RecordingUpdate +import org.wordpress.android.util.audio.VoiceToContentStrategy import javax.inject.Inject class RecordingUseCase @Inject constructor( - private val audioRecorder: IAudioRecorder + @VoiceToContentStrategy private val audioRecorder: IAudioRecorder ) { fun startRecording(onRecordingFinished: (String) -> Unit) { audioRecorder.startRecording(onRecordingFinished) diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index d543ecf64d53..08967f062b24 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -20,14 +20,9 @@ import java.io.File import java.io.IOException class AudioRecorder( - private val applicationContext: Context + private val applicationContext: Context, + private val recordingStrategy: RecordingStrategy ) : IAudioRecorder { - // default recording params - private var recordingParams: RecordingParams = RecordingParams( - maxDuration = 60 * 5, // 5 minutes - maxFileSize = 1000000L * 25 // 25MB - ) - private var onRecordingFinished: (String) -> Unit = {} private val storeInMemory = true @@ -140,10 +135,6 @@ class AudioRecorder( override fun recordingUpdates(): Flow = recordingUpdates - override fun setRecordingParams(params: RecordingParams) { - recordingParams = params - } - @Suppress("MagicNumber") private fun startRecordingUpdates() { recordingJob = coroutineScope.launch { @@ -155,7 +146,7 @@ class AudioRecorder( _recordingUpdates.value = RecordingUpdate( elapsedTime = elapsedTimeInSeconds, fileSize = fileSize, - fileSizeLimitExceeded = fileSize >= recordingParams.maxFileSize, + fileSizeLimitExceeded = fileSize >= recordingStrategy.maxFileSize, ) if ( maxFileSizeExceeded(fileSize) || maxDurationExceeded(elapsedTimeInSeconds) ) { @@ -175,8 +166,8 @@ class AudioRecorder( * no limit. */ private fun maxFileSizeExceeded(fileSize: Long): Boolean = when { - recordingParams.maxFileSize == -1L -> false - else -> fileSize >= recordingParams.maxFileSize - FILE_SIZE_THRESHOLD + recordingStrategy.maxFileSize == -1L -> false + else -> fileSize >= recordingStrategy.maxFileSize - FILE_SIZE_THRESHOLD } /** @@ -188,8 +179,8 @@ class AudioRecorder( * no limit. */ private fun maxDurationExceeded(elapsedTimeInSeconds: Int): Boolean = when { - recordingParams.maxDuration == -1 -> false - else -> elapsedTimeInSeconds >= recordingParams.maxDuration - DURATION_THRESHOLD + recordingStrategy.maxDuration == -1 -> false + else -> elapsedTimeInSeconds >= recordingStrategy.maxDuration - DURATION_THRESHOLD } private fun stopRecordingUpdates() { diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt index 696416bd8372..a79ed90db2cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt @@ -9,7 +9,6 @@ interface IAudioRecorder { fun pauseRecording() fun resumeRecording() fun recordingUpdates(): Flow - fun setRecordingParams(params: RecordingParams) companion object { val REQUIRED_RECORDING_PERMISSIONS = arrayOf( diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingStrategy.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingStrategy.kt new file mode 100644 index 000000000000..24339b323230 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/RecordingStrategy.kt @@ -0,0 +1,25 @@ +package org.wordpress.android.util.audio + +import javax.inject.Qualifier + +@Suppress("MagicNumber") +sealed class RecordingStrategy { + abstract val maxFileSize: Long + abstract val maxDuration: Int + abstract val storeInMemory: Boolean + abstract val recordingFileName: String + + data class VoiceToContentRecordingStrategy( + override val maxFileSize: Long = 1000000L * 25, // 25MB + override val maxDuration: Int = 60 * 5, // 5 minutes + override val recordingFileName: String = "voice_recording.mp4", + override val storeInMemory: Boolean = true + ) : RecordingStrategy() +} + +// Declare here your custom annotation for each RecordingStrategy so it can be provided by Dagger +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class VoiceToContentStrategy + + From e3718c0c4dac8204da1e6ba2faf6c7635b06d1b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 07:36:29 +0000 Subject: [PATCH 76/92] Update Automattic/dangermattic action to v1.1.1 --- .github/workflows/run-danger.yml | 2 +- .github/workflows/validate-issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-danger.yml b/.github/workflows/run-danger.yml index 1031d64a7dd9..dcd58051ec3c 100644 --- a/.github/workflows/run-danger.yml +++ b/.github/workflows/run-danger.yml @@ -7,7 +7,7 @@ on: jobs: dangermattic: if: ${{ (github.event.pull_request.draft == false) }} - uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.0 + uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.1 with: org-slug: "automattic" pipeline-slug: "wordpress-android" diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml index c9899d62603d..29a277550f45 100644 --- a/.github/workflows/validate-issues.yml +++ b/.github/workflows/validate-issues.yml @@ -6,7 +6,7 @@ on: jobs: check-labels-on-issues: - uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.0.0 + uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.1 with: label-format-list: '[ "^\[.+\]", From 91a4f5bfa559598004e06205561f2ef31ae8c477 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Fri, 31 May 2024 11:43:11 +0300 Subject: [PATCH 77/92] Adds: AudioRecorderResult for IAudioRecorder --- .../ui/voicetocontent/RecordingUseCase.kt | 3 +- .../voicetocontent/VoiceToContentViewModel.kt | 21 ++++++++++---- .../android/util/audio/AudioRecorder.kt | 29 ++++++++++++------- .../android/util/audio/IAudioRecorder.kt | 9 +++++- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt index 43d827b4c9b9..3552ea314fe2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/RecordingUseCase.kt @@ -5,11 +5,12 @@ import org.wordpress.android.util.audio.IAudioRecorder import org.wordpress.android.util.audio.RecordingUpdate import org.wordpress.android.util.audio.VoiceToContentStrategy import javax.inject.Inject +import org.wordpress.android.util.audio.IAudioRecorder.AudioRecorderResult class RecordingUseCase @Inject constructor( @VoiceToContentStrategy private val audioRecorder: IAudioRecorder ) { - fun startRecording(onRecordingFinished: (String) -> Unit) { + fun startRecording(onRecordingFinished: (AudioRecorderResult) -> Unit) { audioRecorder.startRecording(onRecordingFinished) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt index 35aa99f89964..845d7e533f98 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentViewModel.kt @@ -18,6 +18,8 @@ import org.wordpress.android.viewmodel.ScopedViewModel import java.io.File import javax.inject.Inject import javax.inject.Named +import org.wordpress.android.util.audio.IAudioRecorder.AudioRecorderResult.Success +import org.wordpress.android.util.audio.IAudioRecorder.AudioRecorderResult.Error @HiltViewModel class VoiceToContentViewModel @Inject constructor( @@ -54,12 +56,19 @@ class VoiceToContentViewModel @Inject constructor( } fun startRecording() { - recordingUseCase.startRecording { recordingPath -> - val file = getRecordingFile(recordingPath) - file?.let { - executeVoiceToContent(it) - } ?: run { - _uiState.postValue(VoiceToContentResult(isError = true)) + recordingUseCase.startRecording { audioRecorderResult -> + when (audioRecorderResult) { + is Success -> { + val file = getRecordingFile(audioRecorderResult.recordingPath) + file?.let { + executeVoiceToContent(it) + } ?: run { + _uiState.postValue(VoiceToContentResult(isError = true)) + } + } + is Error -> { + _uiState.postValue(VoiceToContentResult(isError = true)) + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt index 08967f062b24..7b6f73ad6d90 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/AudioRecorder.kt @@ -18,12 +18,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.io.File import java.io.IOException +import org.wordpress.android.util.audio.IAudioRecorder.AudioRecorderResult +import org.wordpress.android.util.audio.IAudioRecorder.AudioRecorderResult.Success +import org.wordpress.android.util.audio.IAudioRecorder.AudioRecorderResult.Error class AudioRecorder( private val applicationContext: Context, private val recordingStrategy: RecordingStrategy ) : IAudioRecorder { - private var onRecordingFinished: (String) -> Unit = {} + private var onRecordingFinished: (AudioRecorderResult) -> Unit = {} private val storeInMemory = true private val filePath by lazy { @@ -50,7 +53,7 @@ class AudioRecorder( val isPaused: StateFlow = _isPaused @Suppress("DEPRECATION") - override fun startRecording(onRecordingFinished: (String) -> Unit) { + override fun startRecording(onRecordingFinished: (AudioRecorderResult) -> Unit) { this.onRecordingFinished = onRecordingFinished if (applicationContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { @@ -72,18 +75,22 @@ class AudioRecorder( _isPaused.value = false } } catch (e: IOException) { - Log.e(TAG, "Error starting recording: ${e.message}") - onRecordingFinished("") + val errorMessage = "Error preparing MediaRecorder: ${e.message}" + Log.e(TAG, errorMessage) + onRecordingFinished(Error(errorMessage)) } catch (e: IllegalStateException) { - Log.e(TAG, "Illegal state when starting recording: ${e.message}") - onRecordingFinished("") + val errorMessage = "Illegal state when starting recording: ${e.message}" + Log.e(TAG, errorMessage) + onRecordingFinished(Error(errorMessage)) } catch (e: SecurityException) { - Log.e(TAG, "Security exception: ${e.message}") - onRecordingFinished("") + val errorMessage = "Security exception when starting recording: ${e.message}" + Log.e(TAG, errorMessage) + onRecordingFinished(Error(errorMessage)) } } else { - Log.e(TAG, "Permission to record audio not granted") - onRecordingFinished("") + val errorMessage = "Permission to record audio not granted" + Log.e(TAG, errorMessage) + onRecordingFinished(Error(errorMessage)) } } @@ -102,7 +109,7 @@ class AudioRecorder( _isRecording.value = false } // return filePath - onRecordingFinished(filePath) + onRecordingFinished(Success(filePath)) } override fun pauseRecording() { diff --git a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt index a79ed90db2cd..73ab0ca30725 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/audio/IAudioRecorder.kt @@ -4,12 +4,17 @@ import android.Manifest import kotlinx.coroutines.flow.Flow interface IAudioRecorder { - fun startRecording(onRecordingFinished: (String) -> Unit) + fun startRecording(onRecordingFinished: (AudioRecorderResult) -> Unit) fun stopRecording() fun pauseRecording() fun resumeRecording() fun recordingUpdates(): Flow + sealed class AudioRecorderResult { + data class Success(val recordingPath: String) : AudioRecorderResult() + data class Error(val errorMessage: String) : AudioRecorderResult() + } + companion object { val REQUIRED_RECORDING_PERMISSIONS = arrayOf( Manifest.permission.RECORD_AUDIO @@ -17,3 +22,5 @@ interface IAudioRecorder { } } + + From 50a8ae4a7ffe2acd13f19d9522046d9aa303b247 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 31 May 2024 11:51:12 +0200 Subject: [PATCH 78/92] Remove test content --- .../android/editor/gutenberg/GutenbergEditorFragment.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index 41263b5caf60..c8dc71940370 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -407,8 +407,6 @@ public void onEditorDidMount(ArrayList unsupportedBlocks) { @Override public void run() { setEditorProgressBarVisibility(!mEditorDidMount); - - getGutenbergContainerFragment().onContentUpdate("# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n - Duis sagittis ipsum\n## Conclusion\n- Praesent libero\n- Sed cursus ante dapibus diam\n- Suspendisse malesuada lacus ex"); } }); } From 26d45c560b6c4136fbfff54f4745d780ce8eb3f3 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 31 May 2024 11:51:40 +0200 Subject: [PATCH 79/92] Update Gutenberg ref --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6c52fa2c85e4..6e2ee16bf21d 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ ext { automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' automatticTracksVersion = '5.0.0' - gutenbergMobileVersion = '6878-64fe89bbf66583e5a7f93fd2d28de3342f14ce85' + gutenbergMobileVersion = 'v1.120.0-alpha1' wordPressAztecVersion = 'v2.1.3' wordPressFluxCVersion = 'trunk-a9a471914d3ebd1093986d3d0a95c2a29b29dca0' wordPressLoginVersion = '1.15.0' From 7c41ee1c3bb4c44a1926ac2a844a5fb824ecea79 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 31 May 2024 11:54:23 +0200 Subject: [PATCH 80/92] Update Release notes --- RELEASE-NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 9f0ec9aa0d54..8d292613c065 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,7 +2,7 @@ 25.1 ----- - +* [*] [internal] Block editor: Add onContentUpdate bridge functionality [https://github.com/wordpress-mobile/gutenberg-mobile/pull/20852] 25.0 ----- From b080aacb4d19563dcdbc25ee922ce823a568b4d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 10:56:46 +0000 Subject: [PATCH 81/92] Update automatticTracksVersion to v5.1.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6e2ee16bf21d..baa560a3a822 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ ext { // libs automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' - automatticTracksVersion = '5.0.0' + automatticTracksVersion = '5.1.0' gutenbergMobileVersion = 'v1.120.0-alpha1' wordPressAztecVersion = 'v2.1.3' wordPressFluxCVersion = 'trunk-a9a471914d3ebd1093986d3d0a95c2a29b29dca0' From 8ac3b6d444fc3ffd0afe71d6bf609488a014b846 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 31 May 2024 09:16:04 -0400 Subject: [PATCH 82/92] Add log line for transcription error while building out the feature --- .../android/ui/voicetocontent/VoiceToContentUseCase.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 58f8e5854080..70f7b09b14e9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.voicetocontent +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.model.SiteModel @@ -35,6 +36,7 @@ class VoiceToContentUseCase @Inject constructor( transcriptionResponse.model } is JetpackAITranscriptionResponse.Error -> { + Log.i(javaClass.simpleName, "Error transcribing audio file: ${transcriptionResponse.type} ${transcriptionResponse.message}") null } } From 405ccc8b634959eaa04eddb8c004ac5672061d8d Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 31 May 2024 09:16:15 -0400 Subject: [PATCH 83/92] Update FluxC hash --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6e2ee16bf21d..506a989bcf25 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.120.0-alpha1' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = 'trunk-a9a471914d3ebd1093986d3d0a95c2a29b29dca0' + wordPressFluxCVersion = '3026-6c41807a193ee1eaa5967eb34221895a6282f4e3' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From d15f662bb6d87b3534ab07be6237c1a81bfe1101 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 31 May 2024 09:49:42 -0400 Subject: [PATCH 84/92] Address detekt too long line error --- .../android/ui/voicetocontent/VoiceToContentUseCase.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt index 70f7b09b14e9..88edbf2e5f60 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/voicetocontent/VoiceToContentUseCase.kt @@ -36,7 +36,11 @@ class VoiceToContentUseCase @Inject constructor( transcriptionResponse.model } is JetpackAITranscriptionResponse.Error -> { - Log.i(javaClass.simpleName, "Error transcribing audio file: ${transcriptionResponse.type} ${transcriptionResponse.message}") + val message = "${transcriptionResponse.type} ${transcriptionResponse.message}" + Log.i( + javaClass.simpleName, + "Error transcribing audio file: $message" + ) null } } From 2de0177b95f5d15279223634e3e267d7e1dcee8c Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 31 May 2024 10:18:31 -0400 Subject: [PATCH 85/92] Update fluxC hash --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 506a989bcf25..9c9244a539ce 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '5.0.0' gutenbergMobileVersion = 'v1.120.0-alpha1' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = '3026-6c41807a193ee1eaa5967eb34221895a6282f4e3' + wordPressFluxCVersion = 'trunk-8b7eeade00f33c5b4296722fb3854b3a32e06ad8' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' From 13f5363043dec3e97a9ddbc8a04e16cdd0b17b22 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Fri, 31 May 2024 16:50:35 +0200 Subject: [PATCH 86/92] Use v1 for dangermattic GitHub Actions It will match the newest v1.x.x release --- .github/workflows/run-danger.yml | 2 +- .github/workflows/validate-issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-danger.yml b/.github/workflows/run-danger.yml index dcd58051ec3c..ae4888413a2b 100644 --- a/.github/workflows/run-danger.yml +++ b/.github/workflows/run-danger.yml @@ -7,7 +7,7 @@ on: jobs: dangermattic: if: ${{ (github.event.pull_request.draft == false) }} - uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.1 + uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1 with: org-slug: "automattic" pipeline-slug: "wordpress-android" diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml index 29a277550f45..f170af9d9776 100644 --- a/.github/workflows/validate-issues.yml +++ b/.github/workflows/validate-issues.yml @@ -6,7 +6,7 @@ on: jobs: check-labels-on-issues: - uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.1 + uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1 with: label-format-list: '[ "^\[.+\]", From 5c8dc4887aa12635623afd53ddc16970836c672a Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 3 Jun 2024 09:49:45 +0200 Subject: [PATCH 87/92] Revert "Use v1 for dangermattic GitHub Actions" This reverts commit 13f5363043dec3e97a9ddbc8a04e16cdd0b17b22. --- .github/workflows/run-danger.yml | 2 +- .github/workflows/validate-issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-danger.yml b/.github/workflows/run-danger.yml index ae4888413a2b..dcd58051ec3c 100644 --- a/.github/workflows/run-danger.yml +++ b/.github/workflows/run-danger.yml @@ -7,7 +7,7 @@ on: jobs: dangermattic: if: ${{ (github.event.pull_request.draft == false) }} - uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1 + uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.1 with: org-slug: "automattic" pipeline-slug: "wordpress-android" diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml index f170af9d9776..29a277550f45 100644 --- a/.github/workflows/validate-issues.yml +++ b/.github/workflows/validate-issues.yml @@ -6,7 +6,7 @@ on: jobs: check-labels-on-issues: - uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1 + uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.1 with: label-format-list: '[ "^\[.+\]", From 3e362b534fc494bdb5dcd3a1f880484d80f480ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 08:18:14 +0000 Subject: [PATCH 88/92] Update Automattic/dangermattic action to v1.1.2 --- .github/workflows/run-danger.yml | 2 +- .github/workflows/validate-issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-danger.yml b/.github/workflows/run-danger.yml index dcd58051ec3c..ec845c47c73a 100644 --- a/.github/workflows/run-danger.yml +++ b/.github/workflows/run-danger.yml @@ -7,7 +7,7 @@ on: jobs: dangermattic: if: ${{ (github.event.pull_request.draft == false) }} - uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.1 + uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.2 with: org-slug: "automattic" pipeline-slug: "wordpress-android" diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml index 29a277550f45..01b05ac2ffe2 100644 --- a/.github/workflows/validate-issues.yml +++ b/.github/workflows/validate-issues.yml @@ -6,7 +6,7 @@ on: jobs: check-labels-on-issues: - uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.1 + uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.1.2 with: label-format-list: '[ "^\[.+\]", From 92397ef7b362a370608d2cfa9cf93d8ed4868871 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Jun 2024 11:43:52 +0300 Subject: [PATCH 89/92] Replaces OAuth2 section with self-hosted test instructions --- README.md | 38 +++++--------------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 6de18b6906d5..8fc673a822d3 100644 --- a/README.md +++ b/README.md @@ -16,41 +16,8 @@ If you're a developer wanting to contribute, read on. Notes: -* To use WordPress.com features (login to WordPress.com, access Reader and Stats, etc) you need a WordPress.com OAuth2 ID and secret. Please read the [OAuth2 Authentication](#oauth2-authentication) section. * While loading/building the app in Android Studio ignore the prompt to update the gradle plugin version as that will probably introduce build errors. On the other hand, feel free to update if you are planning to work on ensuring the compatibility of the newer version. - -## OAuth2 Authentication ## - -In order to use WordPress.com functions you will need a client ID and -a client secret key. These details will be used to authenticate your -application and verify that the API calls being made are valid. You can -create an application or view details for your existing applications with -our [WordPress.com applications manager][5]. - -When creating your application, you should select "Native client" for the application type. -The "**Website URL**", "**Redirect URLs**", and "**Javascript Origins**" fields are required but not used for -the mobile apps. Just use "**[https://localhost](https://localhost)**". - -Once you've created your application in the [applications manager][5], you'll -need to edit the `./gradle.properties` file and change the -`wp.oauth.app_id` and `wp.oauth.app_secret` fields. Then you can compile and -run the app on a device or an emulator and try to login with a WordPress.com -account. Note that authenticating to WordPress.com via Google is not supported -in development builds of the app, only in the official release. - -Note that credentials created with our [WordPress.com applications manager][5] -allow login only and not signup. New accounts must be created using the [official app][1] -or [on the web](https://wordpress.com/start). Login is restricted to the WordPress.com -account with which the credentials were created. In other words, if the credentials -were created with foo@email.com, you will only be able to login with foo@email.com. -Using another account like bar@email.com will cause the `Client cannot use "password" grant_type` error. - -For security reasons, some account-related actions aren't supported for development -builds when using a WordPress.com account with 2-factor authentication enabled. - -Read more about [OAuth2][6] and the [WordPress.com REST endpoint][7]. - ## Build and Test ## To build, install, and test the project from the command line: @@ -61,6 +28,11 @@ To build, install, and test the project from the command line: $ ./gradlew :WordPress:testWordPressVanillaDebugUnitTest # assemble, install and run unit tests $ ./gradlew :WordPress:connectedWordPressVanillaDebugAndroidTest # assemble, install and run Android tests +## Running the app ## + +You can use your own WordPress site for developing and testing the app. If you don't have one, you can create a temporary test site for free at https://jurassic.ninja/. +On the app start up screen choose "Enter your existing site address" and enter the URL of your site and your credentials. + ## Directory structure ## . ├── libs # dependencies used to build debug variants From 2557cba29d2161ccfd17a63ba18b17bbc0776304 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Jun 2024 13:51:20 +0300 Subject: [PATCH 90/92] Fixes typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fc673a822d3..2965f1d0f61e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ To build, install, and test the project from the command line: ## Running the app ## You can use your own WordPress site for developing and testing the app. If you don't have one, you can create a temporary test site for free at https://jurassic.ninja/. -On the app start up screen choose "Enter your existing site address" and enter the URL of your site and your credentials. +On the app start up screen, choose "Enter your existing site address" and enter the URL of your site and your credentials. ## Directory structure ## . From 01ab55f2407fcebcfca5de416d75de3a9c180cfd Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Jun 2024 13:54:23 +0300 Subject: [PATCH 91/92] Adds note on the wpcom features access limitation --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2965f1d0f61e..abdabaee6db2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ To build, install, and test the project from the command line: You can use your own WordPress site for developing and testing the app. If you don't have one, you can create a temporary test site for free at https://jurassic.ninja/. On the app start up screen, choose "Enter your existing site address" and enter the URL of your site and your credentials. +Note: Access to WordPress.com features is temporarily disabled in the development environment. + ## Directory structure ## . ├── libs # dependencies used to build debug variants From 08070c481807237c7a59f063c19d0a449c0c0355 Mon Sep 17 00:00:00 2001 From: Pantelis Stampoulis Date: Tue, 4 Jun 2024 17:44:48 +0300 Subject: [PATCH 92/92] Adds: delays for app restart in InAppUpdateManagerImpl --- .../inappupdate/InAppUpdateAnalyticsTracker.kt | 2 +- .../inappupdate/InAppUpdateManagerImpl.kt | 18 ++++++++++++++++-- .../android/modules/ApplicationModule.java | 6 ++++++ .../android/analytics/AnalyticsTracker.java | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt index abf15b094bfd..6bbc8cefe071 100644 --- a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt @@ -21,7 +21,7 @@ class InAppUpdateAnalyticsTracker @Inject constructor( } fun trackAppRestartToCompleteUpdate() { - tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART) + tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART_BY_USER) } private fun createPropertyMap(updateType: Int): Map { diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt index 11ac7fc132e1..23809e8796ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt @@ -22,6 +22,10 @@ import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_T import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_NOT_AVAILABLE import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_FLEXIBLE_REQUEST_CODE import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_IMMEDIATE_REQUEST_CODE @@ -33,6 +37,7 @@ import javax.inject.Singleton @Suppress("TooManyFunctions") class InAppUpdateManagerImpl( @ApplicationContext private val applicationContext: Context, + private val coroutineScope: CoroutineScope, private val appUpdateManager: AppUpdateManager, private val remoteConfigWrapper: RemoteConfigWrapper, private val buildConfigWrapper: BuildConfigWrapper, @@ -51,8 +56,16 @@ class InAppUpdateManagerImpl( } override fun completeAppUpdate() { - inAppUpdateAnalyticsTracker.trackAppRestartToCompleteUpdate() - appUpdateManager.completeUpdate() + coroutineScope.launch(Dispatchers.Main) { + // Track the app restart to complete update + inAppUpdateAnalyticsTracker.trackAppRestartToCompleteUpdate() + + // Delay so the event above can be logged + delay(RESTART_DELAY_IN_MILLIS) + + // Complete the update + appUpdateManager.completeUpdate() + } } override fun cancelAppUpdate(updateType: Int) { @@ -226,5 +239,6 @@ class InAppUpdateManagerImpl( private const val TAG = "AppUpdateChecker" private const val PREF_NAME = "in_app_update_prefs" private const val KEY_LAST_APP_UPDATE_CHECK_VERSION = "last_app_update_check_version" + private const val RESTART_DELAY_IN_MILLIS = 500L } } diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index ddd839347355..a2d0f248d15b 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -39,6 +39,8 @@ import org.wordpress.android.viewmodel.helpers.ConnectionStatus; import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData; +import javax.inject.Named; + import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -46,6 +48,8 @@ import dagger.hilt.InstallIn; import dagger.hilt.android.qualifiers.ApplicationContext; import dagger.hilt.components.SingletonComponent; +import kotlinx.coroutines.CoroutineScope; +import static org.wordpress.android.modules.ThreadModuleKt.APPLICATION_SCOPE; @InstallIn(SingletonComponent.class) @Module(includes = AndroidInjectionModule.class) @@ -98,6 +102,7 @@ public static AppUpdateManager provideAppUpdateManager(@ApplicationContext Conte @Provides public static IInAppUpdateManager provideInAppUpdateManager( @ApplicationContext Context context, + @Named(APPLICATION_SCOPE) CoroutineScope appScope, AppUpdateManager appUpdateManager, RemoteConfigWrapper remoteConfigWrapper, BuildConfigWrapper buildConfigWrapper, @@ -108,6 +113,7 @@ public static IInAppUpdateManager provideInAppUpdateManager( return inAppUpdatesFeatureConfig.isEnabled() ? new InAppUpdateManagerImpl( context, + appScope, appUpdateManager, remoteConfigWrapper, buildConfigWrapper, diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 699f1078283e..14cee38b4b75 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -1134,7 +1134,7 @@ public enum Stat { IN_APP_UPDATE_SHOWN, IN_APP_UPDATE_DISMISSED, IN_APP_UPDATE_ACCEPTED, - IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART; + IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART_BY_USER; /* * Please set the event name in the enum only if the new Stat's name in lower case does not match it.