Dump contexthub in protobuf format

Add protobuf dumpsys of connmetrics. Primarily used by incident service
to capture an incident report proto.

Command to invoke (any of the following after lunch and env setup):
$ adb shell dumpsys contexthub --proto
$ adb shell incident 3051
$ incident_report 3051

Bug: 146086599
Test: $ incident_report 3051, then compare the content with "dumpsys
      contexthub"

Change-Id: I6c2ed76a1a17f9bfc60d07e68cd66d5eea00e9b0
diff --git a/core/java/android/hardware/location/ContextHubInfo.java b/core/java/android/hardware/location/ContextHubInfo.java
index a11f2e9..6d56d2d 100644
--- a/core/java/android/hardware/location/ContextHubInfo.java
+++ b/core/java/android/hardware/location/ContextHubInfo.java
@@ -21,6 +21,7 @@
 import android.hardware.contexthub.V1_0.ContextHub;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
 
 import java.util.Arrays;
 
@@ -270,6 +271,30 @@
         return retVal;
     }
 
+    /**
+     * Dump the internal state as a ContextHubInfoProto to the given ProtoOutputStream.
+     *
+     * If the output belongs to a sub message, the caller is responsible for wrapping this function
+     * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}.
+     *
+     * @hide
+     */
+    public void dump(ProtoOutputStream proto) {
+        proto.write(ContextHubInfoProto.ID, mId);
+        proto.write(ContextHubInfoProto.NAME, mName);
+        proto.write(ContextHubInfoProto.VENDOR, mVendor);
+        proto.write(ContextHubInfoProto.TOOLCHAIN, mToolchain);
+        proto.write(ContextHubInfoProto.PLATFORM_VERSION, mPlatformVersion);
+        proto.write(ContextHubInfoProto.STATIC_SW_VERSION, getStaticSwVersion());
+        proto.write(ContextHubInfoProto.TOOLCHAIN_VERSION, mToolchainVersion);
+        proto.write(ContextHubInfoProto.CHRE_PLATFORM_ID, mChrePlatformId);
+        proto.write(ContextHubInfoProto.PEAK_MIPS, mPeakMips);
+        proto.write(ContextHubInfoProto.STOPPED_POWER_DRAW_MW, mStoppedPowerDrawMw);
+        proto.write(ContextHubInfoProto.SLEEP_POWER_DRAW_MW, mSleepPowerDrawMw);
+        proto.write(ContextHubInfoProto.PEAK_POWER_DRAW_MW, mPeakPowerDrawMw);
+        proto.write(ContextHubInfoProto.MAX_PACKET_LENGTH_BYTES, mMaxPacketLengthBytes);
+    }
+
     @Override
     public boolean equals(@Nullable Object object) {
         if (object == this) {
diff --git a/core/proto/android/hardware/location/context_hub_info.proto b/core/proto/android/hardware/location/context_hub_info.proto
new file mode 100644
index 0000000..de5cd55
--- /dev/null
+++ b/core/proto/android/hardware/location/context_hub_info.proto
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+syntax = "proto2";
+package android.hardware.location;
+
+import "frameworks/base/core/proto/android/privacy.proto";
+
+option java_multiple_files = true;
+
+message ContextHubInfoProto {
+    option (android.msg_privacy).dest = DEST_AUTOMATIC;
+    // Context hub unique identifier
+    optional int32 id = 1;
+    // A name for the hub
+    optional string name = 2;
+    // A name for the vendor
+    optional string vendor = 3;
+    // Description of the tool chain
+    optional string toolchain = 4;
+    optional int32 platform_version = 5;
+    optional int32 static_sw_version = 6;
+    optional int32 toolchain_version = 7;
+    // The CHRE platform ID as defined in chre/version.h
+    optional int64 chre_platform_id = 8;
+    // Peak MIPS that this hub can deliver
+    optional float peak_mips = 9;
+    // Power draw in stopped state in milli watts
+    optional float stopped_power_draw_mw = 10;
+    // Power draw in sleep state in milli watts
+    optional float sleep_power_draw_mw = 11;
+    // Peak power draw in milli watts
+    optional float peak_power_draw_mw = 12;
+    // The maximum number of bytes that can be sent per message to the hub
+    optional int32 max_packet_length_bytes = 13;
+}
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index d08cbed..8adcc9e 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -36,6 +36,7 @@
 import "frameworks/base/core/proto/android/server/alarmmanagerservice.proto";
 import "frameworks/base/core/proto/android/server/fingerprint.proto";
 import "frameworks/base/core/proto/android/server/jobscheduler.proto";
+import "frameworks/base/core/proto/android/server/location/context_hub.proto";
 import "frameworks/base/core/proto/android/server/powermanagerservice.proto";
 import "frameworks/base/core/proto/android/server/rolemanagerservice.proto";
 import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
@@ -486,6 +487,11 @@
         (section).args = "connmetrics --proto"
     ];
 
+    optional com.android.server.location.ContextHubServiceProto context_hub = 3051 [
+        (section).type = SECTION_DUMPSYS,
+        (section).args = "contexthub --proto"
+    ];
+
     // Reserved for OEMs.
     extensions 50000 to 100000;
 }
