Send logs to TextClassifier by calling onTextClassifierEvent in NAS

Whenever NAS called TextClassifier.suggestConversationActions,
it will cache the notification key to result id mapping.
The result id will be used to log subsequent events related to
these suggestions.

This change should allow us to collect CTR.
TODO: Log the coverage, i.e. among all suggestConversationActions
request, how many of them actually contains some suggestions.

BUG: 120803809
Test: atest SmartActionHelperTest
Test: Manual, add a log in TextClassifierImpl.onTextClassifierEvent
1. Send a message notification
2. Expand the notification, observe event is logged.
4. Clicked on one of the replies, observe event is logged.
5. Send another message to myself
6. Inline reply it, observe event is logged.

Change-Id: I590d9bfcdb7ae7ee7976740d71bf7f1204683939
diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
index b41096c..77cb4cd 100644
--- a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
+++ b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
@@ -17,6 +17,7 @@
 package android.view.textclassifier;
 
 import android.app.Person;
+import android.content.Context;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 
@@ -28,7 +29,10 @@
 import java.util.ArrayList;
 import java.util.Deque;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -84,6 +88,29 @@
                 new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
     }
 
+    /**
+     * Returns the result id for logging.
+     */
+    public static String createResultId(
+            Context context,
+            List<ConversationActions.Message> messages,
+            int modelVersion,
+            List<Locale> modelLocales) {
+        final StringJoiner localesJoiner = new StringJoiner(",");
+        for (Locale locale : modelLocales) {
+            localesJoiner.add(locale.toLanguageTag());
+        }
+        final String modelName = String.format(
+                Locale.US, "%s_v%d", localesJoiner.toString(), modelVersion);
+        final int hash = Objects.hash(
+                messages.stream()
+                        .map(ConversationActions.Message::getText)
+                        .collect(Collectors.toList()),
+                context.getPackageName());
+        return SelectionSessionLogger.SignatureParser.createSignature(
+                SelectionSessionLogger.CLASSIFIER_ID, modelName, hash);
+    }
+
     private static final class PersonEncoder {
         private final Map<Person, Integer> mMapping = new ArrayMap<>();
         private int mNextUserId = FIRST_NON_LOCAL_USER;
diff --git a/core/java/android/view/textclassifier/TextClassifierEvent.java b/core/java/android/view/textclassifier/TextClassifierEvent.java
index 3bb9ee8..f2fea02 100644
--- a/core/java/android/view/textclassifier/TextClassifierEvent.java
+++ b/core/java/android/view/textclassifier/TextClassifierEvent.java
@@ -27,6 +27,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
 
 /**
  * A text classifier event.
@@ -498,4 +499,25 @@
         }
         // TODO: Add build(boolean validate).
     }
+
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder(128);
+        out.append("TextClassifierEvent{");
+        out.append("mEventCategory=").append(mEventCategory);
+        out.append(", mEventType=").append(mEventType);
+        out.append(", mEventContext=").append(mEventContext);
+        out.append(", mResultId=").append(mResultId);
+        out.append(", mEventIndex=").append(mEventIndex);
+        out.append(", mEventTime=").append(mEventTime);
+        out.append(", mExtras=").append(mExtras);
+        out.append(", mRelativeWordStartIndex=").append(mRelativeWordStartIndex);
+        out.append(", mRelativeWordEndIndex=").append(mRelativeWordEndIndex);
+        out.append(", mRelativeSuggestedWordStartIndex=").append(mRelativeSuggestedWordStartIndex);
+        out.append(", mRelativeSuggestedWordEndIndex=").append(mRelativeSuggestedWordEndIndex);
+        out.append(", mActionIndices=").append(Arrays.toString(mActionIndices));
+        out.append(", mLanguage=").append(mLanguage);
+        out.append("}");
+        return out.toString();
+    }
 }
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 9b0f9c6..fcd06c3 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -80,6 +80,8 @@
 
     private static final String LOG_TAG = DEFAULT_LOG_TAG;
 
+    private static final boolean DEBUG = false;
+
     private static final File FACTORY_MODEL_DIR = new File("/etc/textclassifier/");
     // Annotator
     private static final String ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX =
@@ -109,6 +111,8 @@
     @GuardedBy("mLock") // Do not access outside this lock.
     private LangIdModel mLangIdImpl;
     @GuardedBy("mLock") // Do not access outside this lock.
+    private ModelFileManager.ModelFile mActionModelInUse;
+    @GuardedBy("mLock") // Do not access outside this lock.
     private ActionsSuggestionsModel mActionsImpl;
 
     private final Object mLoggerLock = new Object();
@@ -342,8 +346,10 @@
     }
 
     @Override
-    public void onTextClassifierEvent(@NonNull TextClassifierEvent event) {
-        // TODO: Implement.
+    public void onTextClassifierEvent(TextClassifierEvent event) {
+        if (DEBUG) {
+            Log.d(DEFAULT_LOG_TAG, "onTextClassifierEvent() called with: event = [" + event + "]");
+        }
     }
 
     /** @inheritDoc */
@@ -408,7 +414,12 @@
                                 .setConfidenceScore(nativeSuggestion.getScore())
                                 .build());
             }
-            return new ConversationActions(conversationActions, /*id*/ null);
+            String resultId = ActionsSuggestionsHelper.createResultId(
+                    mContext,
+                    request.getConversation(),
+                    mActionModelInUse.getVersion(),
+                    mActionModelInUse.getSupportedLocales());
+            return new ConversationActions(conversationActions, resultId);
         } catch (Throwable t) {
             // Avoid throwing from this method. Log the error.
             Log.e(LOG_TAG, "Error suggesting conversation actions.", t);
@@ -517,6 +528,7 @@
                 try {
                     if (pfd != null) {
                         mActionsImpl = new ActionsSuggestionsModel(pfd.getFd());
+                        mActionModelInUse = bestModel;
                     }
                 } finally {
                     maybeCloseAndLogError(pfd);