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 */ StatsManager(Context context)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 }) addConfig(long configKey, byte[] config)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 }) addConfiguration(long configKey, byte[] config)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 }) removeConfig(long configKey)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 }) removeConfiguration(long configKey)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 }) setBroadcastSubscriber( PendingIntent pendingIntent, long configKey, long subscriberId)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 }) setBroadcastSubscriber( long configKey, long subscriberId, PendingIntent pendingIntent)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 }) setFetchReportsOperation(PendingIntent pendingIntent, long configKey)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 }) setActiveConfigsChangedOperation(@ullable PendingIntent pendingIntent)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 }) setDataFetchOperation(long configKey, PendingIntent pendingIntent)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 }) getReports(long configKey)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 }) getData(long configKey)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 }) getStatsMetadata()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 }) getMetadata()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}) getRegisteredExperimentIds()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 }) setPullerCallback(int atomTag, IStatsPullerCallback callback)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 binderDied()477 public void binderDied() { 478 synchronized (this) { 479 mService = null; 480 } 481 } 482 } 483 getIStatsManagerLocked()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 { StatsUnavailableException(String reason)505 public StatsUnavailableException(String reason) { 506 super("Failed to connect to statsd: " + reason); 507 } 508 StatsUnavailableException(String reason, Throwable e)509 public StatsUnavailableException(String reason, Throwable e) { 510 super("Failed to connect to statsd: " + reason, e); 511 } 512 } 513 } 514