Metrics for content capture.

Bug: 119613670
Test: statsd_testdrive & manual test
Change-Id: If43465ccee7454a7ebf9e15caa23fce7bae33cfe
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index f5dd852..8e78637 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -299,6 +299,10 @@
         CarPowerStateChanged car_power_state_changed = 203;
         GarageModeInfo garage_mode_info = 204;
         TestAtomReported test_atom_reported = 205 [(log_from_module) = "cts"];
+        ContentCaptureCallerMismatchReported content_capture_caller_mismatch_reported = 206;
+        ContentCaptureServiceEvents content_capture_service_events = 207;
+        ContentCaptureSessionEvents content_capture_session_events = 208;
+        ContentCaptureFlushed content_capture_flushed = 209;
     }
 
     // Pulled events will start at field 10000.
@@ -4830,6 +4834,95 @@
 }
 
 /**
+ * Logs information about mismatched caller for content capture.
+ *
+ * Logged from:
+ *   frameworks/base/core/java/android/service/contentcapture/ContentCaptureService.java
+ */
+message ContentCaptureCallerMismatchReported {
+    optional string intended_package = 1;
+    optional string calling_package = 2;
+}
+
+/**
+ * Logs information about content capture service events.
+ *
+ * Logged from:
+ *   frameworks/base/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
+ */
+message ContentCaptureServiceEvents {
+    // The type of event.
+    enum Event {
+        UNKNOWN = 0;
+        ON_CONNECTED = 1;
+        ON_DISCONNECTED = 2;
+        SET_WHITELIST = 3;
+        SET_DISABLED = 4;
+        ON_USER_DATA_REMOVED = 5;
+    }
+    optional Event event = 1;
+    // component/package of content capture service.
+    optional string service_info = 2;
+    // component/package of target.
+    // it's a concatenated list of component/package for SET_WHITELIST event
+    // separated by " ".
+    optional string target_info = 3;
+}
+
+/**
+ * Logs information about content capture session events.
+ *
+ * Logged from:
+ *   frameworks/base/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
+ */
+message ContentCaptureSessionEvents {
+    // The type of event.
+    enum Event {
+        UNKNOWN = 0;
+        ON_SESSION_STARTED = 1;
+        ON_SESSION_FINISHED = 2;
+        SESSION_NOT_CREATED = 3;
+    }
+    optional int32 session_id = 1;
+    optional Event event = 2;
+    // (n/a on session finished)
+    optional int32 state_flags = 3;
+    // component/package of content capture service.
+    optional string service_info = 4;
+    // component/package of app.
+    // (n/a on session finished)
+    optional string app_info = 5;
+    optional bool is_child_session = 6;
+}
+
+/**
+ * Logs information about session being flushed.
+ *
+ * Logged from:
+ *   frameworks/base/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
+ */
+message ContentCaptureFlushed {
+    optional int32 session_id = 1;
+    // component/package of content capture service.
+    optional string service_info = 2;
+    // component/package of app.
+    optional string app_info = 3;
+    // session start/finish events
+    optional int32 child_session_started = 4;
+    optional int32 child_session_finished = 5;
+    // count of view events.
+    optional int32 view_appeared_count = 6;
+    optional int32 view_disappeared_count = 7;
+    optional int32 view_text_changed_count = 8;
+
+    // Flush stats.
+    optional int32 max_events = 9;
+    optional int32 idle_flush_freq = 10;
+    optional int32 text_flush_freq = 11;
+    optional int32 flush_reason = 12;
+}
+
+/**
  * Pulls on-device BatteryStats power use calculations for the overall device.
  */
 message DeviceCalculatedPowerUse {
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index 3345523..3d82946 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -29,6 +29,7 @@
 import android.annotation.TestApi;
 import android.app.Service;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -40,6 +41,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseIntArray;
+import android.util.StatsLog;
 import android.view.contentcapture.ContentCaptureCondition;
 import android.view.contentcapture.ContentCaptureContext;
 import android.view.contentcapture.ContentCaptureEvent;
@@ -114,6 +116,9 @@
     private Handler mHandler;
     private IContentCaptureServiceCallback mCallback;
 
+    private long mCallerMismatchTimeout = 1000;
+    private long mLastCallerMismatchLog;
+
     /**
      * Binder that receives calls from the system server.
      */
@@ -174,9 +179,10 @@
             new IContentCaptureDirectManager.Stub() {
 
         @Override
-        public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events) {
+        public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events, int reason,
+                ContentCaptureOptions options) {
             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents,
-                            ContentCaptureService.this, Binder.getCallingUid(), events));
+                    ContentCaptureService.this, Binder.getCallingUid(), events, reason, options));
         }
     };
 
