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