StatsManager.java revision 6b649257377b4ba2dd8a2a02b8dd692a72a2cc1e
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 */ 16package android.app; 17 18import static android.Manifest.permission.DUMP; 19import static android.Manifest.permission.PACKAGE_USAGE_STATS; 20 21import android.annotation.Nullable; 22import android.annotation.RequiresPermission; 23import android.annotation.SystemApi; 24import android.content.Context; 25import android.os.IBinder; 26import android.os.IStatsManager; 27import android.os.RemoteException; 28import android.os.ServiceManager; 29import android.util.AndroidException; 30import android.util.Slog; 31 32/** 33 * API for statsd clients to send configurations and retrieve data. 34 * 35 * @hide 36 */ 37@SystemApi 38public final class StatsManager { 39 private static final String TAG = "StatsManager"; 40 private static final boolean DEBUG = false; 41 42 private final Context mContext; 43 44 private IStatsManager mService; 45 46 /** 47 * Long extra of uid that added the relevant stats config. 48 */ 49 public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID"; 50 /** 51 * Long extra of the relevant stats config's configKey. 52 */ 53 public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY"; 54 /** 55 * Long extra of the relevant statsd_config.proto's Subscription.id. 56 */ 57 public static final String EXTRA_STATS_SUBSCRIPTION_ID = 58 "android.app.extra.STATS_SUBSCRIPTION_ID"; 59 /** 60 * Long extra of the relevant statsd_config.proto's Subscription.rule_id. 61 */ 62 public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID = 63 "android.app.extra.STATS_SUBSCRIPTION_RULE_ID"; 64 /** 65 * List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie. 66 * Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}. 67 */ 68 public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES = 69 "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES"; 70 /** 71 * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value 72 * information. 73 */ 74 public static final String EXTRA_STATS_DIMENSIONS_VALUE = 75 "android.app.extra.STATS_DIMENSIONS_VALUE"; 76 77 /** 78 * Broadcast Action: Statsd has started. 79 * Configurations and PendingIntents can now be sent to it. 80 */ 81 public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED"; 82 83 /** 84 * Constructor for StatsManagerClient. 85 * 86 * @hide 87 */ 88 public StatsManager(Context context) { 89 mContext = context; 90 } 91 92 /** 93 * Adds the given configuration and associates it with the given configKey. If a config with the 94 * given configKey already exists for the caller's uid, it is replaced with the new one. 95 * 96 * @param configKey An arbitrary integer that allows clients to track the configuration. 97 * @param config Wire-encoded StatsdConfig proto that specifies metrics (and all 98 * dependencies eg, conditions and matchers). 99 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 100 * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto 101 */ 102 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 103 public void addConfig(long configKey, byte[] config) throws StatsUnavailableException { 104 synchronized (this) { 105 try { 106 IStatsManager service = getIStatsManagerLocked(); 107 // can throw IllegalArgumentException 108 service.addConfiguration(configKey, config, mContext.getOpPackageName()); 109 } catch (RemoteException e) { 110 Slog.e(TAG, "Failed to connect to statsd when adding configuration"); 111 throw new StatsUnavailableException("could not connect", e); 112 } catch (SecurityException e) { 113 throw new StatsUnavailableException(e.getMessage(), e); 114 } 115 } 116 } 117 118 /** 119 * TODO: Temporary for backwards compatibility. Remove. 120 */ 121 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 122 public boolean addConfiguration(long configKey, byte[] config) { 123 try { 124 addConfig(configKey, config); 125 return true; 126 } catch (StatsUnavailableException | IllegalArgumentException e) { 127 return false; 128 } 129 } 130 131 /** 132 * Remove a configuration from logging. 133 * 134 * @param configKey Configuration key to remove. 135 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 136 */ 137 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 138 public void removeConfig(long configKey) throws StatsUnavailableException { 139 synchronized (this) { 140 try { 141 IStatsManager service = getIStatsManagerLocked(); 142 service.removeConfiguration(configKey, mContext.getOpPackageName()); 143 } catch (RemoteException e) { 144 Slog.e(TAG, "Failed to connect to statsd when removing configuration"); 145 throw new StatsUnavailableException("could not connect", e); 146 } catch (SecurityException e) { 147 throw new StatsUnavailableException(e.getMessage(), e); 148 } 149 } 150 } 151 152 /** 153 * TODO: Temporary for backwards compatibility. Remove. 154 */ 155 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 156 public boolean removeConfiguration(long configKey) { 157 try { 158 removeConfig(configKey); 159 return true; 160 } catch (StatsUnavailableException e) { 161 return false; 162 } 163 } 164 165 /** 166 * Set the PendingIntent to be used when broadcasting subscriber information to the given 167 * subscriberId within the given config. 168 * <p> 169 * Suppose that the calling uid has added a config with key configKey, and that in this config 170 * it is specified that when a particular anomaly is detected, a broadcast should be sent to 171 * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with 172 * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast 173 * when the anomaly is detected. 174 * <p> 175 * When statsd sends the broadcast, the PendingIntent will used to send an intent with 176 * information of 177 * {@link #EXTRA_STATS_CONFIG_UID}, 178 * {@link #EXTRA_STATS_CONFIG_KEY}, 179 * {@link #EXTRA_STATS_SUBSCRIPTION_ID}, 180 * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID}, 181 * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and 182 * {@link #EXTRA_STATS_DIMENSIONS_VALUE}. 183 * <p> 184 * This function can only be called by the owner (uid) of the config. It must be called each 185 * time statsd starts. The config must have been added first (via {@link #addConfig}). 186 * 187 * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber 188 * associated with the given subscriberId. May be null, in which case 189 * it undoes any previous setting of this subscriberId. 190 * @param configKey The integer naming the config to which this subscriber is attached. 191 * @param subscriberId ID of the subscriber, as used in the config. 192 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 193 */ 194 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 195 public void setBroadcastSubscriber( 196 PendingIntent pendingIntent, long configKey, long subscriberId) 197 throws StatsUnavailableException { 198 synchronized (this) { 199 try { 200 IStatsManager service = getIStatsManagerLocked(); 201 if (pendingIntent != null) { 202 // Extracts IIntentSender from the PendingIntent and turns it into an IBinder. 203 IBinder intentSender = pendingIntent.getTarget().asBinder(); 204 service.setBroadcastSubscriber(configKey, subscriberId, intentSender, 205 mContext.getOpPackageName()); 206 } else { 207 service.unsetBroadcastSubscriber(configKey, subscriberId, 208 mContext.getOpPackageName()); 209 } 210 } catch (RemoteException e) { 211 Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e); 212 throw new StatsUnavailableException("could not connect", e); 213 } catch (SecurityException e) { 214 throw new StatsUnavailableException(e.getMessage(), e); 215 } 216 } 217 } 218 219 /** 220 * TODO: Temporary for backwards compatibility. Remove. 221 */ 222 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 223 public boolean setBroadcastSubscriber( 224 long configKey, long subscriberId, PendingIntent pendingIntent) { 225 try { 226 setBroadcastSubscriber(pendingIntent, configKey, subscriberId); 227 return true; 228 } catch (StatsUnavailableException e) { 229 return false; 230 } 231 } 232 233 /** 234 * Registers the operation that is called to retrieve the metrics data. This must be called 235 * each time statsd starts. The config must have been added first (via {@link #addConfig}, 236 * although addConfig could have been called on a previous boot). This operation allows 237 * statsd to send metrics data whenever statsd determines that the metrics in memory are 238 * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch 239 * the data, which also deletes the retrieved metrics from statsd's memory. 240 * 241 * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber 242 * associated with the given subscriberId. May be null, in which case 243 * it removes any associated pending intent with this configKey. 244 * @param configKey The integer naming the config to which this operation is attached. 245 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 246 */ 247 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 248 public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey) 249 throws StatsUnavailableException { 250 synchronized (this) { 251 try { 252 IStatsManager service = getIStatsManagerLocked(); 253 if (pendingIntent == null) { 254 service.removeDataFetchOperation(configKey, mContext.getOpPackageName()); 255 } else { 256 // Extracts IIntentSender from the PendingIntent and turns it into an IBinder. 257 IBinder intentSender = pendingIntent.getTarget().asBinder(); 258 service.setDataFetchOperation(configKey, intentSender, 259 mContext.getOpPackageName()); 260 } 261 262 } catch (RemoteException e) { 263 Slog.e(TAG, "Failed to connect to statsd when registering data listener."); 264 throw new StatsUnavailableException("could not connect", e); 265 } catch (SecurityException e) { 266 throw new StatsUnavailableException(e.getMessage(), e); 267 } 268 } 269 } 270 271 /** 272 * TODO: Temporary for backwards compatibility. Remove. 273 */ 274 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 275 public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) { 276 try { 277 setFetchReportsOperation(pendingIntent, configKey); 278 return true; 279 } catch (StatsUnavailableException e) { 280 return false; 281 } 282 } 283 284 /** 285 * Request the data collected for the given configKey. 286 * This getter is destructive - it also clears the retrieved metrics from statsd's memory. 287 * 288 * @param configKey Configuration key to retrieve data from. 289 * @return Serialized ConfigMetricsReportList proto. 290 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 291 */ 292 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 293 public byte[] getReports(long configKey) throws StatsUnavailableException { 294 synchronized (this) { 295 try { 296 IStatsManager service = getIStatsManagerLocked(); 297 return service.getData(configKey, mContext.getOpPackageName()); 298 } catch (RemoteException e) { 299 Slog.e(TAG, "Failed to connect to statsd when getting data"); 300 throw new StatsUnavailableException("could not connect", e); 301 } catch (SecurityException e) { 302 throw new StatsUnavailableException(e.getMessage(), e); 303 } 304 } 305 } 306 307 /** 308 * TODO: Temporary for backwards compatibility. Remove. 309 */ 310 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 311 public @Nullable byte[] getData(long configKey) { 312 try { 313 return getReports(configKey); 314 } catch (StatsUnavailableException e) { 315 return null; 316 } 317 } 318 319 /** 320 * Clients can request metadata for statsd. Will contain stats across all configurations but not 321 * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}. 322 * This getter is not destructive and will not reset any metrics/counters. 323 * 324 * @return Serialized StatsdStatsReport proto. 325 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 326 */ 327 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 328 public byte[] getStatsMetadata() throws StatsUnavailableException { 329 synchronized (this) { 330 try { 331 IStatsManager service = getIStatsManagerLocked(); 332 return service.getMetadata(mContext.getOpPackageName()); 333 } catch (RemoteException e) { 334 Slog.e(TAG, "Failed to connect to statsd when getting metadata"); 335 throw new StatsUnavailableException("could not connect", e); 336 } catch (SecurityException e) { 337 throw new StatsUnavailableException(e.getMessage(), e); 338 } 339 } 340 } 341 342 /** 343 * Clients can request metadata for statsd. Will contain stats across all configurations but not 344 * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}. 345 * This getter is not destructive and will not reset any metrics/counters. 346 * 347 * @return Serialized StatsdStatsReport proto. Returns null on failure (eg, if statsd crashed). 348 */ 349 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) 350 public @Nullable byte[] getMetadata() { 351 try { 352 return getStatsMetadata(); 353 } catch (StatsUnavailableException e) { 354 return null; 355 } 356 } 357 358 private class StatsdDeathRecipient implements IBinder.DeathRecipient { 359 @Override 360 public void binderDied() { 361 synchronized (this) { 362 mService = null; 363 } 364 } 365 } 366 367 private IStatsManager getIStatsManagerLocked() throws StatsUnavailableException { 368 if (mService != null) { 369 return mService; 370 } 371 mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats")); 372 if (mService == null) { 373 throw new StatsUnavailableException("could not be found"); 374 } 375 try { 376 mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0); 377 } catch (RemoteException e) { 378 throw new StatsUnavailableException("could not connect when linkToDeath", e); 379 } 380 return mService; 381 } 382 383 /** 384 * Exception thrown when communication with the stats service fails (eg if it is not available). 385 * This might be thrown early during boot before the stats service has started or if it crashed. 386 */ 387 public static class StatsUnavailableException extends AndroidException { 388 public StatsUnavailableException(String reason) { 389 super("Failed to connect to statsd: " + reason); 390 } 391 392 public StatsUnavailableException(String reason, Throwable e) { 393 super("Failed to connect to statsd: " + reason, e); 394 } 395 } 396} 397