@@ -422,14 +428,23 @@
     }
 
     private void handleSendEvents(int uid,
-            @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents) {
+            @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
+            @Nullable ContentCaptureOptions options) {
+        final List<ContentCaptureEvent> events = parceledEvents.getList();
+        if (events.isEmpty()) {
+            Log.w(TAG, "handleSendEvents() received empty list of events");
+            return;
+        }
+
+        // Metrics.
+        final FlushMetrics metrics = new FlushMetrics();
+        ComponentName activityComponent = null;
 
         // Most events belong to the same session, so we can keep a reference to the last one
         // to avoid creating too many ContentCaptureSessionId objects
         int lastSessionId = NO_SESSION_ID;
         ContentCaptureSessionId sessionId = null;
 
-        final List<ContentCaptureEvent> events = parceledEvents.getList();
         for (int i = 0; i < events.size(); i++) {
             final ContentCaptureEvent event = events.get(i);
             if (!handleIsRightCallerFor(event, uid)) continue;
@@ -437,22 +452,44 @@
             if (sessionIdInt != lastSessionId) {
                 sessionId = new ContentCaptureSessionId(sessionIdInt);
                 lastSessionId = sessionIdInt;
+                if (i != 0) {
+                    writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
+                    metrics.reset();
+                }
+            }
+            final ContentCaptureContext clientContext = event.getContentCaptureContext();
+            if (activityComponent == null && clientContext != null) {
+                activityComponent = clientContext.getActivityComponent();
             }
             switch (event.getType()) {
                 case ContentCaptureEvent.TYPE_SESSION_STARTED:
-                    final ContentCaptureContext clientContext = event.getContentCaptureContext();
                     clientContext.setParentSessionId(event.getParentSessionId());
                     mSessionUids.put(sessionIdInt, uid);
                     onCreateContentCaptureSession(clientContext, sessionId);
+                    metrics.sessionStarted++;
                     break;
                 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
                     mSessionUids.delete(sessionIdInt);
                     onDestroyContentCaptureSession(sessionId);
+                    metrics.sessionFinished++;
+                    break;
+                case ContentCaptureEvent.TYPE_VIEW_APPEARED:
+                    onContentCaptureEvent(sessionId, event);
+                    metrics.viewAppearedCount++;
+                    break;
+                case ContentCaptureEvent.TYPE_VIEW_DISAPPEARED:
+                    onContentCaptureEvent(sessionId, event);
+                    metrics.viewDisappearedCount++;
+                    break;
+                case ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED:
+                    onContentCaptureEvent(sessionId, event);
+                    metrics.viewTextChangedCount++;
                     break;
                 default:
                     onContentCaptureEvent(sessionId, event);
             }
         }
+        writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
     }
 
     private void handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData) {
@@ -497,7 +534,13 @@
         if (rightUid != uid) {
             Log.e(TAG, "invalid call from UID " + uid + ": session " + sessionId + " belongs to "
                     + rightUid);
-            //TODO(b/111276913): log metrics as this could be a malicious app forging a sessionId
+            long now = System.currentTimeMillis();
+            if (now - mLastCallerMismatchLog > mCallerMismatchTimeout) {
+                StatsLog.write(StatsLog.CONTENT_CAPTURE_CALLER_MISMATCH_REPORTED,
+                        getPackageManager().getNameForUid(rightUid),
+                        getPackageManager().getNameForUid(uid));
+                mLastCallerMismatchLog = now;
+            }
             return false;
         }
         return true;
@@ -528,4 +571,22 @@
             Slog.w(TAG, "Error async reporting result to client: " + e);
         }
     }