diff --git a/core/proto/android/server/location/context_hub.proto b/core/proto/android/server/location/context_hub.proto
new file mode 100644
index 0000000..c87f791
--- /dev/null
+++ b/core/proto/android/server/location/context_hub.proto
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+syntax = "proto2";
+package com.android.server.location;
+
+import "frameworks/base/core/proto/android/hardware/location/context_hub_info.proto";
+import "frameworks/base/core/proto/android/privacy.proto";
+
+option java_multiple_files = true;
+
+message ContextHubServiceProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    repeated .android.hardware.location.ContextHubInfoProto context_hub_info = 1;
+    optional ClientManagerProto client_manager = 2;
+}
+
+message ClientManagerProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    message RegistrationRecord {
+        option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+        optional int64 timestamp_ms = 1;
+        optional int32 action = 2;
+        // ClientBroker endpoint id, contexthub id and package name
+        optional string broker = 3;
+    }
+
+    repeated ClientBrokerProto client_brokers = 1;
+    repeated RegistrationRecord registration_records = 2;
+}
+
+message ClientBrokerProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    optional int32 endpoint_id = 1;
+    optional int32 attached_context_hub_id = 2;
+    optional string package = 3;
+    optional int64 nano_app_id = 4;
+    optional bool pending_intent_request_valid = 5;
+    optional bool has_pending_intent = 6;
+    optional bool pending_intent_cancelled = 7;
+    optional bool registered = 8;
+
+}
diff --git a/services/core/java/com/android/server/location/ContextHubClientBroker.java b/services/core/java/com/android/server/location/ContextHubClientBroker.java
index 45d9bae..bb96e98 100644
--- a/services/core/java/com/android/server/location/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/ContextHubClientBroker.java
@@ -33,6 +33,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.proto.ProtoOutputStream;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Supplier;
@@ -449,6 +450,28 @@
         }
     }
 
