Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
[Reader] Track how many people select text content in Post Details
In order to move forward with the content annotating proposal, we need to start tracking how many people select text content in the Reader and how many copy it. Ref: p1716377347554169-slack-C06D0RNJG11

iOS issue: https://github.com/wordpress-mobile/WordPress-iOS/issues/23250

Add the following events:
- [x] `reader_article_text_highlighted`
- [x] `reader_article_text_copied`
- [ ] `reader_comment_text_highlighted`
- [ ] `reader_comment_text_copied`


Discussion hints:
Thanks for reporting! 👍 <!-- generated_by_dangermattic -->


Pull request: [Reader] Add tracks for text highlight/copy in Post Details body
Fixes #20888 

Add tracks to text highlight and copy in Post Details body WebView. For that, I used the same javascript code used on iOS (https://github.com/wordpress-mobile/WordPress-iOS/pull/23251/files#diff-0c87a62d20f4b5ede4c7274c4e990d865a633b92dfe70d702672ee27191613d9) to be able to listen to the appropriate events and send a message to the Java/Kotlin code.

-----

## To Test:

1. Open Jetpack
2. Make sure data collection is enabled in `App Settings -> Privacy`
3. Go to Reader
4. Go to any post
5. Select some text
6. **Verify** the `reader_article_text_highlighted` event is tracked (use logcat)
7. Tap on `Copy` in the context menu
8. **Verify** the `reader_article_text_copied` event is tracked

Note: the highlight event has a slight delay (1 second) due to a debounce mechanism. This was done to make sure the event is sent usually after the user selects and adjusts the selection and avoid sending multiple events for each selection change. 

-----

## Regression Notes

1. Potential unintended areas of impact

    - N/A

10. What I did to test those areas of impact (or what existing automated tests I relied on)

    - N/A

11. What automated tests I added (or what prevented me from doing so)

    - Added unit tests for tracking

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## Testing Checklist (strike-out the not-applying and unnecessary ones):

- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)


Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModelTest"

Fix patch:
diff --git a/WordPress/src/jetpack/assets/reader_text_events.js b/WordPress/src/jetpack/assets/reader_text_events.js
new file mode 100644
index 000000000000..68cc132b5eeb
--- /dev/null
+++ b/WordPress/src/jetpack/assets/reader_text_events.js
@@ -0,0 +1,26 @@
+/*
+ * wvHandler is the name of the message handler in the webview, added via createJsObject inside
+ * ReaderPostRenderer. It handles the `postMessage` calls from the WebView.
+ */
+
+function debounce(fn, timeout) {
+    let timer;
+    return () => {
+        clearTimeout(timer);
+        timer = setTimeout(fn, timeout);
+    }
+}
+
+const textHighlighted = debounce(
+    () => wvHandler.postMessage("articleTextHighlighted"),
+    1000
+);
+
+document.addEventListener('selectionchange', function(event) {
+    const selection = document.getSelection().toString();
+    if (selection.length > 0) {
+        textHighlighted();
+    }
+});
+
+document.addEventListener('copy', event => wvHandler.postMessage("articleTextCopied"));
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..6b69d3320af1 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
@@ -94,6 +94,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder
 import org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType
 import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption
 import org.wordpress.android.ui.reader.ReaderPostPagerActivity.DirectOperation
+import org.wordpress.android.ui.reader.ReaderPostRenderer.ReaderPostMessageListener
 import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType
 import org.wordpress.android.ui.reader.actions.ReaderActions
 import org.wordpress.android.ui.reader.actions.ReaderPostActions
@@ -1605,7 +1606,17 @@ class ReaderPostDetailFragment : ViewPagerFragment(),
             viewModel.post,
             readerCssProvider,
             getReadingPreferences()
-        )
+        ).also {
+            it.setPostMessageListener(object : ReaderPostMessageListener {
+                override fun onArticleTextCopied() {
+                    viewModel.onArticleTextCopied()
+                }
+
+                override fun onArticleTextHighlighted() {
+                    viewModel.onArticleTextHighlighted()
+                }
+            })
+        }
 
         // if the post is from private atomic site postpone render until we have a special access cookie
         if (post.isPrivateAtomic && privateAtomicCookie.isCookieRefreshRequired()) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java
index 2ac457f79167..c7157a096ebe 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java
@@ -3,14 +3,17 @@
 import android.annotation.SuppressLint;
 import android.net.Uri;
 import android.os.Handler;
