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.CallbackExecutor;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.RequiresPermission;
25 import android.annotation.SystemApi;
26 import android.content.Context;
27 import android.os.Binder;
28 import android.os.IPullAtomCallback;
29 import android.os.IPullAtomResultReceiver;
30 import android.os.IStatsManagerService;
31 import android.os.RemoteException;
32 import android.os.StatsFrameworkInitializer;
33 import android.util.AndroidException;
34 import android.util.Log;
35 import android.util.StatsEvent;
36 import android.util.StatsEventParcel;
37 
38 import com.android.internal.annotations.GuardedBy;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.concurrent.Executor;
44 
45 /**
46  * API for statsd clients to send configurations and retrieve data.
47  *
48  * @hide
49  */
50 @SystemApi
51 public final class StatsManager {
52     private static final String TAG = "StatsManager";
53     private static final boolean DEBUG = false;
54 
55     private static final Object sLock = new Object();
56     private final Context mContext;
57 
58     @GuardedBy("sLock")
59     private IStatsManagerService mStatsManagerService;
60 
61     /**
62      * Long extra of uid that added the relevant stats config.
63      */
64     public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID";
65     /**
66      * Long extra of the relevant stats config's configKey.
67      */
68     public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY";
69     /**
70      * Long extra of the relevant statsd_config.proto's Subscription.id.
71      */
72     public static final String EXTRA_STATS_SUBSCRIPTION_ID =
73             "android.app.extra.STATS_SUBSCRIPTION_ID";
74     /**
75      * Long extra of the relevant statsd_config.proto's Subscription.rule_id.
76      */
77     public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
78             "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
79     /**
80      *   List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie.
81      *   Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}.
82      */
83     public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES =
84             "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES";
85     /**
86      * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
87      * information.
88      */
89     public static final String EXTRA_STATS_DIMENSIONS_VALUE =
90             "android.app.extra.STATS_DIMENSIONS_VALUE";
91     /**
92      * Long array extra of the active configs for the uid that added those configs.
93      */
94     public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS =
95             "android.app.extra.STATS_ACTIVE_CONFIG_KEYS";
96 
97     /**
98      * Broadcast Action: Statsd has started.
99      * Configurations and PendingIntents can now be sent to it.
100      */
101     public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED";
102 
103     // Pull atom callback return codes.
104     /**
105      * Value indicating that this pull was successful and that the result should be used.
106      *
107      **/
108     public static final int PULL_SUCCESS = 0;
109 
110     /**
111      * Value indicating that this pull was unsuccessful and that the result should not be used.
112      **/
113     public static final int PULL_SKIP = 1;
114 
115     /**
116      * @hide
117      **/
118     @VisibleForTesting public static final long DEFAULT_COOL_DOWN_MILLIS = 1_000L; // 1 second.
119 
120     /**
121      * @hide
122      **/
123     @VisibleForTesting public static final long DEFAULT_TIMEOUT_MILLIS = 2_000L; // 2 seconds.
124 
125     /**
126      * Constructor for StatsManagerClient.
127      *
128      * @hide
129      */
StatsManager(Context context)130     public StatsManager(Context context) {
131         mContext = context;
132     }
133 
134     /**
135      * Adds the given configuration and associates it with the given configKey. If a config with the
136      * given configKey already exists for the caller's uid, it is replaced with the new one.
137      *
138      * @param configKey An arbitrary integer that allows clients to track the configuration.
139      * @param config    Wire-encoded StatsdConfig proto that specifies metrics (and all
140      *                  dependencies eg, conditions and matchers).
141      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
142      * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
143      */
144     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
addConfig(long configKey, byte[] config)145     public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
146         synchronized (sLock) {
147             try {
148                 IStatsManagerService service = getIStatsManagerServiceLocked();
149                 // can throw IllegalArgumentException
150                 service.addConfiguration(configKey, config, mContext.getOpPackageName());
151             } catch (RemoteException e) {
152                 Log.e(TAG, "Failed to connect to statsmanager when adding configuration");
153                 throw new StatsUnavailableException("could not connect", e);
154             } catch (SecurityException e) {
155                 throw new StatsUnavailableException(e.getMessage(), e);
156             } catch (IllegalStateException e) {
157                 Log.e(TAG, "Failed to addConfig in statsmanager");
158                 throw new StatsUnavailableException(e.getMessage(), e);
159             }
160         }
161     }
162 
163     // TODO: Temporary for backwards compatibility. Remove.
164     /**
165      * @deprecated Use {@link #addConfig(long, byte[])}
166      */
167     @Deprecated
168     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
addConfiguration(long configKey, byte[] config)169     public boolean addConfiguration(long configKey, byte[] config) {
170         try {
171             addConfig(configKey, config);
172             return true;
173         } catch (StatsUnavailableException | IllegalArgumentException e) {
174             return false;
175         }
176     }
177 
178     /**
179      * Remove a configuration from logging.
180      *
181      * @param configKey Configuration key to remove.
182      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
183      */
184     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
removeConfig(long configKey)185     public void removeConfig(long configKey) throws StatsUnavailableException {
186         synchronized (sLock) {
187             try {
188                 IStatsManagerService service = getIStatsManagerServiceLocked();
189                 service.removeConfiguration(configKey, mContext.getOpPackageName());
190             } catch (RemoteException e) {
191                 Log.e(TAG, "Failed to connect to statsmanager when removing configuration");
192                 throw new StatsUnavailableException("could not connect", e);
193             } catch (SecurityException e) {
194                 throw new StatsUnavailableException(e.getMessage(), e);
195             } catch (IllegalStateException e) {
196                 Log.e(TAG, "Failed to removeConfig in statsmanager");
197                 throw new StatsUnavailableException(e.getMessage(), e);
198             }
199         }
200     }
201 
202     // TODO: Temporary for backwards compatibility. Remove.
203     /**
204      * @deprecated Use {@link #removeConfig(long)}
205      */
206     @Deprecated
207     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
removeConfiguration(long configKey)208     public boolean removeConfiguration(long configKey) {
209         try {
210             removeConfig(configKey);
211             return true;
212         } catch (StatsUnavailableException e) {
213             return false;
214         }
215     }
216 
217     /**
218      * Set the PendingIntent to be used when broadcasting subscriber information to the given
219      * subscriberId within the given config.
220      * <p>
221      * Suppose that the calling uid has added a config with key configKey, and that in this config
222      * it is specified that when a particular anomaly is detected, a broadcast should be sent to
223      * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
224      * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
225      * when the anomaly is detected.
226      * <p>
227      * When statsd sends the broadcast, the PendingIntent will used to send an intent with
228      * information of
229      * {@link #EXTRA_STATS_CONFIG_UID},
230      * {@link #EXTRA_STATS_CONFIG_KEY},
231      * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
232      * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID},
233      * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and
234      * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
235      * <p>
236      * This function can only be called by the owner (uid) of the config. It must be called each
237      * time statsd starts. The config must have been added first (via {@link #addConfig}).
238      *
239      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
240      *                      associated with the given subscriberId. May be null, in which case
241      *                      it undoes any previous setting of this subscriberId.
242      * @param configKey     The integer naming the config to which this subscriber is attached.
243      * @param subscriberId  ID of the subscriber, as used in the config.
244      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
245      */
246     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setBroadcastSubscriber( PendingIntent pendingIntent, long configKey, long subscriberId)247     public void setBroadcastSubscriber(
248             PendingIntent pendingIntent, long configKey, long subscriberId)
249             throws StatsUnavailableException {
250         synchronized (sLock) {
251             try {
252                 IStatsManagerService service = getIStatsManagerServiceLocked();
253                 if (pendingIntent != null) {
254                     service.setBroadcastSubscriber(configKey, subscriberId, pendingIntent,
255                             mContext.getOpPackageName());
256                 } else {
257                     service.unsetBroadcastSubscriber(configKey, subscriberId,
258                             mContext.getOpPackageName());
259                 }
260             } catch (RemoteException e) {
261                 Log.e(TAG, "Failed to connect to statsmanager when adding broadcast subscriber",
262                         e);
263                 throw new StatsUnavailableException("could not connect", e);
264             } catch (SecurityException e) {
265                 throw new StatsUnavailableException(e.getMessage(), e);
266             }
267         }
268     }
269 
270     // TODO: Temporary for backwards compatibility. Remove.
271     /**
272      * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)}
273      */
274     @Deprecated
275     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setBroadcastSubscriber( long configKey, long subscriberId, PendingIntent pendingIntent)276     public boolean setBroadcastSubscriber(
277             long configKey, long subscriberId, PendingIntent pendingIntent) {
278         try {
279             setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
280             return true;
281         } catch (StatsUnavailableException e) {
282             return false;
283         }
284     }
285 
286     /**
287      * Registers the operation that is called to retrieve the metrics data. This must be called
288      * each time statsd starts. The config must have been added first (via {@link #addConfig},
289      * although addConfig could have been called on a previous boot). This operation allows
290      * statsd to send metrics data whenever statsd determines that the metrics in memory are
291      * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
292      * the data, which also deletes the retrieved metrics from statsd's memory.
293      *
294      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
295      *                      associated with the given subscriberId. May be null, in which case
296      *                      it removes any associated pending intent with this configKey.
297      * @param configKey     The integer naming the config to which this operation is attached.
298      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
299      */
300     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setFetchReportsOperation(PendingIntent pendingIntent, long configKey)301     public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
302             throws StatsUnavailableException {
303         synchronized (sLock) {
304             try {
305                 IStatsManagerService service = getIStatsManagerServiceLocked();
306                 if (pendingIntent == null) {
307                     service.removeDataFetchOperation(configKey, mContext.getOpPackageName());
308                 } else {
309                     service.setDataFetchOperation(configKey, pendingIntent,
310                             mContext.getOpPackageName());
311                 }
312 
313             } catch (RemoteException e) {
314                 Log.e(TAG, "Failed to connect to statsmanager when registering data listener.");
315                 throw new StatsUnavailableException("could not connect", e);
316             } catch (SecurityException e) {
317                 throw new StatsUnavailableException(e.getMessage(), e);
318             }
319         }
320     }
321 
322     /**
323      * Registers the operation that is called whenever there is a change in which configs are
324      * active. This must be called each time statsd starts. This operation allows
325      * statsd to inform clients that they should pull data of the configs that are currently
326      * active. The activeConfigsChangedOperation should set periodic alarms to pull data of configs
327      * that are active and stop pulling data of configs that are no longer active.
328      *
329      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
330      *                      associated with the given subscriberId. May be null, in which case
331      *                      it removes any associated pending intent for this client.
332      * @return A list of configs that are currently active for this client. If the pendingIntent is
333      *         null, this will be an empty list.
334      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
335      */
336     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setActiveConfigsChangedOperation(@ullable PendingIntent pendingIntent)337     public @NonNull long[] setActiveConfigsChangedOperation(@Nullable PendingIntent pendingIntent)
338             throws StatsUnavailableException {
339         synchronized (sLock) {
340             try {
341                 IStatsManagerService service = getIStatsManagerServiceLocked();
342                 if (pendingIntent == null) {
343                     service.removeActiveConfigsChangedOperation(mContext.getOpPackageName());
344                     return new long[0];
345                 } else {
346                     return service.setActiveConfigsChangedOperation(pendingIntent,
347                             mContext.getOpPackageName());
348                 }
349 
350             } catch (RemoteException e) {
351                 Log.e(TAG, "Failed to connect to statsmanager "
352                         + "when registering active configs listener.");
353                 throw new StatsUnavailableException("could not connect", e);
354             } catch (SecurityException e) {
355                 throw new StatsUnavailableException(e.getMessage(), e);
356             }
357         }
358     }
359 
360     // TODO: Temporary for backwards compatibility. Remove.
361     /**
362      * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)}
363      */
364     @Deprecated
365     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setDataFetchOperation(long configKey, PendingIntent pendingIntent)366     public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
367         try {
368             setFetchReportsOperation(pendingIntent, configKey);
369             return true;
370         } catch (StatsUnavailableException e) {
371             return false;
372         }
373     }
374 
375     /**
376      * Request the data collected for the given configKey.
377      * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
378      *
379      * @param configKey Configuration key to retrieve data from.
380      * @return Serialized ConfigMetricsReportList proto.
381      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
382      */
383     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getReports(long configKey)384     public byte[] getReports(long configKey) throws StatsUnavailableException {
385         synchronized (sLock) {
386             try {
387                 IStatsManagerService service = getIStatsManagerServiceLocked();
388                 return service.getData(configKey, mContext.getOpPackageName());
389             } catch (RemoteException e) {
390                 Log.e(TAG, "Failed to connect to statsmanager when getting data");
391                 throw new StatsUnavailableException("could not connect", e);
392             } catch (SecurityException e) {
393                 throw new StatsUnavailableException(e.getMessage(), e);
394             } catch (IllegalStateException e) {
395                 Log.e(TAG, "Failed to getReports in statsmanager");
396                 throw new StatsUnavailableException(e.getMessage(), e);
397             }
398         }
399     }
400 
401     // TODO: Temporary for backwards compatibility. Remove.
402     /**
403      * @deprecated Use {@link #getReports(long)}
404      */
405     @Deprecated
406     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getData(long configKey)407     public @Nullable byte[] getData(long configKey) {
408         try {
409             return getReports(configKey);
410         } catch (StatsUnavailableException e) {
411             return null;
412         }
413     }
414 
415     /**
416      * Clients can request metadata for statsd. Will contain stats across all configurations but not
417      * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
418      * This getter is not destructive and will not reset any metrics/counters.
419      *
420      * @return Serialized StatsdStatsReport proto.
421      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
422      */
423     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getStatsMetadata()424     public byte[] getStatsMetadata() throws StatsUnavailableException {
425         synchronized (sLock) {
426             try {
427                 IStatsManagerService service = getIStatsManagerServiceLocked();
428                 return service.getMetadata(mContext.getOpPackageName());
429             } catch (RemoteException e) {
430                 Log.e(TAG, "Failed to connect to statsmanager when getting metadata");
431                 throw new StatsUnavailableException("could not connect", e);
432             } catch (SecurityException e) {
433                 throw new StatsUnavailableException(e.getMessage(), e);
434             } catch (IllegalStateException e) {
435                 Log.e(TAG, "Failed to getStatsMetadata in statsmanager");
436                 throw new StatsUnavailableException(e.getMessage(), e);
437             }
438         }
439     }
440 
441     // TODO: Temporary for backwards compatibility. Remove.
442     /**
443      * @deprecated Use {@link #getStatsMetadata()}
444      */
445     @Deprecated
446     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getMetadata()447     public @Nullable byte[] getMetadata() {
448         try {
449             return getStatsMetadata();
450         } catch (StatsUnavailableException e) {
451             return null;
452         }
453     }
454 
455     /**
456      * Returns the experiments IDs registered with statsd, or an empty array if there aren't any.
457      *
458      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
459      */
460     @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
getRegisteredExperimentIds()461     public long[] getRegisteredExperimentIds()
462             throws StatsUnavailableException {
463         synchronized (sLock) {
464             try {
465                 IStatsManagerService service = getIStatsManagerServiceLocked();
466                 return service.getRegisteredExperimentIds();
467             } catch (RemoteException e) {
468                 if (DEBUG) {
469                     Log.d(TAG,
470                             "Failed to connect to StatsManagerService when getting "
471                                     + "registered experiment IDs");
472                 }
473                 throw new StatsUnavailableException("could not connect", e);
474             } catch (IllegalStateException e) {
475                 Log.e(TAG, "Failed to getRegisteredExperimentIds in statsmanager");
476                 throw new StatsUnavailableException(e.getMessage(), e);
477             }
478         }
479     }
480 
481     /**
482      * Sets a callback for an atom when that atom is to be pulled. The stats service will
483      * invoke pullData in the callback when the stats service determines that this atom needs to be
484      * pulled. This method should not be called by third-party apps.
485      *
486      * @param atomTag           The tag of the atom for this puller callback.
487      * @param metadata          Optional metadata specifying the timeout, cool down time, and
488      *                          additive fields for mapping isolated to host uids.
489      * @param executor          The executor in which to run the callback.
490      * @param callback          The callback to be invoked when the stats service pulls the atom.
491      *
492      */
493     @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata, @NonNull @CallbackExecutor Executor executor, @NonNull StatsPullAtomCallback callback)494     public void setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata,
495             @NonNull @CallbackExecutor Executor executor,
496             @NonNull StatsPullAtomCallback callback) {
497         long coolDownMillis =
498                 metadata == null ? DEFAULT_COOL_DOWN_MILLIS : metadata.mCoolDownMillis;
499         long timeoutMillis = metadata == null ? DEFAULT_TIMEOUT_MILLIS : metadata.mTimeoutMillis;
500         int[] additiveFields = metadata == null ? new int[0] : metadata.mAdditiveFields;
501         if (additiveFields == null) {
502             additiveFields = new int[0];
503         }
504 
505         synchronized (sLock) {
506             try {
507                 IStatsManagerService service = getIStatsManagerServiceLocked();
508                 PullAtomCallbackInternal rec =
509                     new PullAtomCallbackInternal(atomTag, callback, executor);
510                 service.registerPullAtomCallback(
511                         atomTag, coolDownMillis, timeoutMillis, additiveFields, rec);
512             } catch (RemoteException e) {
513                 throw new RuntimeException("Unable to register pull callback", e);
514             }
515         }
516     }
517 
518     /**
519      * Clears a callback for an atom when that atom is to be pulled. Note that any ongoing
520      * pulls will still occur. This method should not be called by third-party apps.
521      *
522      * @param atomTag           The tag of the atom of which to unregister
523      *
524      */
525     @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
clearPullAtomCallback(int atomTag)526     public void clearPullAtomCallback(int atomTag) {
527         synchronized (sLock) {
528             try {
529                 IStatsManagerService service = getIStatsManagerServiceLocked();
530                 service.unregisterPullAtomCallback(atomTag);
531             } catch (RemoteException e) {
532                 throw new RuntimeException("Unable to unregister pull atom callback");
533             }
534         }
535     }
536 
537     private static class PullAtomCallbackInternal extends IPullAtomCallback.Stub {
538         public final int mAtomId;
539         public final StatsPullAtomCallback mCallback;
540         public final Executor mExecutor;
541 
PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor)542         PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor) {
543             mAtomId = atomId;
544             mCallback = callback;
545             mExecutor = executor;
546         }
547 
548         @Override
onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver)549         public void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver) {
550             long token = Binder.clearCallingIdentity();
551             try {
552                 mExecutor.execute(() -> {
553                     List<StatsEvent> data = new ArrayList<>();
554                     int successInt = mCallback.onPullAtom(atomTag, data);
555                     boolean success = successInt == PULL_SUCCESS;
556                     StatsEventParcel[] parcels = new StatsEventParcel[data.size()];
557                     for (int i = 0; i < data.size(); i++) {
558                         parcels[i] = new StatsEventParcel();
559                         parcels[i].buffer = data.get(i).getBytes();
560                     }
561                     try {
562                         resultReceiver.pullFinished(atomTag, success, parcels);
563                     } catch (RemoteException e) {
564                         Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
565                                 + " due to TransactionTooLarge. Calling pullFinish with no data");
566                         StatsEventParcel[] emptyData = new StatsEventParcel[0];
567                         try {
568                             resultReceiver.pullFinished(atomTag, /*success=*/false, emptyData);
569                         } catch (RemoteException nestedException) {
570                             Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
571                                     + " with empty payload");
572                         }
573                     }
574                 });
575             } finally {
576                 Binder.restoreCallingIdentity(token);
577             }
578         }
579     }
580 
581     /**
582      * Metadata required for registering a StatsPullAtomCallback.
583      * All fields are optional, and defaults will be used for fields that are unspecified.
584      *
585      */
586     public static class PullAtomMetadata {
587         private final long mCoolDownMillis;
588         private final long mTimeoutMillis;
589         private final int[] mAdditiveFields;
590 
591         // Private Constructor for builder
PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields)592         private PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields) {
593             mCoolDownMillis = coolDownMillis;
594             mTimeoutMillis = timeoutMillis;
595             mAdditiveFields = additiveFields;
596         }
597 
598         /**
599          *  Builder for PullAtomMetadata.
600          */
601         public static class Builder {
602             private long mCoolDownMillis;
603             private long mTimeoutMillis;
604             private int[] mAdditiveFields;
605 
606             /**
607              * Returns a new PullAtomMetadata.Builder object for constructing PullAtomMetadata for
608              * StatsManager#registerPullAtomCallback
609              */
Builder()610             public Builder() {
611                 mCoolDownMillis = DEFAULT_COOL_DOWN_MILLIS;
612                 mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
613                 mAdditiveFields = null;
614             }
615 
616             /**
617              * Set the cool down time of the pull in milliseconds. If two successive pulls are
618              * issued within the cool down, a cached version of the first pull will be used for the
619              * second pull. The minimum allowed cool down is 1 second.
620              */
621             @NonNull
setCoolDownMillis(long coolDownMillis)622             public Builder setCoolDownMillis(long coolDownMillis) {
623                 mCoolDownMillis = coolDownMillis;
624                 return this;
625             }
626 
627             /**
628              * Set the maximum time the pull can take in milliseconds. The maximum allowed timeout
629              * is 10 seconds.
630              */
631             @NonNull
setTimeoutMillis(long timeoutMillis)632             public Builder setTimeoutMillis(long timeoutMillis) {
633                 mTimeoutMillis = timeoutMillis;
634                 return this;
635             }
636 
637             /**
638              * Set the additive fields of this pulled atom.
639              *
640              * This is only applicable for atoms which have a uid field. When tasks are run in
641              * isolated processes, the data will be attributed to the host uid. Additive fields
642              * will be combined when the non-additive fields are the same.
643              */
644             @NonNull
setAdditiveFields(@onNull int[] additiveFields)645             public Builder setAdditiveFields(@NonNull int[] additiveFields) {
646                 mAdditiveFields = additiveFields;
647                 return this;
648             }
649 
650             /**
651              * Builds and returns a PullAtomMetadata object with the values set in the builder and
652              * defaults for unset fields.
653              */
654             @NonNull
build()655             public PullAtomMetadata build() {
656                 return new PullAtomMetadata(mCoolDownMillis, mTimeoutMillis, mAdditiveFields);
657             }
658         }
659 
660         /**
661          * Return the cool down time of this pull in milliseconds.
662          */
getCoolDownMillis()663         public long getCoolDownMillis() {
664             return mCoolDownMillis;
665         }
666 
667         /**
668          * Return the maximum amount of time this pull can take in milliseconds.
669          */
getTimeoutMillis()670         public long getTimeoutMillis() {
671             return mTimeoutMillis;
672         }
673 
674         /**
675          * Return the additive fields of this pulled atom.
676          *
677          * This is only applicable for atoms that have a uid field. When tasks are run in
678          * isolated processes, the data will be attributed to the host uid. Additive fields
679          * will be combined when the non-additive fields are the same.
680          */
681         @Nullable
getAdditiveFields()682         public int[] getAdditiveFields() {
683             return mAdditiveFields;
684         }
685     }
686 
687     /**
688      * Callback interface for pulling atoms requested by the stats service.
689      *
690      */
691     public interface StatsPullAtomCallback {
692         /**
693          * Pull data for the specified atom tag, filling in the provided list of StatsEvent data.
694          * @return {@link #PULL_SUCCESS} if the pull was successful, or {@link #PULL_SKIP} if not.
695          */
onPullAtom(int atomTag, @NonNull List<StatsEvent> data)696         int onPullAtom(int atomTag, @NonNull List<StatsEvent> data);
697     }
698 
699     @GuardedBy("sLock")
getIStatsManagerServiceLocked()700     private IStatsManagerService getIStatsManagerServiceLocked() {
701         if (mStatsManagerService != null) {
702             return mStatsManagerService;
703         }
704         mStatsManagerService = IStatsManagerService.Stub.asInterface(
705                 StatsFrameworkInitializer
706                 .getStatsServiceManager()
707                 .getStatsManagerServiceRegisterer()
708                 .get());
709         return mStatsManagerService;
710     }
711 
712     /**
713      * Exception thrown when communication with the stats service fails (eg if it is not available).
714      * This might be thrown early during boot before the stats service has started or if it crashed.
715      */
716     public static class StatsUnavailableException extends AndroidException {
StatsUnavailableException(String reason)717         public StatsUnavailableException(String reason) {
718             super("Failed to connect to statsd: " + reason);
719         }
720 
StatsUnavailableException(String reason, Throwable e)721         public StatsUnavailableException(String reason, Throwable e) {
722             super("Failed to connect to statsd: " + reason, e);
723         }
724     }
725 }
726