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