1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.location.contexthub;
18 
19 import android.annotation.IntDef;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.hardware.location.ContextHubInfo;
23 import android.hardware.location.IContextHubClient;
24 import android.hardware.location.IContextHubClientCallback;
25 import android.hardware.location.NanoAppMessage;
26 import android.os.RemoteException;
27 import android.util.Log;
28 import android.util.proto.ProtoOutputStream;
29 
30 import com.android.server.location.ClientManagerProto;
31 
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.function.Consumer;
38 
39 /**
40  * A class that manages registration/unregistration of clients and manages messages to/from clients.
41  *
42  * @hide
43  */
44 /* package */ class ContextHubClientManager {
45     private static final String TAG = "ContextHubClientManager";
46 
47     /*
48      * The maximum host endpoint ID value that a client can be assigned.
49      */
50     private static final int MAX_CLIENT_ID = 0x7fff;
51 
52     /*
53      * Local flag to enable debug logging.
54      */
55     private static final boolean DEBUG_LOG_ENABLED = false;
56 
57     /*
58      * The context of the service.
59      */
60     private final Context mContext;
61 
62     /*
63      * The proxy to talk to the Context Hub.
64      */
65     private final IContextHubWrapper mContextHubProxy;
66 
67     /*
68      * A mapping of host endpoint IDs to the ContextHubClientBroker object of registered clients.
69      * A concurrent data structure is used since the registration/unregistration can occur in
70      * multiple threads.
71      */
72     private final ConcurrentHashMap<Short, ContextHubClientBroker> mHostEndPointIdToClientMap =
73             new ConcurrentHashMap<>();
74 
75     /*
76      * The next host endpoint ID to start iterating for the next available host endpoint ID.
77      */
78     private int mNextHostEndPointId = 0;
79 
80     /*
81      * The list of previous registration records.
82      */
83     private static final int NUM_CLIENT_RECORDS = 20;
84     private final ConcurrentLinkedEvictingDeque<RegistrationRecord> mRegistrationRecordDeque =
85             new ConcurrentLinkedEvictingDeque<>(NUM_CLIENT_RECORDS);
86 
87     @Retention(RetentionPolicy.SOURCE)
88     @IntDef(prefix = { "ACTION_" }, value = {
89             ACTION_REGISTERED,
90             ACTION_UNREGISTERED,
91             ACTION_CANCELLED,
92     })
93     public @interface Action {}
94     public static final int ACTION_REGISTERED = 0;
95     public static final int ACTION_UNREGISTERED = 1;
96     public static final int ACTION_CANCELLED = 2;
97 
98     /**
99      * A container class to store a record of ContextHubClient registration.
100      */
101     private class RegistrationRecord {
102         private final String mBroker;
103         private final int mAction;
104         private final long mTimestamp;
105 
RegistrationRecord(String broker, @Action int action)106         RegistrationRecord(String broker, @Action int action) {
107             mBroker = broker;
108             mAction = action;
109             mTimestamp = System.currentTimeMillis();
110         }
111 
dump(ProtoOutputStream proto)112         void dump(ProtoOutputStream proto) {
113             proto.write(ClientManagerProto.RegistrationRecord.TIMESTAMP_MS, mTimestamp);
114             proto.write(ClientManagerProto.RegistrationRecord.ACTION, mAction);
115             proto.write(ClientManagerProto.RegistrationRecord.BROKER, mBroker);
116         }
117 
118         @Override
toString()119         public String toString() {
120             StringBuilder sb = new StringBuilder();
121             sb.append(ContextHubServiceUtil.formatDateFromTimestamp(mTimestamp));
122             sb.append(" ");
123             sb.append(mAction == ACTION_REGISTERED ? "+ " : "- ");
124             sb.append(mBroker);
125             if (mAction == ACTION_CANCELLED) {
126                 sb.append(" (cancelled)");
127             }
128             return sb.toString();
129         }
130     }
131 
ContextHubClientManager(Context context, IContextHubWrapper contextHubProxy)132     /* package */ ContextHubClientManager(Context context, IContextHubWrapper contextHubProxy) {
133         mContext = context;
134         mContextHubProxy = contextHubProxy;
135     }
136 
137     /**
138      * Registers a new client with the service.
139      *
140      * @param contextHubInfo the object describing the hub this client is attached to
141      * @param clientCallback the callback interface of the client to register
142      * @param attributionTag an optional attribution tag within the given package
143      *
144      * @return the client interface
145      *
146      * @throws IllegalStateException if max number of clients have already registered
147      */
registerClient( ContextHubInfo contextHubInfo, IContextHubClientCallback clientCallback, String attributionTag, ContextHubTransactionManager transactionManager, String packageName)148     /* package */ IContextHubClient registerClient(
149             ContextHubInfo contextHubInfo, IContextHubClientCallback clientCallback,
150             String attributionTag, ContextHubTransactionManager transactionManager,
151             String packageName) {
152         ContextHubClientBroker broker;
153         synchronized (this) {
154             short hostEndPointId = getHostEndPointId();
155             broker = new ContextHubClientBroker(
156                     mContext, mContextHubProxy, this /* clientManager */, contextHubInfo,
157                     hostEndPointId, clientCallback, attributionTag, transactionManager,
158                     packageName);
159             mHostEndPointIdToClientMap.put(hostEndPointId, broker);
160             mRegistrationRecordDeque.add(
161                     new RegistrationRecord(broker.toString(), ACTION_REGISTERED));
162         }
163 
164         try {
165             broker.attachDeathRecipient();
166         } catch (RemoteException e) {
167             // The client process has died, so we close the connection and return null
168             Log.e(TAG, "Failed to attach death recipient to client");
169             broker.close();
170             return null;
171         }
172 
173         Log.d(TAG, "Registered client with host endpoint ID " + broker.getHostEndPointId());
174         return IContextHubClient.Stub.asInterface(broker);
175     }
176 
177     /**
178      * Registers a new client with the service.
179      *
180      * @param pendingIntent  the callback interface of the client to register
181      * @param contextHubInfo the object describing the hub this client is attached to
182      * @param nanoAppId      the ID of the nanoapp to receive Intent events for
183      * @param attributionTag an optional attribution tag within the given package
184      *
185      * @return the client interface
186      *
187      * @throws IllegalStateException    if there were too many registered clients at the service
188      */
registerClient( ContextHubInfo contextHubInfo, PendingIntent pendingIntent, long nanoAppId, String attributionTag, ContextHubTransactionManager transactionManager)189     /* package */ IContextHubClient registerClient(
190             ContextHubInfo contextHubInfo, PendingIntent pendingIntent, long nanoAppId,
191             String attributionTag, ContextHubTransactionManager transactionManager) {
192         ContextHubClientBroker broker;
193         String registerString = "Regenerated";
194         synchronized (this) {
195             broker = getClientBroker(contextHubInfo.getId(), pendingIntent, nanoAppId);
196 
197             if (broker == null) {
198                 short hostEndPointId = getHostEndPointId();
199                 broker = new ContextHubClientBroker(
200                         mContext, mContextHubProxy, this /* clientManager */, contextHubInfo,
201                         hostEndPointId, pendingIntent, nanoAppId, attributionTag,
202                         transactionManager);
203                 mHostEndPointIdToClientMap.put(hostEndPointId, broker);
204                 registerString = "Registered";
205                 mRegistrationRecordDeque.add(
206                         new RegistrationRecord(broker.toString(), ACTION_REGISTERED));
207             } else {
208                 // Update the attribution tag to the latest value provided by the client app in
209                 // case the app was updated and decided to change its tag.
210                 broker.setAttributionTag(attributionTag);
211             }
212         }
213 
214         Log.d(TAG, registerString + " client with host endpoint ID " + broker.getHostEndPointId());
215         return IContextHubClient.Stub.asInterface(broker);
216     }
217 
218     /**
219      * Handles a message sent from a nanoapp.
220      *
221      * @param contextHubId the ID of the hub where the nanoapp sent the message from
222      * @param hostEndpointId The host endpoint ID of the client that this message is for.
223      * @param message the message send by a nanoapp
224      * @param nanoappPermissions the set of permissions the nanoapp holds
225      * @param messagePermissions the set of permissions that should be used for attributing
226      * permissions when this message is consumed by a client
227      */
onMessageFromNanoApp( int contextHubId, short hostEndpointId, NanoAppMessage message, List<String> nanoappPermissions, List<String> messagePermissions)228     /* package */ void onMessageFromNanoApp(
229             int contextHubId, short hostEndpointId, NanoAppMessage message,
230             List<String> nanoappPermissions, List<String> messagePermissions) {
231         if (DEBUG_LOG_ENABLED) {
232             Log.v(TAG, "Received " + message);
233         }
234 
235         if (message.isBroadcastMessage()) {
236             // Broadcast messages shouldn't be sent with any permissions tagged per CHRE API
237             // requirements.
238             if (!messagePermissions.isEmpty()) {
239                 Log.wtf(TAG, "Received broadcast message with permissions from "
240                         + message.getNanoAppId());
241             }
242 
243             ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message, true);
244             broadcastMessage(contextHubId, message, nanoappPermissions, messagePermissions);
245         } else {
246             ContextHubClientBroker proxy = mHostEndPointIdToClientMap.get(hostEndpointId);
247             if (proxy != null) {
248                 ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
249                                                                           true);
250                 proxy.sendMessageToClient(message, nanoappPermissions, messagePermissions);
251             } else {
252                 ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
253                                                                           false);
254                 Log.e(TAG, "Cannot send message to unregistered client (host endpoint ID = "
255                         + hostEndpointId + ")");
256             }
257         }
258     }
259 
260     /**
261      * Unregisters a client from the service.
262      *
263      * This method should be invoked as a result of a client calling the ContextHubClient.close(),
264      * or if the client process has died.
265      *
266      * @param hostEndPointId the host endpoint ID of the client that has died
267      */
unregisterClient(short hostEndPointId)268     /* package */ void unregisterClient(short hostEndPointId) {
269         ContextHubClientBroker broker = mHostEndPointIdToClientMap.get(hostEndPointId);
270         if (broker != null) {
271             @Action int action =
272                     broker.isPendingIntentCancelled() ? ACTION_CANCELLED : ACTION_UNREGISTERED;
273             mRegistrationRecordDeque.add(new RegistrationRecord(broker.toString(), action));
274         }
275 
276         if (mHostEndPointIdToClientMap.remove(hostEndPointId) != null) {
277             Log.d(TAG, "Unregistered client with host endpoint ID " + hostEndPointId);
278         } else {
279             Log.e(TAG, "Cannot unregister non-existing client with host endpoint ID "
280                     + hostEndPointId);
281         }
282     }
283 
284     /**
285      * @param contextHubId the ID of the hub where the nanoapp was loaded
286      * @param nanoAppId    the ID of the nanoapp that was loaded
287      */
onNanoAppLoaded(int contextHubId, long nanoAppId)288     /* package */ void onNanoAppLoaded(int contextHubId, long nanoAppId) {
289         forEachClientOfHub(contextHubId, client -> client.onNanoAppLoaded(nanoAppId));
290     }
291 
292     /**
293      * @param contextHubId the ID of the hub where the nanoapp was unloaded
294      * @param nanoAppId    the ID of the nanoapp that was unloaded
295      */
onNanoAppUnloaded(int contextHubId, long nanoAppId)296     /* package */ void onNanoAppUnloaded(int contextHubId, long nanoAppId) {
297         forEachClientOfHub(contextHubId, client -> client.onNanoAppUnloaded(nanoAppId));
298     }
299 
300     /**
301      * @param contextHubId the ID of the hub that has reset
302      */
onHubReset(int contextHubId)303     /* package */ void onHubReset(int contextHubId) {
304         forEachClientOfHub(contextHubId, client -> client.onHubReset());
305     }
306 
307     /**
308      * @param contextHubId the ID of the hub that contained the nanoapp that aborted
309      * @param nanoAppId the ID of the nanoapp that aborted
310      * @param abortCode the nanoapp specific abort code
311      */
onNanoAppAborted(int contextHubId, long nanoAppId, int abortCode)312     /* package */ void onNanoAppAborted(int contextHubId, long nanoAppId, int abortCode) {
313         forEachClientOfHub(contextHubId, client -> client.onNanoAppAborted(nanoAppId, abortCode));
314     }
315 
316     /**
317      * Runs a command for each client that is attached to a hub with the given ID.
318      *
319      * @param contextHubId the ID of the hub
320      * @param callback     the command to invoke for the client
321      */
forEachClientOfHub( int contextHubId, Consumer<ContextHubClientBroker> callback)322     /* package */ void forEachClientOfHub(
323             int contextHubId, Consumer<ContextHubClientBroker> callback) {
324         for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
325             if (broker.getAttachedContextHubId() == contextHubId) {
326                 callback.accept(broker);
327             }
328         }
329     }
330 
331     /**
332      * Returns an available host endpoint ID.
333      *
334      * @returns an available host endpoint ID
335      *
336      * @throws IllegalStateException if max number of clients have already registered
337      */
getHostEndPointId()338     private short getHostEndPointId() {
339         if (mHostEndPointIdToClientMap.size() == MAX_CLIENT_ID + 1) {
340             throw new IllegalStateException("Could not register client - max limit exceeded");
341         }
342 
343         int id = mNextHostEndPointId;
344         for (int i = 0; i <= MAX_CLIENT_ID; i++) {
345             if (!mHostEndPointIdToClientMap.containsKey((short) id)) {
346                 mNextHostEndPointId = (id == MAX_CLIENT_ID) ? 0 : id + 1;
347                 break;
348             }
349 
350             id = (id == MAX_CLIENT_ID) ? 0 : id + 1;
351         }
352 
353         return (short) id;
354     }
355 
356     /**
357      * Broadcasts a message from a nanoapp to all clients attached to the associated hub.
358      *
359      * @param contextHubId the ID of the hub where the nanoapp sent the message from
360      * @param message      the message send by a nanoapp
361      */
broadcastMessage( int contextHubId, NanoAppMessage message, List<String> nanoappPermissions, List<String> messagePermissions)362     private void broadcastMessage(
363             int contextHubId, NanoAppMessage message, List<String> nanoappPermissions,
364             List<String> messagePermissions) {
365         forEachClientOfHub(contextHubId,
366                 client -> client.sendMessageToClient(
367                         message, nanoappPermissions, messagePermissions));
368     }
369 
370     /**
371      * Retrieves a ContextHubClientBroker object with a matching PendingIntent and Context Hub ID.
372      *
373      * @param pendingIntent the PendingIntent to match
374      * @param contextHubId  the ID of the Context Hub the client is attached to
375      * @return the matching ContextHubClientBroker, null if not found
376      */
getClientBroker( int contextHubId, PendingIntent pendingIntent, long nanoAppId)377     private ContextHubClientBroker getClientBroker(
378             int contextHubId, PendingIntent pendingIntent, long nanoAppId) {
379         for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
380             if (broker.hasPendingIntent(pendingIntent, nanoAppId)
381                     && broker.getAttachedContextHubId() == contextHubId) {
382                 return broker;
383             }
384         }
385 
386         return null;
387     }
388 
389     /**
390      * Dump debugging info as ClientManagerProto
391      *
392      * If the output belongs to a sub message, the caller is responsible for wrapping this function
393      * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}.
394      *
395      * @param proto the ProtoOutputStream to write to
396      */
dump(ProtoOutputStream proto)397     void dump(ProtoOutputStream proto) {
398         for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
399             long token = proto.start(ClientManagerProto.CLIENT_BROKERS);
400             broker.dump(proto);
401             proto.end(token);
402         }
403         Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator();
404         while (it.hasNext()) {
405             long token = proto.start(ClientManagerProto.REGISTRATION_RECORDS);
406             it.next().dump(proto);
407             proto.end(token);
408         }
409     }
410 
411     @Override
toString()412     public String toString() {
413         StringBuilder sb = new StringBuilder();
414         for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
415             sb.append(broker);
416             sb.append(System.lineSeparator());
417         }
418 
419         sb.append(System.lineSeparator());
420         sb.append("Registration History:");
421         sb.append(System.lineSeparator());
422         Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator();
423         while (it.hasNext()) {
424             sb.append(it.next());
425             sb.append(System.lineSeparator());
426         }
427 
428         return sb.toString();
429     }
430 }
431