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