+    /**
+     * Dump debugging info as ClientBrokerProto
+     *
+     * If the output belongs to a sub message, the caller is responsible for wrapping this function
+     * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}.
+     *
+     * @param proto the ProtoOutputStream to write to
+     */
+    void dump(ProtoOutputStream proto) {
+        proto.write(ClientBrokerProto.ENDPOINT_ID, getHostEndPointId());
+        proto.write(ClientBrokerProto.ATTACHED_CONTEXT_HUB_ID, getAttachedContextHubId());
+        proto.write(ClientBrokerProto.PACKAGE, mPackage);
+        if (mPendingIntentRequest.isValid()) {
+            proto.write(ClientBrokerProto.PENDING_INTENT_REQUEST_VALID, true);
+            proto.write(ClientBrokerProto.NANO_APP_ID, mPendingIntentRequest.getNanoAppId());
+        }
+        proto.write(ClientBrokerProto.HAS_PENDING_INTENT, mPendingIntentRequest.hasPendingIntent());
+        proto.write(ClientBrokerProto.PENDING_INTENT_CANCELLED, isPendingIntentCancelled());
+        proto.write(ClientBrokerProto.REGISTERED, mRegistered);
+
+    }
+
     @Override
     public String toString() {
         String out = "[ContextHubClient ";
diff --git a/services/core/java/com/android/server/location/ContextHubClientManager.java b/services/core/java/com/android/server/location/ContextHubClientManager.java
index 46db8dc..0f70bb8 100644
--- a/services/core/java/com/android/server/location/ContextHubClientManager.java
+++ b/services/core/java/com/android/server/location/ContextHubClientManager.java
@@ -27,10 +27,13 @@
 import android.hardware.location.NanoAppMessage;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.proto.ProtoOutputStream;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Calendar;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedDeque;
@@ -45,6 +48,11 @@
     private static final String TAG = "ContextHubClientManager";
 
     /*
+     * The DateFormat for printing RegistrationRecord.
+     */
+    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd HH:mm:ss.SSS");
+
+    /*
      * The maximum host endpoint ID value that a client can be assigned.
      */
     private static final int MAX_CLIENT_ID = 0x7fff;
@@ -123,24 +131,24 @@
     private class RegistrationRecord {
         private final String mBroker;
         private final int mAction;
-        private final String mDate;
+        private final long mTimestamp;
 
         RegistrationRecord(String broker, @Action int action) {
             mBroker = broker;
             mAction = action;
-            Calendar instance = Calendar.getInstance();
-            mDate = String.format("%02d", instance.get(Calendar.MONTH) + 1) // Jan == 0
-                + "/" + String.format("%02d", instance.get(Calendar.DAY_OF_MONTH))
-                + " " + String.format("%02d", instance.get(Calendar.HOUR_OF_DAY))
-                + ":" + String.format("%02d", instance.get(Calendar.MINUTE))
-                + ":" + String.format("%02d", instance.get(Calendar.SECOND))
-                + "." + String.format("%03d", instance.get(Calendar.MILLISECOND));
+            mTimestamp = System.currentTimeMillis();
+        }
+
+        void dump(ProtoOutputStream proto) {
+            proto.write(ClientManagerProto.RegistrationRecord.TIMESTAMP_MS, mTimestamp);
+            proto.write(ClientManagerProto.RegistrationRecord.ACTION, mAction);
+            proto.write(ClientManagerProto.RegistrationRecord.BROKER, mBroker);
         }
 
         @Override
         public String toString() {
             String out = "";
-            out += mDate + " ";
+            out += DATE_FORMAT.format(new Date(mTimestamp)) + " ";
             out += mAction == ACTION_REGISTERED ? "+ " : "- ";
             out += mBroker;
             if (mAction == ACTION_CANCELLED) {
@@ -376,6 +384,28 @@
         return null;
     }
 
+    /**
+     * Dump debugging info as ClientManagerProto
+     *
+     * If the output belongs to a sub message, the caller is responsible for wrapping this function
+     * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}.
+     *
+     * @param proto the ProtoOutputStream to write to
+     */
+    void dump(ProtoOutputStream proto) {
+        for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
+            long token = proto.start(ClientManagerProto.CLIENT_BROKERS);
+            broker.dump(proto);
+            proto.end(token);
+        }
+        Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator();
+        while (it.hasNext()) {
+            long token = proto.start(ClientManagerProto.REGISTRATION_RECORDS);
+            it.next().dump(proto);
+            proto.end(token);
+        }
+    }
+
     @Override
     public String toString() {
         String out = "";
diff --git a/services/core/java/com/android/server/location/ContextHubService.java b/services/core/java/com/android/server/location/ContextHubService.java
index 787a800..e79eddf 100644
--- a/services/core/java/com/android/server/location/ContextHubService.java
+++ b/services/core/java/com/android/server/location/ContextHubService.java
@@ -43,6 +43,7 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.DumpUtils;
 
@@ -782,6 +783,13 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
+        for (String arg : args) {
+            if ("--proto".equals(arg)) {
+                dump(new ProtoOutputStream(fd));
+                return;
+            }
+        }
+
         pw.println("Dumping ContextHub Service");
 
         pw.println("");
@@ -802,6 +810,20 @@
         // dump eventLog
     }
 
+    private void dump(ProtoOutputStream proto) {
+        mContextHubIdToInfoMap.values().forEach(hubInfo -> {
+            long token = proto.start(ContextHubServiceProto.CONTEXT_HUB_INFO);
+            hubInfo.dump(proto);
+            proto.end(token);
+        });
+
+        long token = proto.start(ContextHubServiceProto.CLIENT_MANAGER);
+        mClientManager.dump(proto);
+        proto.end(token);
+
+        proto.flush();
+    }
+
     private void checkPermissions() {
         ContextHubServiceUtil.checkPermissions(mContext);
     }