Adds client info in ContextHubService dump

Example:
...
=================== CLIENTS ====================
[ContextHubClient endpointID: 0, contextHub: 0, package: android.uid.system:1000]
[ContextHubClient endpointID: 1, contextHub: 0, package: com.google.oslo]
[ContextHubClient endpointID: 2, contextHub: 0, package: com.google.oslo]
[ContextHubClient endpointID: 3, contextHub: 0, package: com.google.oslo]
[ContextHubClient endpointID: 4, contextHub: 0, package: com.google.oslo]
[ContextHubClient endpointID: 5, contextHub: 0, package: com.google.oslo]
[ContextHubClient endpointID: 6, contextHub: 0, package: com.google.oslo]
[ContextHubClient endpointID: 7, contextHub: 0, package: android.uid.systemui:10124]
[ContextHubClient endpointID: 8, contextHub: 0, intentCreatorPackage: com.google.android.apps.scone, nanoAppId: 0x476f6f676c001016]

Registration history:
12/03 14:58:17.410 - [ContextHubClient endpointID: 9, contextHub: 0, package: com.google.uid.shared:10113]
12/03 14:58:17.403 + [ContextHubClient endpointID: 9, contextHub: 0, package: com.google.uid.shared:10113]
12/03 14:58:10.295 + [ContextHubClient endpointID: 8, contextHub: 0, intentCreatorPackage: com.google.android.apps.scone, nanoAppId: 0x476f6f676c001016]
12/03 14:58:05.501 + [ContextHubClient endpointID: 7, contextHub: 0, package: android.uid.systemui:10124]
12/03 14:58:05.379 + [ContextHubClient endpointID: 6, contextHub: 0, package: com.google.oslo]
12/03 14:58:05.375 + [ContextHubClient endpointID: 5, contextHub: 0, package: com.google.oslo]
12/03 14:58:05.371 + [ContextHubClient endpointID: 4, contextHub: 0, package: com.google.oslo]
12/03 14:58:05.367 + [ContextHubClient endpointID: 3, contextHub: 0, package: com.google.oslo]
12/03 14:58:05.365 + [ContextHubClient endpointID: 2, contextHub: 0, package: com.google.oslo]
12/03 14:58:05.362 + [ContextHubClient endpointID: 1, contextHub: 0, package: com.google.oslo]
12/03 14:58:04.257 + [ContextHubClient endpointID: 0, contextHub: 0, package: android.uid.system:1000]

Bug: 121033022
Test: adb shell dumpsys contexthub
Change-Id: I33f49c1e8a144ea2818add0e0b4c52d1f5454b65
diff --git a/services/core/java/com/android/server/location/ContextHubClientBroker.java b/services/core/java/com/android/server/location/ContextHubClientBroker.java
index 675e59e..45d9bae 100644
--- a/services/core/java/com/android/server/location/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/ContextHubClientBroker.java
@@ -29,10 +29,12 @@
 import android.hardware.location.IContextHubClient;
 import android.hardware.location.IContextHubClientCallback;
 import android.hardware.location.NanoAppMessage;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Supplier;
 
 /**
@@ -97,6 +99,16 @@
     private final PendingIntentRequest mPendingIntentRequest;
 
     /*
+     * The host package associated with this client.
+     */
+    private final String mPackage;
+
+    /*
+     * True if a PendingIntent has been cancelled.
+     */
+    private AtomicBoolean mIsPendingIntentCancelled = new AtomicBoolean(false);
+
+    /*
      * Helper class to manage registered PendingIntent requests from the client.
      */
     private class PendingIntentRequest {
@@ -110,11 +122,14 @@
          */
         private long mNanoAppId;
 
+        private boolean mValid = false;
+
         PendingIntentRequest() {}
 
         PendingIntentRequest(PendingIntent pendingIntent, long nanoAppId) {
             mPendingIntent = pendingIntent;
             mNanoAppId = nanoAppId;
+            mValid = true;
         }
 
         public long getNanoAppId() {
@@ -132,6 +147,10 @@
         public void clear() {
             mPendingIntent = null;
         }
+
+        public boolean isValid() {
+            return mValid;
+        }
     }
 
     /* package */ ContextHubClientBroker(
@@ -145,6 +164,7 @@
         mHostEndPointId = hostEndPointId;
         mCallbackInterface = callback;
         mPendingIntentRequest = new PendingIntentRequest();
+        mPackage = mContext.getPackageManager().getNameForUid(Binder.getCallingUid());
     }
 
     /* package */ ContextHubClientBroker(
@@ -157,6 +177,7 @@
         mAttachedContextHubInfo = contextHubInfo;
         mHostEndPointId = hostEndPointId;
         mPendingIntentRequest = new PendingIntentRequest(pendingIntent, nanoAppId);
+        mPackage = pendingIntent.getCreatorPackage();
     }
 
     /**
@@ -313,6 +334,13 @@
     }
 
     /**
+     * @return true if the client is a PendingIntent client that has been cancelled.
+     */
+    /* package */ boolean isPendingIntentCancelled() {
+        return mIsPendingIntentCancelled.get();
+    }
+
+    /**
      * Helper function to invoke a specified client callback, if the connection is open.
      *
      * @param consumer the consumer specifying the callback to invoke
@@ -392,6 +420,7 @@
                     Manifest.permission.LOCATION_HARDWARE /* requiredPermission */,
                     null /* options */);
         } catch (PendingIntent.CanceledException e) {
+            mIsPendingIntentCancelled.set(true);
             // The PendingIntent is no longer valid
             Log.w(TAG, "PendingIntent has been canceled, unregistering from client"
                     + " (host endpoint ID " + mHostEndPointId + ")");
@@ -419,4 +448,20 @@
             mRegistered = false;
         }
     }