+
+    /**
+     * Logs the metrics for content capture events flushing.
+     */
+    private void writeFlushMetrics(int sessionId, @Nullable ComponentName app,
+            @NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options,
+            int flushReason) {
+        if (mCallback == null) {
+            Log.w(TAG, "writeSessionFlush(): no server callback");
+            return;
+        }
+
+        try {
+            mCallback.writeSessionFlush(sessionId, app, flushMetrics, options, flushReason);
+        } catch (RemoteException e) {
+            Log.e(TAG, "failed to write flush metrics: " + e);
+        }
+    }
 }
diff --git a/core/java/android/service/contentcapture/FlushMetrics.aidl b/core/java/android/service/contentcapture/FlushMetrics.aidl
new file mode 100644
index 0000000..d0b935f
--- /dev/null
+++ b/core/java/android/service/contentcapture/FlushMetrics.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.contentcapture;
+
+/* @hide */
+parcelable FlushMetrics;
diff --git a/core/java/android/service/contentcapture/FlushMetrics.java b/core/java/android/service/contentcapture/FlushMetrics.java
new file mode 100644
index 0000000..01f3a12
--- /dev/null
+++ b/core/java/android/service/contentcapture/FlushMetrics.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.contentcapture;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Holds metrics for content capture events flushing.
+ *
+ * @hide
+ */
+public final class FlushMetrics implements Parcelable {
+    public int viewAppearedCount;
+    public int viewDisappearedCount;
+    public int viewTextChangedCount;
+    public int sessionStarted;
+    public int sessionFinished;
+
+    /**
+     * Resets all flush metrics.
+     */
+    public void reset() {
+        viewAppearedCount = 0;
+        viewDisappearedCount = 0;
+        viewTextChangedCount = 0;
+        sessionStarted = 0;
+        sessionFinished = 0;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(sessionStarted);
+        out.writeInt(sessionFinished);
+        out.writeInt(viewAppearedCount);
+        out.writeInt(viewDisappearedCount);
+        out.writeInt(viewTextChangedCount);
+    }
+
+    @NonNull
+    public static final Creator<FlushMetrics> CREATOR = new Creator<FlushMetrics>() {
+        @NonNull
+        @Override
+        public FlushMetrics createFromParcel(Parcel in) {
+            final FlushMetrics flushMetrics = new FlushMetrics();
+            flushMetrics.sessionStarted = in.readInt();
+            flushMetrics.sessionFinished = in.readInt();
+            flushMetrics.viewAppearedCount = in.readInt();
+            flushMetrics.viewDisappearedCount = in.readInt();
+            flushMetrics.viewTextChangedCount = in.readInt();
+            return flushMetrics;
+        }
+
+        @Override
+        public FlushMetrics[] newArray(int size) {
+            return new FlushMetrics[size];
+        }
+    };
+}
diff --git a/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl b/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl
index 0550ad3..ea6e76b 100644
--- a/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl
+++ b/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl
@@ -18,6 +18,8 @@
 
 import android.content.ComponentName;
 import android.view.contentcapture.ContentCaptureCondition;
+import android.service.contentcapture.FlushMetrics;
+import android.content.ContentCaptureOptions;
 
 import java.util.List;
 
@@ -30,4 +32,8 @@
     void setContentCaptureWhitelist(in List<String> packages, in List<ComponentName> activities);
     void setContentCaptureConditions(String packageName, in List<ContentCaptureCondition> conditions);
     void disableSelf();
- }
+
+    // Logs aggregated content capture flush metrics to Statsd
+    void writeSessionFlush(int sessionId, in ComponentName app, in FlushMetrics flushMetrics,
+            in ContentCaptureOptions options, int flushReason);
+}
diff --git a/core/java/android/view/contentcapture/IContentCaptureDirectManager.aidl b/core/java/android/view/contentcapture/IContentCaptureDirectManager.aidl
index 8d8117b..959bf13 100644
--- a/core/java/android/view/contentcapture/IContentCaptureDirectManager.aidl
+++ b/core/java/android/view/contentcapture/IContentCaptureDirectManager.aidl
@@ -18,6 +18,7 @@
 
 import android.content.pm.ParceledListSlice;
 import android.view.contentcapture.ContentCaptureEvent;
