• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import static android.Manifest.permission.READ_RESTRICTED_STATS;
21 
22 import android.annotation.CallbackExecutor;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.RequiresPermission;
26 import android.annotation.SystemApi;
27 import android.content.Context;
28 import android.os.Binder;
29 import android.os.Build;
30 import android.os.IPullAtomCallback;
31 import android.os.IPullAtomResultReceiver;
32 import android.os.IStatsManagerService;
33 import android.os.IStatsQueryCallback;
34 import android.os.OutcomeReceiver;
35 import android.os.RemoteException;
36 import android.os.StatsFrameworkInitializer;
37 import android.util.AndroidException;
38 import android.util.Log;
39 import android.util.StatsEvent;
40 import android.util.StatsEventParcel;
41 
42 import androidx.annotation.RequiresApi;
43 
44 import com.android.internal.annotations.GuardedBy;
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.modules.utils.build.SdkLevel;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.concurrent.Executor;
51 
52 /**
53  * API for statsd clients to send configurations and retrieve data.
54  *
55  * @hide
56  */
57 @SystemApi
58 public final class StatsManager {
59     private static final String TAG = "StatsManager";
60     private static final boolean DEBUG = false;
61 
62     private static final Object sLock = new Object();
63     private final Context mContext;
64 
65     @GuardedBy("sLock")
66     private IStatsManagerService mStatsManagerService;
67 
68     /**
69      * Long extra of uid that added the relevant stats config.
70      */
71     public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID";
72     /**
73      * Long extra of the relevant stats config's configKey.
74      */
75     public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY";
76     /**
77      * Long extra of the relevant statsd_config.proto's Subscription.id.
78      */
79     public static final String EXTRA_STATS_SUBSCRIPTION_ID =
80             "android.app.extra.STATS_SUBSCRIPTION_ID";
81     /**
82      * Long extra of the relevant statsd_config.proto's Subscription.rule_id.
83      */
84     public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
85             "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
86     /**
87      *   List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie.
88      *   Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}.
89      */
90     public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES =
91             "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES";
92     /**
93      * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
94      * information.
95      */
96     public static final String EXTRA_STATS_DIMENSIONS_VALUE =
97             "android.app.extra.STATS_DIMENSIONS_VALUE";
98     /**
99      * Long array extra of the active configs for the uid that added those configs.
100      */
101     public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS =
102             "android.app.extra.STATS_ACTIVE_CONFIG_KEYS";
103 
104     /**
105      * Long array extra of the restricted metric ids present for the client.
106      */
107     public static final String EXTRA_STATS_RESTRICTED_METRIC_IDS =
108             "android.app.extra.STATS_RESTRICTED_METRIC_IDS";
109 
110     /**
111      * Broadcast Action: Statsd has started.
112      * Configurations and PendingIntents can now be sent to it.
113      */
114     public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED";
115 
116     // Pull atom callback return codes.
117     /**
118      * Value indicating that this pull was successful and that the result should be used.
119      *
120      **/
121     public static final int PULL_SUCCESS = 0;
122 
123     /**
124      * Value indicating that this pull was unsuccessful and that the result should not be used.
125      **/
126     public static final int PULL_SKIP = 1;
127 
128     /**
129      * @hide
130      **/
131     @VisibleForTesting public static final long DEFAULT_COOL_DOWN_MILLIS = 1_000L; // 1 second.
132 
133     /**
134      * @hide
135      **/
136     @VisibleForTesting public static final long DEFAULT_TIMEOUT_MILLIS = 1_500L; // 1.5 seconds.
137 
138     /**
139      * Constructor for StatsManagerClient.
140      *
141      * @hide
142      */
StatsManager(Context context)143     public StatsManager(Context context) {
144         mContext = context;
145     }
146 
147     /**
148      * Adds the given configuration and associates it with the given configKey. If a config with the
149      * given configKey already exists for the caller's uid, it is replaced with the new one.
150      * This call can block on statsd.
151      *
152      * @param configKey An arbitrary integer that allows clients to track the configuration.
153      * @param config    Wire-encoded StatsdConfig proto that specifies metrics (and all
154      *                  dependencies eg, conditions and matchers).
155      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
156      * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
157      */
158     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
addConfig(long configKey, byte[] config)159     public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
160         synchronized (sLock) {
161             try {
162                 IStatsManagerService service = getIStatsManagerServiceLocked();
163                 // can throw IllegalArgumentException
164                 service.addConfiguration(configKey, config, mContext.getOpPackageName());
165             } catch (RemoteException e) {
166                 Log.e(TAG, "Failed to connect to statsmanager when adding configuration");
167                 throw new StatsUnavailableException("could not connect", e);
168             } catch (SecurityException e) {
169                 throw new StatsUnavailableException(e.getMessage(), e);
170             } catch (IllegalStateException e) {
171                 Log.e(TAG, "Failed to addConfig in statsmanager");
172                 throw new StatsUnavailableException(e.getMessage(), e);
173             }
174         }
175     }
176 
177     // TODO: Temporary for backwards compatibility. Remove.
178     /**
179      * @deprecated Use {@link #addConfig(long, byte[])}
180      */
181     @Deprecated
182     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
addConfiguration(long configKey, byte[] config)183     public boolean addConfiguration(long configKey, byte[] config) {
184         try {
185             addConfig(configKey, config);
186             return true;
187         } catch (StatsUnavailableException | IllegalArgumentException e) {
188             return false;
189         }
190     }
191 
192     /**
193      * Remove a configuration from logging.
194      *
195      * This call can block on statsd.
196      *
197      * @param configKey Configuration key to remove.
198      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
199      */
200     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
removeConfig(long configKey)201     public void removeConfig(long configKey) throws StatsUnavailableException {
202         synchronized (sLock) {
203             try {
204                 IStatsManagerService service = getIStatsManagerServiceLocked();
205                 service.removeConfiguration(configKey, mContext.getOpPackageName());
206             } catch (RemoteException e) {
207                 Log.e(TAG, "Failed to connect to statsmanager when removing configuration");
208                 throw new StatsUnavailableException("could not connect", e);
209             } catch (SecurityException e) {
210                 throw new StatsUnavailableException(e.getMessage(), e);
211             } catch (IllegalStateException e) {
212                 Log.e(TAG, "Failed to removeConfig in statsmanager");
213                 throw new StatsUnavailableException(e.getMessage(), e);
214             }
215         }
216     }
217 
218     // TODO: Temporary for backwards compatibility. Remove.
219     /**
220      * @deprecated Use {@link #removeConfig(long)}
221      */
222     @Deprecated
223     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
removeConfiguration(long configKey)224     public boolean removeConfiguration(long configKey) {
225         try {
226             removeConfig(configKey);
227             return true;
228         } catch (StatsUnavailableException e) {
229             return false;
230         }
231     }
232 
233     /**
234      * Set the PendingIntent to be used when broadcasting subscriber information to the given
235      * subscriberId within the given config.
236      * <p>
237      * Suppose that the calling uid has added a config with key configKey, and that in this config
238      * it is specified that when a particular anomaly is detected, a broadcast should be sent to
239      * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
240      * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
241      * when the anomaly is detected.
242      * <p>
243      * When statsd sends the broadcast, the PendingIntent will used to send an intent with
244      * information of
245      * {@link #EXTRA_STATS_CONFIG_UID},
246      * {@link #EXTRA_STATS_CONFIG_KEY},
247      * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
248      * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID},
249      * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and
250      * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
251      * <p>
252      * This function can only be called by the owner (uid) of the config. It must be called each
253      * time statsd starts. The config must have been added first (via {@link #addConfig}).
254      * This call can block on statsd.
255      *
256      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
257      *                      associated with the given subscriberId. May be null, in which case
258      *                      it undoes any previous setting of this subscriberId.
259      * @param configKey     The integer naming the config to which this subscriber is attached.
260      * @param subscriberId  ID of the subscriber, as used in the config.
261      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
262      */
263     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setBroadcastSubscriber( PendingIntent pendingIntent, long configKey, long subscriberId)264     public void setBroadcastSubscriber(
265             PendingIntent pendingIntent, long configKey, long subscriberId)
266             throws StatsUnavailableException {
267         synchronized (sLock) {
268             try {
269                 IStatsManagerService service = getIStatsManagerServiceLocked();
270                 if (pendingIntent != null) {
271                     service.setBroadcastSubscriber(configKey, subscriberId, pendingIntent,
272                             mContext.getOpPackageName());
273                 } else {
274                     service.unsetBroadcastSubscriber(configKey, subscriberId,
275                             mContext.getOpPackageName());
276                 }
277             } catch (RemoteException e) {
278                 Log.e(TAG, "Failed to connect to statsmanager when adding broadcast subscriber",
279                         e);
280                 throw new StatsUnavailableException("could not connect", e);
281             } catch (SecurityException e) {
282                 throw new StatsUnavailableException(e.getMessage(), e);
283             }
284         }
285     }
286 
287     // TODO: Temporary for backwards compatibility. Remove.
288     /**
289      * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)}
290      */
291     @Deprecated
292     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setBroadcastSubscriber( long configKey, long subscriberId, PendingIntent pendingIntent)293     public boolean setBroadcastSubscriber(
294             long configKey, long subscriberId, PendingIntent pendingIntent) {
295         try {
296             setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
297             return true;
298         } catch (StatsUnavailableException e) {
299             return false;
300         }
301     }
302 
303     /**
304      * Registers the operation that is called to retrieve the metrics data. This must be called
305      * each time statsd starts. The config must have been added first (via {@link #addConfig},
306      * although addConfig could have been called on a previous boot). This operation allows
307      * statsd to send metrics data whenever statsd determines that the metrics in memory are
308      * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
309      * the data, which also deletes the retrieved metrics from statsd's memory.
310      * This call can block on statsd.
311      *
312      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
313      *                      associated with the given subscriberId. May be null, in which case
314      *                      it removes any associated pending intent with this configKey.
315      * @param configKey     The integer naming the config to which this operation is attached.
316      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
317      */
318     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setFetchReportsOperation(PendingIntent pendingIntent, long configKey)319     public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
320             throws StatsUnavailableException {
321         synchronized (sLock) {
322             try {
323                 IStatsManagerService service = getIStatsManagerServiceLocked();
324                 if (pendingIntent == null) {
325                     service.removeDataFetchOperation(configKey, mContext.getOpPackageName());
326                 } else {
327                     service.setDataFetchOperation(configKey, pendingIntent,
328                             mContext.getOpPackageName());
329                 }
330 
331             } catch (RemoteException e) {
332                 Log.e(TAG, "Failed to connect to statsmanager when registering data listener.");
333                 throw new StatsUnavailableException("could not connect", e);
334             } catch (SecurityException e) {
335                 throw new StatsUnavailableException(e.getMessage(), e);
336             }
337         }
338     }
339 
340     /**
341      * Registers the operation that is called whenever there is a change in which configs are
342      * active. This must be called each time statsd starts. This operation allows
343      * statsd to inform clients that they should pull data of the configs that are currently
344      * active. The activeConfigsChangedOperation should set periodic alarms to pull data of configs
345      * that are active and stop pulling data of configs that are no longer active.
346      * This call can block on statsd.
347      *
348      * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
349      *                      associated with the given subscriberId. May be null, in which case
350      *                      it removes any associated pending intent for this client.
351      * @return A list of configs that are currently active for this client. If the pendingIntent is
352      *         null, this will be an empty list.
353      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
354      */
355     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setActiveConfigsChangedOperation(@ullable PendingIntent pendingIntent)356     public @NonNull long[] setActiveConfigsChangedOperation(@Nullable PendingIntent pendingIntent)
357             throws StatsUnavailableException {
358         synchronized (sLock) {
359             try {
360                 IStatsManagerService service = getIStatsManagerServiceLocked();
361                 if (pendingIntent == null) {
362                     service.removeActiveConfigsChangedOperation(mContext.getOpPackageName());
363                     return new long[0];
364                 } else {
365                     return service.setActiveConfigsChangedOperation(pendingIntent,
366                             mContext.getOpPackageName());
367                 }
368 
369             } catch (RemoteException e) {
370                 Log.e(TAG, "Failed to connect to statsmanager "
371                         + "when registering active configs listener.");
372                 throw new StatsUnavailableException("could not connect", e);
373             } catch (SecurityException e) {
374                 throw new StatsUnavailableException(e.getMessage(), e);
375             }
376         }
377     }
378 
379     /**
380      * Registers the operation that is called whenever there is a change in the restricted metrics
381      * for a specified config that are present for this client. This operation allows statsd to
382      * inform the client about the current restricted metric ids available to be queried for the
383      * specified config. This call can block on statsd.
384      *
385      * If there is no config in statsd that matches the provided config package and key, an empty
386      * list is returned. The pending intent will be tracked, and the operation will be called
387      * whenever a matching config is added.
388      *
389      * @param configKey The configKey passed by the package that added the config in
390      *                  StatsManager#addConfig
391      * @param configPackage The package that added the config in StatsManager#addConfig
392      * @param pendingIntent the PendingIntent to use when broadcasting info to caller.
393      *                      May be null, in which case it removes any associated pending intent
394      *                      for this client.
395      * @return A list of metric ids identifying the restricted metrics that are currently available
396      *         to be queried for the specified config.
397      *         If the pendingIntent is null, this will be an empty list.
398      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
399      */
400     @RequiresPermission(READ_RESTRICTED_STATS)
401     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
setRestrictedMetricsChangedOperation(long configKey, @NonNull String configPackage, @Nullable PendingIntent pendingIntent)402     public @NonNull long[] setRestrictedMetricsChangedOperation(long configKey,
403             @NonNull String configPackage,
404             @Nullable PendingIntent pendingIntent)
405             throws StatsUnavailableException {
406         synchronized (sLock) {
407             try {
408                 IStatsManagerService service = getIStatsManagerServiceLocked();
409                 if (pendingIntent == null) {
410                     service.removeRestrictedMetricsChangedOperation(configKey, configPackage);
411                     return new long[0];
412                 } else {
413                     return service.setRestrictedMetricsChangedOperation(pendingIntent,
414                             configKey, configPackage);
415                 }
416 
417             } catch (RemoteException e) {
418                 Log.e(TAG, "Failed to connect to statsmanager "
419                         + "when registering restricted metrics  listener.");
420                 throw new StatsUnavailableException("could not connect", e);
421             } catch (SecurityException e) {
422                 throw new StatsUnavailableException(e.getMessage(), e);
423             }
424         }
425     }
426 
427     /**
428      * Queries the underlying service based on query received and populates the OutcomeReceiver via
429      * callback. This call is blocking on statsd being available, but is otherwise nonblocking.
430      * i.e. the call can return before the query processing is done.
431      * <p>
432      * Two types of tables are supported: Metric tables and the device information table.
433      * </p>
434      * <p>
435      * The device information table is named device_info and contains the following columns:
436      * sdkVersion, model, product, hardware, device, osBuild, fingerprint, brand, manufacturer, and
437      * board. These columns correspond to {@link Build.VERSION.SDK_INT}, {@link Build.MODEL},
438      * {@link Build.PRODUCT}, {@link Build.HARDWARE}, {@link Build.DEVICE}, {@link Build.ID},
439      * {@link Build.FINGERPRINT}, {@link Build.BRAND}, {@link Build.MANUFACTURER},
440      * {@link Build.BOARD} respectively.
441      * </p>
442      * <p>
443      * The metric tables are named metric_METRIC_ID where METRIC_ID is the metric id that is part
444      * of the wire encoded config passed to {@link #addConfig(long, byte[])}. If the metric id is
445      * negative, then the '-' character is replaced with 'n' in the table name. Each metric table
446      * contains the 3 columns followed by n columns of the following form: atomId,
447      * elapsedTimestampNs, wallTimestampNs, field_1, field_2, field_3 ... field_n. These
448      * columns correspond to to the id of the atom from frameworks/proto_logging/stats/atoms.proto,
449      * time when the atom is recorded, and the data fields within each atom.
450      * </p>
451      * @param configKey The configKey passed by the package that added
452      *                        the config being queried in StatsManager#addConfig
453      * @param configPackage The package that added the config being queried in
454      *                        StatsManager#addConfig
455      * @param query the query object encapsulating a sql-string and necessary config to query
456      *              underlying sql-based data store.
457      * @param executor the executor on which outcomeReceiver will be invoked.
458      * @param outcomeReceiver the receiver to be populated with cursor pointing to result data.
459      */
460     @RequiresPermission(READ_RESTRICTED_STATS)
461     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
query(long configKey, @NonNull String configPackage, @NonNull StatsQuery query, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<StatsCursor, StatsQueryException> outcomeReceiver)462     public void query(long configKey, @NonNull String configPackage, @NonNull StatsQuery query,
463             @NonNull @CallbackExecutor Executor executor,
464             @NonNull OutcomeReceiver<StatsCursor, StatsQueryException> outcomeReceiver)
465             throws StatsUnavailableException {
466         if(query.getSqlDialect() != StatsQuery.DIALECT_SQLITE) {
467             executor.execute(() -> {
468                 outcomeReceiver.onError(new StatsQueryException("Unsupported Sql Dialect"));
469             });
470             return;
471         }
472 
473         StatsQueryCallbackInternal callbackInternal =
474                 new StatsQueryCallbackInternal(outcomeReceiver, executor);
475         synchronized (sLock) {
476             try {
477                 IStatsManagerService service = getIStatsManagerServiceLocked();
478                 service.querySql(query.getRawSql(), query.getMinSqlClientVersion(),
479                         query.getPolicyConfig(), callbackInternal, configKey,
480                         configPackage);
481             } catch (RemoteException | IllegalStateException e) {
482                 throw new StatsUnavailableException("could not connect", e);
483             }
484         }
485     }
486 
487 
488     // TODO: Temporary for backwards compatibility. Remove.
489     /**
490      * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)}
491      */
492     @Deprecated
493     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
setDataFetchOperation(long configKey, PendingIntent pendingIntent)494     public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
495         try {
496             setFetchReportsOperation(pendingIntent, configKey);
497             return true;
498         } catch (StatsUnavailableException e) {
499             return false;
500         }
501     }
502 
503     /**
504      * Request the data collected for the given configKey.
505      * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
506      * This call can block on statsd.
507      *
508      * @param configKey Configuration key to retrieve data from.
509      * @return Serialized ConfigMetricsReportList proto.
510      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
511      */
512     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getReports(long configKey)513     public byte[] getReports(long configKey) throws StatsUnavailableException {
514         synchronized (sLock) {
515             try {
516                 IStatsManagerService service = getIStatsManagerServiceLocked();
517                 return service.getData(configKey, mContext.getOpPackageName());
518             } catch (RemoteException e) {
519                 Log.e(TAG, "Failed to connect to statsmanager when getting data");
520                 throw new StatsUnavailableException("could not connect", e);
521             } catch (SecurityException e) {
522                 throw new StatsUnavailableException(e.getMessage(), e);
523             } catch (IllegalStateException e) {
524                 Log.e(TAG, "Failed to getReports in statsmanager");
525                 throw new StatsUnavailableException(e.getMessage(), e);
526             }
527         }
528     }
529 
530     // TODO: Temporary for backwards compatibility. Remove.
531     /**
532      * @deprecated Use {@link #getReports(long)}
533      */
534     @Deprecated
535     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getData(long configKey)536     public @Nullable byte[] getData(long configKey) {
537         try {
538             return getReports(configKey);
539         } catch (StatsUnavailableException e) {
540             return null;
541         }
542     }
543 
544     /**
545      * Clients can request metadata for statsd. Will contain stats across all configurations but not
546      * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
547      * This getter is not destructive and will not reset any metrics/counters.
548      * This call can block on statsd.
549      *
550      * @return Serialized StatsdStatsReport proto.
551      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
552      */
553     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getStatsMetadata()554     public byte[] getStatsMetadata() throws StatsUnavailableException {
555         synchronized (sLock) {
556             try {
557                 IStatsManagerService service = getIStatsManagerServiceLocked();
558                 return service.getMetadata(mContext.getOpPackageName());
559             } catch (RemoteException e) {
560                 Log.e(TAG, "Failed to connect to statsmanager when getting metadata");
561                 throw new StatsUnavailableException("could not connect", e);
562             } catch (SecurityException e) {
563                 throw new StatsUnavailableException(e.getMessage(), e);
564             } catch (IllegalStateException e) {
565                 Log.e(TAG, "Failed to getStatsMetadata in statsmanager");
566                 throw new StatsUnavailableException(e.getMessage(), e);
567             }
568         }
569     }
570 
571     // TODO: Temporary for backwards compatibility. Remove.
572     /**
573      * @deprecated Use {@link #getStatsMetadata()}
574      */
575     @Deprecated
576     @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
getMetadata()577     public @Nullable byte[] getMetadata() {
578         try {
579             return getStatsMetadata();
580         } catch (StatsUnavailableException e) {
581             return null;
582         }
583     }
584 
585     /**
586      * Returns the experiments IDs registered with statsd, or an empty array if there aren't any.
587      *
588      * This call can block on statsd.
589      *
590      * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
591      */
592     @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
getRegisteredExperimentIds()593     public long[] getRegisteredExperimentIds()
594             throws StatsUnavailableException {
595         synchronized (sLock) {
596             try {
597                 IStatsManagerService service = getIStatsManagerServiceLocked();
598                 return service.getRegisteredExperimentIds();
599             } catch (RemoteException e) {
600                 if (DEBUG) {
601                     Log.d(TAG,
602                             "Failed to connect to StatsManagerService when getting "
603                                     + "registered experiment IDs");
604                 }
605                 throw new StatsUnavailableException("could not connect", e);
606             } catch (SecurityException e) {
607               throw new StatsUnavailableException(e.getMessage(), e);
608             } catch (IllegalStateException e) {
609               Log.e(TAG, "Failed to getRegisteredExperimentIds in statsmanager");
610               throw new StatsUnavailableException(e.getMessage(), e);
611             }
612         }
613     }
614 
615     /**
616      * Sets a callback for an atom when that atom is to be pulled. The stats service will
617      * invoke pullData in the callback when the stats service determines that this atom needs to be
618      * pulled. This method should not be called by third-party apps.
619      *
620      * @param atomTag           The tag of the atom for this puller callback.
621      * @param metadata          Optional metadata specifying the timeout, cool down time, and
622      *                          additive fields for mapping isolated to host uids.
623      * @param executor          The executor in which to run the callback.
624      * @param callback          The callback to be invoked when the stats service pulls the atom.
625      *
626      */
627     @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata, @NonNull @CallbackExecutor Executor executor, @NonNull StatsPullAtomCallback callback)628     public void setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata,
629             @NonNull @CallbackExecutor Executor executor,
630             @NonNull StatsPullAtomCallback callback) {
631         long coolDownMillis =
632                 metadata == null ? DEFAULT_COOL_DOWN_MILLIS : metadata.mCoolDownMillis;
633         long timeoutMillis = metadata == null ? DEFAULT_TIMEOUT_MILLIS : metadata.mTimeoutMillis;
634         int[] additiveFields = metadata == null ? new int[0] : metadata.mAdditiveFields;
635         if (additiveFields == null) {
636             additiveFields = new int[0];
637         }
638 
639         synchronized (sLock) {
640             try {
641                 IStatsManagerService service = getIStatsManagerServiceLocked();
642                 PullAtomCallbackInternal rec =
643                     new PullAtomCallbackInternal(atomTag, callback, executor);
644                 service.registerPullAtomCallback(
645                         atomTag, coolDownMillis, timeoutMillis, additiveFields, rec);
646             } catch (RemoteException e) {
647                 throw new RuntimeException("Unable to register pull callback", e);
648             }
649         }
650     }
651 
652     /**
653      * Clears a callback for an atom when that atom is to be pulled. Note that any ongoing
654      * pulls will still occur. This method should not be called by third-party apps.
655      *
656      * @param atomTag           The tag of the atom of which to unregister
657      *
658      */
659     @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
clearPullAtomCallback(int atomTag)660     public void clearPullAtomCallback(int atomTag) {
661         synchronized (sLock) {
662             try {
663                 IStatsManagerService service = getIStatsManagerServiceLocked();
664                 service.unregisterPullAtomCallback(atomTag);
665             } catch (RemoteException e) {
666                 throw new RuntimeException("Unable to unregister pull atom callback");
667             }
668         }
669     }
670 
671     private static class PullAtomCallbackInternal extends IPullAtomCallback.Stub {
672         public final int mAtomId;
673         public final StatsPullAtomCallback mCallback;
674         public final Executor mExecutor;
675 
PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor)676         PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor) {
677             mAtomId = atomId;
678             mCallback = callback;
679             mExecutor = executor;
680         }
681 
682         @Override
onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver)683         public void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver) {
684             final long token = Binder.clearCallingIdentity();
685             try {
686                 mExecutor.execute(() -> {
687                     List<StatsEvent> data = new ArrayList<>();
688                     int successInt = mCallback.onPullAtom(atomTag, data);
689                     boolean success = successInt == PULL_SUCCESS;
690                     StatsEventParcel[] parcels = new StatsEventParcel[data.size()];
691                     for (int i = 0; i < data.size(); i++) {
692                         parcels[i] = new StatsEventParcel();
693                         parcels[i].buffer = data.get(i).getBytes();
694                     }
695                     try {
696                         resultReceiver.pullFinished(atomTag, success, parcels);
697                     } catch (RemoteException e) {
698                         Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
699                                 + " due to TransactionTooLarge. Calling pullFinish with no data");
700                         StatsEventParcel[] emptyData = new StatsEventParcel[0];
701                         try {
702                             resultReceiver.pullFinished(atomTag, /*success=*/false, emptyData);
703                         } catch (RemoteException nestedException) {
704                             Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
705                                     + " with empty payload");
706                         }
707                     }
708                 });
709             } finally {
710                 Binder.restoreCallingIdentity(token);
711             }
712         }
713     }
714 
715     /**
716      * Metadata required for registering a StatsPullAtomCallback.
717      * All fields are optional, and defaults will be used for fields that are unspecified.
718      *
719      */
720     public static class PullAtomMetadata {
721         private final long mCoolDownMillis;
722         private final long mTimeoutMillis;
723         private final int[] mAdditiveFields;
724 
725         // Private Constructor for builder
PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields)726         private PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields) {
727             mCoolDownMillis = coolDownMillis;
728             mTimeoutMillis = timeoutMillis;
729             mAdditiveFields = additiveFields;
730         }
731 
732         /**
733          *  Builder for PullAtomMetadata.
734          */
735         public static class Builder {
736             private long mCoolDownMillis;
737             private long mTimeoutMillis;
738             private int[] mAdditiveFields;
739 
740             /**
741              * Returns a new PullAtomMetadata.Builder object for constructing PullAtomMetadata for
742              * StatsManager#registerPullAtomCallback
743              */
Builder()744             public Builder() {
745                 mCoolDownMillis = DEFAULT_COOL_DOWN_MILLIS;
746                 mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
747                 mAdditiveFields = null;
748             }
749 
750             /**
751              * Set the cool down time of the pull in milliseconds. If two successive pulls are
752              * issued within the cool down, a cached version of the first pull will be used for the
753              * second pull. The minimum allowed cool down is 1 second.
754              */
755             @NonNull
setCoolDownMillis(long coolDownMillis)756             public Builder setCoolDownMillis(long coolDownMillis) {
757                 mCoolDownMillis = coolDownMillis;
758                 return this;
759             }
760 
761             /**
762              * Set the maximum time the pull can take in milliseconds. The maximum allowed timeout
763              * is 10 seconds.
764              */
765             @NonNull
setTimeoutMillis(long timeoutMillis)766             public Builder setTimeoutMillis(long timeoutMillis) {
767                 mTimeoutMillis = timeoutMillis;
768                 return this;
769             }
770 
771             /**
772              * Set the additive fields of this pulled atom.
773              *
774              * This is only applicable for atoms which have a uid field. When tasks are run in
775              * isolated processes, the data will be attributed to the host uid. Additive fields
776              * will be combined when the non-additive fields are the same.
777              */
778             @NonNull
setAdditiveFields(@onNull int[] additiveFields)779             public Builder setAdditiveFields(@NonNull int[] additiveFields) {
780                 mAdditiveFields = additiveFields;
781                 return this;
782             }
783 
784             /**
785              * Builds and returns a PullAtomMetadata object with the values set in the builder and
786              * defaults for unset fields.
787              */
788             @NonNull
build()789             public PullAtomMetadata build() {
790                 return new PullAtomMetadata(mCoolDownMillis, mTimeoutMillis, mAdditiveFields);
791             }
792         }
793 
794         /**
795          * Return the cool down time of this pull in milliseconds.
796          */
getCoolDownMillis()797         public long getCoolDownMillis() {
798             return mCoolDownMillis;
799         }
800 
801         /**
802          * Return the maximum amount of time this pull can take in milliseconds.
803          */
getTimeoutMillis()804         public long getTimeoutMillis() {
805             return mTimeoutMillis;
806         }
807 
808         /**
809          * Return the additive fields of this pulled atom.
810          *
811          * This is only applicable for atoms that have a uid field. When tasks are run in
812          * isolated processes, the data will be attributed to the host uid. Additive fields
813          * will be combined when the non-additive fields are the same.
814          */
815         @Nullable
getAdditiveFields()816         public int[] getAdditiveFields() {
817             return mAdditiveFields;
818         }
819     }
820 
821     /**
822      * Callback interface for pulling atoms requested by the stats service.
823      *
824      */
825     public interface StatsPullAtomCallback {
826         /**
827          * Pull data for the specified atom tag, filling in the provided list of StatsEvent data.
828          * @return {@link #PULL_SUCCESS} if the pull was successful, or {@link #PULL_SKIP} if not.
829          */
onPullAtom(int atomTag, @NonNull List<StatsEvent> data)830         int onPullAtom(int atomTag, @NonNull List<StatsEvent> data);
831     }
832 
833     @GuardedBy("sLock")
getIStatsManagerServiceLocked()834     private IStatsManagerService getIStatsManagerServiceLocked() {
835         if (mStatsManagerService != null) {
836             return mStatsManagerService;
837         }
838         mStatsManagerService = IStatsManagerService.Stub.asInterface(
839                 StatsFrameworkInitializer
840                 .getStatsServiceManager()
841                 .getStatsManagerServiceRegisterer()
842                 .get());
843         return mStatsManagerService;
844     }
845 
846     private static class StatsQueryCallbackInternal extends IStatsQueryCallback.Stub {
847         OutcomeReceiver<StatsCursor, StatsQueryException> queryCallback;
848         Executor mExecutor;
849 
StatsQueryCallbackInternal(OutcomeReceiver<StatsCursor, StatsQueryException> queryCallback, @NonNull @CallbackExecutor Executor executor)850         StatsQueryCallbackInternal(OutcomeReceiver<StatsCursor, StatsQueryException> queryCallback,
851                 @NonNull @CallbackExecutor Executor executor) {
852             this.queryCallback = queryCallback;
853             this.mExecutor = executor;
854         }
855 
856         @Override
sendResults(String[] queryData, String[] columnNames, int[] columnTypes, int rowCount)857         public void sendResults(String[] queryData, String[] columnNames, int[] columnTypes,
858                 int rowCount) {
859             if (!SdkLevel.isAtLeastU()) {
860                 throw new IllegalStateException(
861                         "StatsManager#query is not available before Android U");
862             }
863             final long token = Binder.clearCallingIdentity();
864             try {
865                 mExecutor.execute(() -> {
866                     StatsCursor cursor = new StatsCursor(queryData, columnNames, columnTypes,
867                             rowCount);
868                     queryCallback.onResult(cursor);
869                 });
870             } finally {
871                 Binder.restoreCallingIdentity(token);
872             }
873         }
874 
875         @Override
sendFailure(String error)876         public void sendFailure(String error) {
877             if (!SdkLevel.isAtLeastU()) {
878                 throw new IllegalStateException(
879                         "StatsManager#query is not available before Android U");
880             }
881             final long token = Binder.clearCallingIdentity();
882             try {
883                 mExecutor.execute(() -> {
884                     queryCallback.onError(new StatsQueryException(error));
885                 });
886             } finally {
887                 Binder.restoreCallingIdentity(token);
888             }
889         }
890     }
891 
892     /**
893      * Exception thrown when communication with the stats service fails (eg if it is not available).
894      * This might be thrown early during boot before the stats service has started or if it crashed.
895      */
896     public static class StatsUnavailableException extends AndroidException {
StatsUnavailableException(String reason)897         public StatsUnavailableException(String reason) {
898             super("Failed to connect to statsd: " + reason);
899         }
900 
StatsUnavailableException(String reason, Throwable e)901         public StatsUnavailableException(String reason, Throwable e) {
902             super("Failed to connect to statsd: " + reason, e);
903         }
904     }
905 
906     /**
907      * Exception thrown when executing a query in statsd fails for any reason. This might be thrown
908      * if the query is malformed or if there is a database error when executing the query.
909      */
910     public static class StatsQueryException extends AndroidException {
StatsQueryException(@onNull String reason)911         public StatsQueryException(@NonNull String reason) {
912             super("Failed to query statsd: " + reason);
913         }
914 
StatsQueryException(@onNull String reason, @NonNull Throwable e)915         public StatsQueryException(@NonNull String reason, @NonNull Throwable e) {
916             super("Failed to query statsd: " + reason, e);
917         }
918     }
919 }
920