+import android.webkit.WebView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import org.jsoup.Jsoup;
 import org.wordpress.android.R;
 import org.wordpress.android.WordPress;
 import org.wordpress.android.models.ReaderPost;
 import org.wordpress.android.models.ReaderPostDiscoverData;
+import org.wordpress.android.support.JsObjectKt;
 import org.wordpress.android.ui.reader.models.ReaderReadingPreferences;
 import org.wordpress.android.ui.reader.models.ReaderReadingPreferences.ThemeValues;
 import org.wordpress.android.ui.reader.utils.ImageSizeMap;
@@ -44,6 +47,7 @@
  * http://developer.android.com/guide/webapps/targeting.html
  */
 public class ReaderPostRenderer {
+    private static final String JAVASCRIPT_MESSAGE_HANDLER = "wvHandler";
     private final ReaderResourceVars mResourceVars;
     private final ReaderPost mPost;
     private final int mMinFullSizeWidthDp;
@@ -56,6 +60,8 @@ public class ReaderPostRenderer {
     private ReaderCssProvider mCssProvider;
     private ReaderReadingPreferences mReadingPreferences;
     private ReaderReadingPreferences.ThemeValues mReadingPreferencesTheme;
+    @Nullable
+    private ReaderPostMessageListener mPostMessageListener = null;
 
     @SuppressLint("SetJavaScriptEnabled")
     public ReaderPostRenderer(ReaderWebView webView, ReaderPost post, ReaderCssProvider cssProvider,
@@ -80,6 +86,7 @@ public ReaderPostRenderer(ReaderWebView webView, ReaderPost post, ReaderCssProvi
         // enable JavaScript in the webView, otherwise videos and other embedded content won't
         // work - note that the content is scrubbed on the backend so this is considered safe
         webView.getSettings().setJavaScriptEnabled(true);
+        setWebViewMessageHandler(webView);
     }
 
     public void beginRender() {
@@ -543,6 +550,7 @@ private String formatPostContentForWebView(final String content, final Set<Strin
         }
 
         sbHtml.append("</head><body class=\"reader-full-post reader-full-post__story-content\">")
+              .append("<script type=\"text/javascript\" src=\"file:///android_asset/reader_text_events.js\"></script>")
               .append(contentCustomised)
               .append("</body></html>");
 
@@ -635,4 +643,39 @@ private String getContentTextProperties() {
                + "font-weight: 400; "
                + "font-size: " + mReadingPreferences.getFontSize().getValue() + "px; ";
     }
+
+    private void setWebViewMessageHandler(@NonNull WebView webView) {
+        Set<String> allowedOrigins = new HashSet<>();
+        allowedOrigins.add("*");
+
+        JsObjectKt.createJsObject(
+                webView, JAVASCRIPT_MESSAGE_HANDLER, allowedOrigins,
+                (message) -> {
+                    if (mPostMessageListener == null) {
+                        return null;
+                    }
+
+                    switch (message) {
+                        case ReaderPostMessageListener.MSG_ARTICLE_TEXT_COPIED:
+                            mPostMessageListener.onArticleTextCopied();
+                            break;
+                        case ReaderPostMessageListener.MSG_ARTICLE_TEXT_HIGHLIGHTED:
+                            mPostMessageListener.onArticleTextHighlighted();
+                            break;
+                    }
+                    return null;
+                });
+    }
+
+    void setPostMessageListener(@Nullable ReaderPostMessageListener listener) {
+        mPostMessageListener = listener;
+    }
+
+    interface ReaderPostMessageListener {
+        String MSG_ARTICLE_TEXT_COPIED = "articleTextCopied";
+        String MSG_ARTICLE_TEXT_HIGHLIGHTED = "articleTextHighlighted";
+
+        void onArticleTextCopied();
+        void onArticleTextHighlighted();
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt
index 05aeae289c44..884564b2d79d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt
@@ -13,7 +13,6 @@ import kotlinx.coroutines.withContext
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode.MAIN
 import org.wordpress.android.R
-import org.wordpress.android.analytics.AnalyticsTracker
 import org.wordpress.android.analytics.AnalyticsTracker.Stat
 import org.wordpress.android.datasets.wrappers.ReaderCommentTableWrapper
 import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper
@@ -587,9 +586,9 @@ class ReaderPostDetailViewModel @Inject constructor(
 
     private fun trackRelatedPostClickAction(postId: Long, blogId: Long, isGlobal: Boolean) {
         val stat = if (isGlobal) {
-            AnalyticsTracker.Stat.READER_GLOBAL_RELATED_POST_CLICKED
+            Stat.READER_GLOBAL_RELATED_POST_CLICKED
         } else {
-            AnalyticsTracker.Stat.READER_LOCAL_RELATED_POST_CLICKED
+            Stat.READER_LOCAL_RELATED_POST_CLICKED
         }
         readerTracker.trackPost(stat, findPost(blogId, postId))
     }
@@ -625,7 +624,7 @@ class ReaderPostDetailViewModel @Inject constructor(
 
     private fun updatePostDetailsUi() {
         post?.let {
-            readerTracker.trackPost(AnalyticsTracker.Stat.READER_ARTICLE_RENDERED, it)
+            readerTracker.trackPost(Stat.READER_ARTICLE_RENDERED, it)
             _navigationEvents.postValue(Event(ShowPostInWebView(it)))
             _uiState.value = convertPostToUiState(it)
         }
@@ -687,9 +686,9 @@ class ReaderPostDetailViewModel @Inject constructor(
 
     private fun trackNotAuthorisedState() {
         if (shouldOfferSignIn) {
-            post?.let { readerTracker.trackPost(AnalyticsTracker.Stat.READER_WPCOM_SIGN_IN_NEEDED, it) }
+            post?.let { readerTracker.trackPost(Stat.READER_WPCOM_SIGN_IN_NEEDED, it) }
         }
-        post?.let { readerTracker.trackPost(AnalyticsTracker.Stat.READER_USER_UNAUTHORIZED, it) }
+        post?.let { readerTracker.trackPost(Stat.READER_USER_UNAUTHORIZED, it) }
     }
 
     private fun getNotAuthorisedErrorMessageRes() = if (!shouldOfferSignIn) {
@@ -1012,6 +1011,14 @@ class ReaderPostDetailViewModel @Inject constructor(
         }
     }
 
+    fun onArticleTextCopied() {
+        readerTracker.track(Stat.READER_ARTICLE_TEXT_COPIED)
+    }
+
+    fun onArticleTextHighlighted() {
+        readerTracker.track(Stat.READER_ARTICLE_TEXT_HIGHLIGHTED)
+    }
+
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
         const val MAX_NUM_LIKES_FACES_WITH_SELF = 6
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 83135f79892e..699f1078283e 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
@@ -42,6 +42,10 @@ public enum Stat {
         READER_ARTICLE_DETAIL_UNLIKED,
         READER_ARTICLE_RENDERED,
         READER_ARTICLE_VISITED,
+        READER_ARTICLE_TEXT_HIGHLIGHTED,
+        READER_ARTICLE_TEXT_COPIED,
+        READER_COMMENT_TEXT_HIGHLIGHTED,
+        READER_COMMENT_TEXT_COPIED,
         READER_USER_BLOCKED,
         READER_BLOG_BLOCKED,
         READER_BLOG_FOLLOWED("reader_site_followed"),


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt
index a2a7ea471207..ee39aae42d1c 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt
@@ -27,6 +27,7 @@ import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.R
+import org.wordpress.android.analytics.AnalyticsTracker
 import org.wordpress.android.datasets.wrappers.ReaderCommentTableWrapper
 import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper
 import org.wordpress.android.fluxc.model.AccountModel
@@ -1100,6 +1101,18 @@ class ReaderPostDetailViewModelTest : BaseUnitTest() {
         )
     }
 
+    @Test
+    fun `onArticleTextCopied tracks the reader_article_text_copied event`() {
+        viewModel.onArticleTextCopied()
+        verify(readerTracker).track(AnalyticsTracker.Stat.READER_ARTICLE_TEXT_COPIED)
+    }
+
+    @Test
+    fun `onArticleTextHighlighted tracks the reader_article_text_highlighted event`() {
+        viewModel.onArticleTextHighlighted()
+        verify(readerTracker).track(AnalyticsTracker.Stat.READER_ARTICLE_TEXT_HIGHLIGHTED)
+    }
+
     private fun <T> testWithoutLocalPost(block: suspend CoroutineScope.() -> T) {
         test {
             whenever(readerGetPostUseCase.get(any(), any(), any())).thenReturn(Pair(null, false))