+import android.content.ContentCaptureOptions;
 
 /**
   * Interface between an app (ContentCaptureManager / ContentCaptureSession) and the app providing
@@ -26,5 +27,6 @@
   * @hide
   */
 oneway interface IContentCaptureDirectManager {
-    void sendEvents(in ParceledListSlice events);
+    // reason and options are used only for metrics logging.
+    void sendEvents(in ParceledListSlice events, int reason, in ContentCaptureOptions options);
 }
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 7241664..c5a5f73 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -498,7 +498,7 @@
             }
 
             final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
-            mDirectServiceInterface.sendEvents(events);
+            mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions);
         } catch (RemoteException e) {
             Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
                     + ": " + e);
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
new file mode 100644
index 0000000..dd1b84b
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
+import android.service.contentcapture.FlushMetrics;
+import android.util.StatsLog;
+
+import java.util.List;
+
+/** @hide */
+public final class ContentCaptureMetricsLogger {
+    /**
+     * Class only contains static utility functions, and should not be instantiated
+     */
+    private ContentCaptureMetricsLogger() {
+    }
+
+    /** @hide */
+    public static void writeServiceEvent(int eventType, @NonNull String serviceName,
+            @Nullable String targetPackage) {
+        StatsLog.write(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS, eventType, serviceName,
+                targetPackage);
+    }
+
+    /** @hide */
+    public static void writeServiceEvent(int eventType, @NonNull ComponentName service,
+            @Nullable ComponentName target) {
+        writeServiceEvent(eventType, ComponentName.flattenToShortString(service),
+                ComponentName.flattenToShortString(target));
+    }
+
+    /** @hide */
+    public static void writeServiceEvent(int eventType, @NonNull ComponentName service,
+            @Nullable String targetPackage) {
+        writeServiceEvent(eventType, ComponentName.flattenToShortString(service), targetPackage);
+    }
+
+    /** @hide */
+    public static void writeServiceEvent(int eventType, @NonNull ComponentName service) {
+        writeServiceEvent(eventType, ComponentName.flattenToShortString(service), null);
+    }
+
+    /** @hide */
+    public static void writeSetWhitelistEvent(@Nullable ComponentName service,
+            @Nullable List<String> packages, @Nullable List<ComponentName> activities) {
+        final String serviceName = ComponentName.flattenToShortString(service);
+        StringBuilder stringBuilder = new StringBuilder();
+        if (packages != null && packages.size() > 0) {
+            final int size = packages.size();
+            stringBuilder.append(packages.get(0));
+            for (int i = 1; i < size; i++) {
+                stringBuilder.append(" ");
+                stringBuilder.append(packages.get(i));
+            }
+        }
+        if (activities != null && activities.size() > 0) {
+            stringBuilder.append(" ");
+            stringBuilder.append(activities.get(0).flattenToShortString());
+            final int size = activities.size();
+            for (int i = 1; i < size; i++) {
+                stringBuilder.append(" ");
+                stringBuilder.append(activities.get(i).flattenToShortString());
+            }
+        }
+        StatsLog.write(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS,
+                StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__SET_WHITELIST,
+                serviceName, stringBuilder.toString());
+    }
+
+    /** @hide */
+    public static void writeSessionEvent(int sessionId, int event, int flags,
+            @NonNull ComponentName service, @Nullable ComponentName app, boolean isChildSession) {
+        StatsLog.write(StatsLog.CONTENT_CAPTURE_SESSION_EVENTS, sessionId, event, flags,
+                ComponentName.flattenToShortString(service),
+                ComponentName.flattenToShortString(app), isChildSession);
+    }
+
+    /** @hide */
+    public static void writeSessionFlush(int sessionId, @NonNull ComponentName service,
+            @Nullable ComponentName app, @NonNull FlushMetrics fm,
+            @NonNull ContentCaptureOptions options, int flushReason) {
+        StatsLog.write(StatsLog.CONTENT_CAPTURE_FLUSHED, sessionId,
+                ComponentName.flattenToShortString(service),
+                ComponentName.flattenToShortString(app), fm.sessionStarted, fm.sessionFinished,
+                fm.viewAppearedCount, fm.viewDisappearedCount, fm.viewTextChangedCount,
+                options.maxBufferSize, options.idleFlushingFrequencyMs,
+                options.textChangeFlushingFrequencyMs, flushReason);
+    }
+}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 67c3d01..a186d4e 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -24,6 +24,9 @@
 import static android.view.contentcapture.ContentCaptureSession.STATE_NOT_WHITELISTED;
 import static android.view.contentcapture.ContentCaptureSession.STATE_NO_SERVICE;
 
