1 /*
2  * Copyright 2017 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 package android.app;
17 
18 import static android.Manifest.permission.DUMP;
19 import static android.Manifest.permission.PACKAGE_USAGE_STATS;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.RequiresPermission;
24 import android.annotation.SystemApi;
25 import android.content.Context;
26 import android.os.IBinder;
27 import android.os.IStatsManager;
28 import android.os.IStatsPullerCallback;
29 import android.os.RemoteException;
30 import android.os.ServiceManager;
31 import android.util.AndroidException;
32 import android.util.Slog;
33 
34 /**
35  * API for statsd clients to send configurations and retrieve data.
36  *
37  * @hide
38  */
39 @SystemApi
40 public final class StatsManager {
41     private static final String TAG = "StatsManager";
42     private static final boolean DEBUG = false;
43 
44     private final Context mContext;
45 
46     private IStatsManager mService;
47 
48     /**
49      * Long extra of uid that added the relevant stats config.
50      */
51     public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID";
52     /**
53      * Long extra of the relevant stats config's configKey.
54      */
55     public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY";
56     /**
57      * Long extra of the relevant statsd_config.proto's Subscription.id.
58      */
59     public static final String EXTRA_STATS_SUBSCRIPTION_ID =
60             "android.app.extra.STATS_SUBSCRIPTION_ID";
61     /**
62      * Long extra of the relevant statsd_config.proto's Subscription.rule_id.
63      */
64     public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
65             "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
66     /**
67      *   List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie.
68      *   Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}.
69      */
70     public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES =
71             "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES";
72     /**
73      * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
74      * information.
75      */
76     public static final String EXTRA_STATS_DIMENSIONS_VALUE =
77             "android.app.extra.STATS_DIMENSIONS_VALUE";
78     /**
79      * Long array extra of the active configs for the uid that added those configs.
80      */
81     public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS =
82             "android.app.extra.STATS_ACTIVE_CONFIG_KEYS";
83 
84     /**
85      * Broadcast Action: Statsd has started.
86      * Configurations and PendingIntents can now be sent to it.
87      */
88     public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED";
89 
90     /**
91      * Constructor for StatsManagerClient.
92      *
93      * @hide
94      */
StatsManager(Context context)95     public StatsManager(Context context) {
96         mContext = context;
97     }
98 
99     /**
100      * Adds the given configuration and associates it with the given configKey. If a config with the
101      * given configKey already exists for the caller's uid, it is replaced with the new one.
102      *
103      * @param configKey An arbitrary integer that allows clients to track the configuration.
104      * @param config    Wire-encoded StatsdConfig proto that specifies metrics (and all
105      *                  dependencies eg, conditions and matchers).
106      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
107      * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
108      */
109     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
addConfig(long configKey, byte[] config)110     public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
111         synchronized (this) {
112             try {
113                 IStatsManager service = getIStatsManagerLocked();
114                 // can throw IllegalArgumentException
115                 service.addConfiguration(configKey, config, mContext.getOpPackageName());
116             } catch (RemoteException e) {
117                 Slog.e(TAG, "Failed to connect to statsd when adding configuration");
118                 throw new StatsUnavailableException("could not connect", e);
119             } catch (SecurityException e) {
120                 throw new StatsUnavailableException(e.getMessage(), e);
121             }
122         }
123     }
124 
125     // TODO: Temporary for backwards compatibility. Remove.
126     /**
127      * @deprecated Use {@link #addConfig(long, byte[])}
128      */
129     @Deprecated
130     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
addConfiguration(long configKey, byte[] config)131     public boolean addConfiguration(long configKey, byte[] config) {
132         try {
133             addConfig(configKey, config);
134             return true;
135         } catch (StatsUnavailableException | IllegalArgumentException e) {
136             return false;
137         }
138     }
139 
140     /**
141      * Remove a configuration from logging.
142      *
143      * @param configKey Configuration key to remove.
144      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
145      */
146     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
removeConfig(long configKey)147     public void removeConfig(long configKey) throws StatsUnavailableException {
148         synchronized (this) {
149             try {
150                 IStatsManager service = getIStatsManagerLocked();
151                 service.removeConfiguration(configKey, mContext.getOpPackageName());
152             } catch (RemoteException e) {
153                 Slog.e(TAG, "Failed to connect to statsd when removing configuration");
154                 throw new StatsUnavailableException("could not connect", e);
155             } catch (SecurityException e) {
156                 throw new StatsUnavailableException(e.getMessage(), e);
157             }
158         }
159     }
160 
161     // TODO: Temporary for backwards compatibility. Remove.
162     /**
163      * @deprecated Use {@link #removeConfig(long)}
164      */
165     @Deprecated
166     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
removeConfiguration(long configKey)167     public boolean removeConfiguration(long configKey) {
168         try {
169             removeConfig(configKey);
170             return true;
171         } catch (StatsUnavailableException e) {
172             return false;
173         }
174     }
175 
176     /**
177      * Set the PendingIntent to be used when broadcasting subscriber information to the given
178      * subscriberId within the given config.
179      * <p>
180      * Suppose that the calling uid has added a config with key configKey, and that in this config
181      * it is specified that when a particular anomaly is detected, a broadcast should be sent to
182      * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
183      * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
184      * when the anomaly is detected.
185      * <p>
186      * When statsd sends the broadcast, the PendingIntent will used to send an intent with
187      * information of
188      * {@link #EXTRA_STATS_CONFIG_UID},
189      * {@link #EXTRA_STATS_CONFIG_KEY},
190      * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
191      * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID},
192      * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and
193      * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
194      * <p>
195      * This function can only be called by the owner (uid) of the config. It must be called each
196      * time statsd starts. The config must have been added first (via {@link #addConfig}).
197      *
198      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
199      *                      associated with the given subscriberId. May be null, in which case
200      *                      it undoes any previous setting of this subscriberId.
201      * @param configKey     The integer naming the config to which this subscriber is attached.
202      * @param subscriberId  ID of the subscriber, as used in the config.
203      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
204      */
205     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setBroadcastSubscriber( PendingIntent pendingIntent, long configKey, long subscriberId)206     public void setBroadcastSubscriber(
207             PendingIntent pendingIntent, long configKey, long subscriberId)
208             throws StatsUnavailableException {
209         synchronized (this) {
210             try {
211                 IStatsManager service = getIStatsManagerLocked();
212                 if (pendingIntent != null) {
213                     // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
214                     IBinder intentSender = pendingIntent.getTarget().asBinder();
215                     service.setBroadcastSubscriber(configKey, subscriberId, intentSender,
216                             mContext.getOpPackageName());
217                 } else {
218                     service.unsetBroadcastSubscriber(configKey, subscriberId,
219                             mContext.getOpPackageName());
220                 }
221             } catch (RemoteException e) {
222                 Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e);
223                 throw new StatsUnavailableException("could not connect", e);
224             } catch (SecurityException e) {
225                 throw new StatsUnavailableException(e.getMessage(), e);
226             }
227         }
228     }
229 
230     // TODO: Temporary for backwards compatibility. Remove.
231     /**
232      * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)}
233      */
234     @Deprecated
235     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setBroadcastSubscriber( long configKey, long subscriberId, PendingIntent pendingIntent)236     public boolean setBroadcastSubscriber(
237             long configKey, long subscriberId, PendingIntent pendingIntent) {
238         try {
239             setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
240             return true;
241         } catch (StatsUnavailableException e) {
242             return false;
243         }
244     }
245 
246     /**
247      * Registers the operation that is called to retrieve the metrics data. This must be called
248      * each time statsd starts. The config must have been added first (via {@link #addConfig},
249      * although addConfig could have been called on a previous boot). This operation allows
250      * statsd to send metrics data whenever statsd determines that the metrics in memory are
251      * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
252      * the data, which also deletes the retrieved metrics from statsd's memory.
253      *
254      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
255      *                      associated with the given subscriberId. May be null, in which case
256      *                      it removes any associated pending intent with this configKey.
257      * @param configKey     The integer naming the config to which this operation is attached.
258      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
259      */
260     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setFetchReportsOperation(PendingIntent pendingIntent, long configKey)261     public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
262             throws StatsUnavailableException {
263         synchronized (this) {
264             try {
265                 IStatsManager service = getIStatsManagerLocked();
266                 if (pendingIntent == null) {
267                     service.removeDataFetchOperation(configKey, mContext.getOpPackageName());
268                 } else {
269                     // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
270                     IBinder intentSender = pendingIntent.getTarget().asBinder();
271                     service.setDataFetchOperation(configKey, intentSender,
272                             mContext.getOpPackageName());
273                 }
274 
275             } catch (RemoteException e) {
276                 Slog.e(TAG, "Failed to connect to statsd when registering data listener.");
277                 throw new StatsUnavailableException("could not connect", e);
278             } catch (SecurityException e) {
279                 throw new StatsUnavailableException(e.getMessage(), e);
280             }
281         }
282     }
283 
284     /**
285      * Registers the operation that is called whenever there is a change in which configs are
286      * active. This must be called each time statsd starts. This operation allows
287      * statsd to inform clients that they should pull data of the configs that are currently
288      * active. The activeConfigsChangedOperation should set periodic alarms to pull data of configs
289      * that are active and stop pulling data of configs that are no longer active.
290      *
291      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
292      *                      associated with the given subscriberId. May be null, in which case
293      *                      it removes any associated pending intent for this client.
294      * @return A list of configs that are currently active for this client. If the pendingIntent is
295      *         null, this will be an empty list.
296      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
297      */
298     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setActiveConfigsChangedOperation(@ullable PendingIntent pendingIntent)299     public @NonNull long[] setActiveConfigsChangedOperation(@Nullable PendingIntent pendingIntent)
300             throws StatsUnavailableException {
301         synchronized (this) {
302             try {
303                 IStatsManager service = getIStatsManagerLocked();
304                 if (pendingIntent == null) {
305                     service.removeActiveConfigsChangedOperation(mContext.getOpPackageName());
306                     return new long[0];
307                 } else {
308                     // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
309                     IBinder intentSender = pendingIntent.getTarget().asBinder();
310                     return service.setActiveConfigsChangedOperation(intentSender,
311                             mContext.getOpPackageName());
312                 }
313 
314             } catch (RemoteException e) {
315                 Slog.e(TAG,
316                         "Failed to connect to statsd when registering active configs listener.");
317                 throw new StatsUnavailableException("could not connect", e);
318             } catch (SecurityException e) {
319                 throw new StatsUnavailableException(e.getMessage(), e);
320             }
321         }
322     }
323 
324     // TODO: Temporary for backwards compatibility. Remove.
325     /**
326      * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)}
327      */
328     @Deprecated
329     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setDataFetchOperation(long configKey, PendingIntent pendingIntent)330     public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
331         try {
332             setFetchReportsOperation(pendingIntent, configKey);
333             return true;
334         } catch (StatsUnavailableException e) {
335             return false;
336         }
337     }
338 
339     /**
340      * Request the data collected for the given configKey.
341      * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
342      *
343      * @param configKey Configuration key to retrieve data from.
344      * @return Serialized ConfigMetricsReportList proto.
345      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
346      */
347     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getReports(long configKey)348     public byte[] getReports(long configKey) throws StatsUnavailableException {
349         synchronized (this) {
350             try {
351                 IStatsManager service = getIStatsManagerLocked();
352                 return service.getData(configKey, mContext.getOpPackageName());
353             } catch (RemoteException e) {
354                 Slog.e(TAG, "Failed to connect to statsd when getting data");
355                 throw new StatsUnavailableException("could not connect", e);
356             } catch (SecurityException e) {
357                 throw new StatsUnavailableException(e.getMessage(), e);
358             }
359         }
360     }
361 
362     // TODO: Temporary for backwards compatibility. Remove.
363     /**
364      * @deprecated Use {@link #getReports(long)}
365      */
366     @Deprecated
367     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getData(long configKey)368     public @Nullable byte[] getData(long configKey) {
369         try {
370             return getReports(configKey);
371         } catch (StatsUnavailableException e) {
372             return null;
373         }
374     }
375 
376     /**
377      * Clients can request metadata for statsd. Will contain stats across all configurations but not
378      * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
379      * This getter is not destructive and will not reset any metrics/counters.
380      *
381      * @return Serialized StatsdStatsReport proto.
382      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
383      */
384     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getStatsMetadata()385     public byte[] getStatsMetadata() throws StatsUnavailableException {
386         synchronized (this) {
387             try {
388                 IStatsManager service = getIStatsManagerLocked();
389                 return service.getMetadata(mContext.getOpPackageName());
390             } catch (RemoteException e) {
391                 Slog.e(TAG, "Failed to connect to statsd when getting metadata");
392                 throw new StatsUnavailableException("could not connect", e);
393             } catch (SecurityException e) {
394                 throw new StatsUnavailableException(e.getMessage(), e);
395             }
396         }
397     }
398 
399     // TODO: Temporary for backwards compatibility. Remove.
400     /**
401      * @deprecated Use {@link #getStatsMetadata()}
402      */
403     @Deprecated
404     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getMetadata()405     public @Nullable byte[] getMetadata() {
406         try {
407             return getStatsMetadata();
408         } catch (StatsUnavailableException e) {
409             return null;
410         }
411     }
412 
413     /**
414      * Returns the experiments IDs registered with statsd, or an empty array if there aren't any.
415      *
416      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
417      */
418     @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
getRegisteredExperimentIds()419     public long[] getRegisteredExperimentIds()
420             throws StatsUnavailableException {
421         synchronized (this) {
422             try {
423                 IStatsManager service = getIStatsManagerLocked();
424                 if (service == null) {
425                     if (DEBUG) {
426                         Slog.d(TAG, "Failed to find statsd when getting experiment IDs");
427                     }
428                     return new long[0];
429                 }
430                 return service.getRegisteredExperimentIds();
431             } catch (RemoteException e) {
432                 if (DEBUG) {
433                     Slog.d(TAG,
434                             "Failed to connect to StatsCompanionService when getting "
435                                     + "registered experiment IDs");
436                 }
437                 return new long[0];
438             }
439         }
440     }
441 
442     /**
443      * Registers a callback for an atom when that atom is to be pulled. The stats service will
444      * invoke pullData in the callback when the stats service determines that this atom needs to be
445      * pulled. Currently, this only works for atoms with tags above 100,000 that do not have a uid.
446      *
447      * @param atomTag   The tag of the atom for this puller callback. Must be at least 100000.
448      * @param callback  The callback to be invoked when the stats service pulls the atom.
449      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
450      *
451      * @hide
452      */
453     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setPullerCallback(int atomTag, IStatsPullerCallback callback)454     public void setPullerCallback(int atomTag, IStatsPullerCallback callback)
455             throws StatsUnavailableException {
456         synchronized (this) {
457             try {
458                 IStatsManager service = getIStatsManagerLocked();
459                 if (callback == null) {
460                     service.unregisterPullerCallback(atomTag, mContext.getOpPackageName());
461                 } else {
462                     service.registerPullerCallback(atomTag, callback,
463                             mContext.getOpPackageName());
464                 }
465 
466             } catch (RemoteException e) {
467                 Slog.e(TAG, "Failed to connect to statsd when registering data listener.");
468                 throw new StatsUnavailableException("could not connect", e);
469             } catch (SecurityException e) {
470                 throw new StatsUnavailableException(e.getMessage(), e);
471             }
472         }
473     }
474 
475     private class StatsdDeathRecipient implements IBinder.DeathRecipient {
476         @Override
binderDied()477         public void binderDied() {
478             synchronized (this) {
479                 mService = null;
480             }
481         }
482     }
483 
getIStatsManagerLocked()484     private IStatsManager getIStatsManagerLocked() throws StatsUnavailableException {
485         if (mService != null) {
486             return mService;
487         }
488         mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats"));
489         if (mService == null) {
490             throw new StatsUnavailableException("could not be found");
491         }
492         try {
493             mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0);
494         } catch (RemoteException e) {
495             throw new StatsUnavailableException("could not connect when linkToDeath", e);
496         }
497         return mService;
498     }
499 
500     /**
501      * Exception thrown when communication with the stats service fails (eg if it is not available).
502      * This might be thrown early during boot before the stats service has started or if it crashed.
503      */
504     public static class StatsUnavailableException extends AndroidException {
StatsUnavailableException(String reason)505         public StatsUnavailableException(String reason) {
506             super("Failed to connect to statsd: " + reason);
507         }
508 
StatsUnavailableException(String reason, Throwable e)509         public StatsUnavailableException(String reason, Throwable e) {
510             super("Failed to connect to statsd: " + reason, e);
511         }
512     }
513 }
514