+
+    @Override
+    public String toString() {
+        String out = "[ContextHubClient ";
+        out += "endpointID: " + getHostEndPointId() + ", ";
+        out += "contextHub: " + getAttachedContextHubId() + ", ";
+        if (mPendingIntentRequest.isValid()) {
+            out += "intentCreatorPackage: " + mPackage + ", ";
+            out += "nanoAppId: 0x" + Long.toHexString(mPendingIntentRequest.getNanoAppId());
+        } else {
+            out += "package: " + mPackage;
+        }
+        out += "]";
+
+        return out;
+    }
 }
diff --git a/services/core/java/com/android/server/location/ContextHubClientManager.java b/services/core/java/com/android/server/location/ContextHubClientManager.java
index 00b7d62..46db8dc 100644
--- a/services/core/java/com/android/server/location/ContextHubClientManager.java
+++ b/services/core/java/com/android/server/location/ContextHubClientManager.java
@@ -16,6 +16,7 @@
 
 package com.android.server.location;
 
+import android.annotation.IntDef;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.hardware.contexthub.V1_0.ContextHubMsg;
@@ -27,7 +28,12 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.function.Consumer;
 
 /**
@@ -71,6 +77,79 @@
      */
     private int mNextHostEndPointId = 0;
 
+    /*
+     * The list of previous registration records.
+     */
+    private static final int NUM_CLIENT_RECORDS = 20;
+    private final ConcurrentLinkedEvictingDeque<RegistrationRecord> mRegistrationRecordDeque =
+            new ConcurrentLinkedEvictingDeque<>(NUM_CLIENT_RECORDS);
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ACTION_" }, value = {
+            ACTION_REGISTERED,
+            ACTION_UNREGISTERED,
+            ACTION_CANCELLED,
+    })
+    public @interface Action {}
+    public static final int ACTION_REGISTERED = 0;
+    public static final int ACTION_UNREGISTERED = 1;
+    public static final int ACTION_CANCELLED = 2;
+
+    /**
+     * Helper class to make a ConcurrentLinkedDeque fixed-size, evicting old entries when full.
+     */
+    private class ConcurrentLinkedEvictingDeque<E> extends ConcurrentLinkedDeque<E> {
+        private int mSize;
+
+        ConcurrentLinkedEvictingDeque(int size) {
+            mSize = size;
+        }
+
+        @Override
+        public boolean add(E elem) {
+            synchronized (this) {
+                if (size() == mSize) {
+                    poll();
+                }
+
+                return super.add(elem);
+            }
+        }
+    }
+
+    /**
+     * A container class to store a record of ContextHubClient registration.
+     */
+    private class RegistrationRecord {
+        private final String mBroker;
+        private final int mAction;
+        private final String mDate;
+
+        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));
+        }
+
+        @Override
+        public String toString() {
+            String out = "";
+            out += mDate + " ";
+            out += mAction == ACTION_REGISTERED ? "+ " : "- ";
+            out += mBroker;
+            if (mAction == ACTION_CANCELLED) {
+                out += " (cancelled)";
+            }
+            return out;
+        }
+    }
+
     /* package */ ContextHubClientManager(
             Context context, IContexthub contextHubProxy) {
         mContext = context;
@@ -96,6 +175,8 @@
                     mContext, mContextHubProxy, this /* clientManager */, contextHubInfo,
                     hostEndPointId, clientCallback);
             mHostEndPointIdToClientMap.put(hostEndPointId, broker);
+            mRegistrationRecordDeque.add(
+                    new RegistrationRecord(broker.toString(), ACTION_REGISTERED));
         }
 
         try {
@@ -136,6 +217,8 @@
                         hostEndPointId, pendingIntent, nanoAppId);
                 mHostEndPointIdToClientMap.put(hostEndPointId, broker);
                 registerString = "Registered";
+                mRegistrationRecordDeque.add(
+                        new RegistrationRecord(broker.toString(), ACTION_REGISTERED));
             }
         }
 
@@ -178,6 +261,13 @@
      * @param hostEndPointId the host endpoint ID of the client that has died
      */
     /* package */ void unregisterClient(short hostEndPointId) {
+        ContextHubClientBroker broker = mHostEndPointIdToClientMap.get(hostEndPointId);
+        if (broker != null) {
+            @Action int action =
+                    broker.isPendingIntentCancelled() ? ACTION_CANCELLED : ACTION_UNREGISTERED;
+            mRegistrationRecordDeque.add(new RegistrationRecord(broker.toString(), action));
+        }
+
         if (mHostEndPointIdToClientMap.remove(hostEndPointId) != null) {
             Log.d(TAG, "Unregistered client with host endpoint ID " + hostEndPointId);
         } else {
@@ -285,4 +375,20 @@
 
         return null;
     }
+
+    @Override
+    public String toString() {
+        String out = "";
+        for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
+            out += broker + "\n";
+        }
+
+        out += "\nRegistration history:\n";
+        Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator();
+        while (it.hasNext()) {
+            out += it.next() + "\n";
+        }
+
+        return out;
+    }
 }
diff --git a/services/core/java/com/android/server/location/ContextHubService.java b/services/core/java/com/android/server/location/ContextHubService.java
index 36b0342..787a800 100644
--- a/services/core/java/com/android/server/location/ContextHubService.java
+++ b/services/core/java/com/android/server/location/ContextHubService.java
@@ -795,6 +795,10 @@
         // Dump nanoAppHash
         mNanoAppStateManager.foreachNanoAppInstanceInfo((info) -> pw.println(info));
 
+        pw.println("");
+        pw.println("=================== CLIENTS ====================");
+        pw.println(mClientManager);
+
         // dump eventLog
     }