+import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeServiceEvent;
+import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeSessionEvent;
+import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeSetWhitelistEvent;
 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_DATA;
 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE;
@@ -35,6 +38,7 @@
 import android.app.assist.AssistContent;
 import android.app.assist.AssistStructure;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.pm.ActivityPresentationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -48,6 +52,7 @@
 import android.service.contentcapture.ActivityEvent.ActivityEventType;
 import android.service.contentcapture.ContentCaptureService;
 import android.service.contentcapture.ContentCaptureServiceInfo;
+import android.service.contentcapture.FlushMetrics;
 import android.service.contentcapture.IContentCaptureServiceCallback;
 import android.service.contentcapture.SnapshotData;
 import android.util.ArrayMap;
@@ -55,6 +60,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
+import android.util.StatsLog;
 import android.view.contentcapture.ContentCaptureCondition;
 import android.view.contentcapture.DataRemovalRequest;
 
@@ -231,7 +237,6 @@
         resurrectSessionsLocked();
     }
 
-    // TODO(b/119613670): log metrics
     @GuardedBy("mLock")
     public void startSessionLocked(@NonNull IBinder activityToken,
             @NonNull ActivityPresentationInfo activityPresentationInfo, int sessionId, int uid,
@@ -263,9 +268,14 @@
 
         if (!enabled) {
             // TODO: it would be better to split in differet reasons, like
-            // STATE_DISABLED_NO_SERVICE and STATE_DISABLED_BY_DEVICE_POLICY
+            // STATE_DISABLED_NO and STATE_DISABLED_BY_DEVICE_POLICY
             setClientState(clientReceiver, STATE_DISABLED | STATE_NO_SERVICE,
                     /* binder= */ null);
+            // Log metrics.
+            writeSessionEvent(sessionId,
+                    StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
+                    STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName,
+                    componentName, /* isChildSession= */ false);
             return;
         }
         if (serviceComponentName == null) {
@@ -285,6 +295,11 @@
             }
             setClientState(clientReceiver, STATE_DISABLED | STATE_NOT_WHITELISTED,
                     /* binder= */ null);
+            // Log metrics.
+            writeSessionEvent(sessionId,
+                    StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
+                    STATE_DISABLED | STATE_NOT_WHITELISTED, serviceComponentName,
+                    componentName, /* isChildSession= */ false);
             return;
         }
 
@@ -294,6 +309,11 @@
                     + ": ignoring because it already exists for " + existingSession.mActivityToken);
             setClientState(clientReceiver, STATE_DISABLED | STATE_DUPLICATED_ID,
                     /* binder=*/ null);
+            // Log metrics.
+            writeSessionEvent(sessionId,
+                    StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
+                    STATE_DISABLED | STATE_DUPLICATED_ID,
+                    serviceComponentName, componentName, /* isChildSession= */ false);
             return;
         }
 
@@ -302,11 +322,15 @@
         }
 
         if (mRemoteService == null) {
-            // TODO(b/119613670): log metrics
             Slog.w(TAG, "startSession(id=" + existingSession + ", token=" + activityToken
                     + ": ignoring because service is not set");
             setClientState(clientReceiver, STATE_DISABLED | STATE_NO_SERVICE,
                     /* binder= */ null);
+            // Log metrics.
+            writeSessionEvent(sessionId,
+                    StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
+                    STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName,
+                    componentName, /* isChildSession= */ false);
             return;
         }
 
@@ -324,7 +348,6 @@
         newSession.notifySessionStartedLocked(clientReceiver);
     }
 
-    // TODO(b/119613670): log metrics
     @GuardedBy("mLock")
     public void finishSessionLocked(int sessionId) {
         if (!isEnabledLocked()) {
@@ -553,6 +576,7 @@
                         + " for user " + mUserId);
             }
             mMaster.mGlobalContentCaptureOptions.setWhitelist(mUserId, packages, activities);
+            writeSetWhitelistEvent(getServiceComponentName(), packages, activities);
 
             // Must disable session that are not the whitelist anymore...
             final int numSessions = mSessions.size();
@@ -602,7 +626,6 @@
                     mConditionsByPkg.put(packageName, new ArraySet<>(conditions));
                 }
             }
-            // TODO(b/119613670): log metrics
         }
 
         @Override
@@ -616,6 +639,15 @@
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
+            writeServiceEvent(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__SET_DISABLED,
+                    getServiceComponentName());
+        }
+
+        @Override
+        public void writeSessionFlush(int sessionId, ComponentName app, FlushMetrics flushMetrics,
+                ContentCaptureOptions options, int flushReason) {
+            ContentCaptureMetricsLogger.writeSessionFlush(sessionId, getServiceComponentName(), app,
+                    flushMetrics, options, flushReason);
         }
     }
 }
diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
index 00bfb88..01d33b0 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
@@ -18,6 +18,9 @@
 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
 
+import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeServiceEvent;
+import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeSessionEvent;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -28,6 +31,7 @@
 import android.service.contentcapture.IContentCaptureServiceCallback;
 import android.service.contentcapture.SnapshotData;
 import android.util.Slog;
+import android.util.StatsLog;
 import android.view.contentcapture.ContentCaptureContext;
 import android.view.contentcapture.DataRemovalRequest;
 
@@ -77,6 +81,8 @@
             if (connected) {
                 try {
                     mService.onConnected(mServerCallback, sVerbose, sDebug);
+                    writeServiceEvent(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_CONNECTED,
+                            mComponentName);
                 } finally {
                     // Update the system-service state, in case the service reconnected after
                     // dying
@@ -84,6 +90,8 @@
                 }
             } else {
                 mService.onDisconnected();
+                writeServiceEvent(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_DISCONNECTED,
+                        mComponentName);
             }
         } catch (Exception e) {
             Slog.w(mTag, "Exception calling onConnectedStateChanged(" + connected + "): " + e);
@@ -102,6 +110,10 @@
             @NonNull IResultReceiver clientReceiver, int initialState) {
         scheduleAsyncRequest(
                 (s) -> s.onSessionStarted(context, sessionId, uid, clientReceiver, initialState));
+        // Metrics logging.
+        writeSessionEvent(sessionId,
+                StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__ON_SESSION_STARTED, initialState,
+                getComponentName(), context.getActivityComponent(), /* is_child_session= */ false);
     }
 
     /**
@@ -110,6 +122,11 @@
      */
     public void onSessionFinished(int sessionId) {
         scheduleAsyncRequest((s) -> s.onSessionFinished(sessionId));
+        // Metrics logging.
+        writeSessionEvent(sessionId,
+                StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__ON_SESSION_FINISHED,
+                /* flags= */ 0, getComponentName(), /* app= */ null,
+                /* is_child_session= */ false);
     }
 
     /**
@@ -124,6 +141,8 @@
      */
     public void onDataRemovalRequest(@NonNull DataRemovalRequest request) {
         scheduleAsyncRequest((s) -> s.onDataRemovalRequest(request));
+        writeServiceEvent(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_USER_DATA_REMOVED,
+                mComponentName);
     }
 
     /**