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.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SystemApi; 24 import android.content.Context; 25 import android.os.IBinder; 26 import android.os.IStatsManager; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.util.AndroidException; 30 import android.util.Slog; 31 32 /** 33 * API for statsd clients to send configurations and retrieve data. 34 * 35 * @hide 36 */ 37 @SystemApi 38 public 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 */ StatsManager(Context context)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 }) addConfig(long configKey, byte[] config)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 // TODO: Temporary for backwards compatibility. Remove. 119 /** 120 * @deprecated Use {@link #addConfig(long, byte[])} 121 */ 122 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) addConfiguration(long configKey, byte[] config)123 public boolean addConfiguration(long configKey, byte[] config) { 124 try { 125 addConfig(configKey, config); 126 return true; 127 } catch (StatsUnavailableException | IllegalArgumentException e) { 128 return false; 129 } 130 } 131 132 /** 133 * Remove a configuration from logging. 134 * 135 * @param configKey Configuration key to remove. 136 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 137 */ 138 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) removeConfig(long configKey)139 public void removeConfig(long configKey) throws StatsUnavailableException { 140 synchronized (this) { 141 try { 142 IStatsManager service = getIStatsManagerLocked(); 143 service.removeConfiguration(configKey, mContext.getOpPackageName()); 144 } catch (RemoteException e) { 145 Slog.e(TAG, "Failed to connect to statsd when removing configuration"); 146 throw new StatsUnavailableException("could not connect", e); 147 } catch (SecurityException e) { 148 throw new StatsUnavailableException(e.getMessage(), e); 149 } 150 } 151 } 152 153 // TODO: Temporary for backwards compatibility. Remove. 154 /** 155 * @deprecated Use {@link #removeConfig(long)} 156 */ 157 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) removeConfiguration(long configKey)158 public boolean removeConfiguration(long configKey) { 159 try { 160 removeConfig(configKey); 161 return true; 162 } catch (StatsUnavailableException e) { 163 return false; 164 } 165 } 166 167 /** 168 * Set the PendingIntent to be used when broadcasting subscriber information to the given 169 * subscriberId within the given config. 170 * <p> 171 * Suppose that the calling uid has added a config with key configKey, and that in this config 172 * it is specified that when a particular anomaly is detected, a broadcast should be sent to 173 * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with 174 * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast 175 * when the anomaly is detected. 176 * <p> 177 * When statsd sends the broadcast, the PendingIntent will used to send an intent with 178 * information of 179 * {@link #EXTRA_STATS_CONFIG_UID}, 180 * {@link #EXTRA_STATS_CONFIG_KEY}, 181 * {@link #EXTRA_STATS_SUBSCRIPTION_ID}, 182 * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID}, 183 * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and 184 * {@link #EXTRA_STATS_DIMENSIONS_VALUE}. 185 * <p> 186 * This function can only be called by the owner (uid) of the config. It must be called each 187 * time statsd starts. The config must have been added first (via {@link #addConfig}). 188 * 189 * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber 190 * associated with the given subscriberId. May be null, in which case 191 * it undoes any previous setting of this subscriberId. 192 * @param configKey The integer naming the config to which this subscriber is attached. 193 * @param subscriberId ID of the subscriber, as used in the config. 194 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 195 */ 196 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) setBroadcastSubscriber( PendingIntent pendingIntent, long configKey, long subscriberId)197 public void setBroadcastSubscriber( 198 PendingIntent pendingIntent, long configKey, long subscriberId) 199 throws StatsUnavailableException { 200 synchronized (this) { 201 try { 202 IStatsManager service = getIStatsManagerLocked(); 203 if (pendingIntent != null) { 204 // Extracts IIntentSender from the PendingIntent and turns it into an IBinder. 205 IBinder intentSender = pendingIntent.getTarget().asBinder(); 206 service.setBroadcastSubscriber(configKey, subscriberId, intentSender, 207 mContext.getOpPackageName()); 208 } else { 209 service.unsetBroadcastSubscriber(configKey, subscriberId, 210 mContext.getOpPackageName()); 211 } 212 } catch (RemoteException e) { 213 Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e); 214 throw new StatsUnavailableException("could not connect", e); 215 } catch (SecurityException e) { 216 throw new StatsUnavailableException(e.getMessage(), e); 217 } 218 } 219 } 220 221 // TODO: Temporary for backwards compatibility. Remove. 222 /** 223 * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)} 224 */ 225 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) setBroadcastSubscriber( long configKey, long subscriberId, PendingIntent pendingIntent)226 public boolean setBroadcastSubscriber( 227 long configKey, long subscriberId, PendingIntent pendingIntent) { 228 try { 229 setBroadcastSubscriber(pendingIntent, configKey, subscriberId); 230 return true; 231 } catch (StatsUnavailableException e) { 232 return false; 233 } 234 } 235 236 /** 237 * Registers the operation that is called to retrieve the metrics data. This must be called 238 * each time statsd starts. The config must have been added first (via {@link #addConfig}, 239 * although addConfig could have been called on a previous boot). This operation allows 240 * statsd to send metrics data whenever statsd determines that the metrics in memory are 241 * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch 242 * the data, which also deletes the retrieved metrics from statsd's memory. 243 * 244 * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber 245 * associated with the given subscriberId. May be null, in which case 246 * it removes any associated pending intent with this configKey. 247 * @param configKey The integer naming the config to which this operation is attached. 248 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 249 */ 250 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) setFetchReportsOperation(PendingIntent pendingIntent, long configKey)251 public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey) 252 throws StatsUnavailableException { 253 synchronized (this) { 254 try { 255 IStatsManager service = getIStatsManagerLocked(); 256 if (pendingIntent == null) { 257 service.removeDataFetchOperation(configKey, mContext.getOpPackageName()); 258 } else { 259 // Extracts IIntentSender from the PendingIntent and turns it into an IBinder. 260 IBinder intentSender = pendingIntent.getTarget().asBinder(); 261 service.setDataFetchOperation(configKey, intentSender, 262 mContext.getOpPackageName()); 263 } 264 265 } catch (RemoteException e) { 266 Slog.e(TAG, "Failed to connect to statsd when registering data listener."); 267 throw new StatsUnavailableException("could not connect", e); 268 } catch (SecurityException e) { 269 throw new StatsUnavailableException(e.getMessage(), e); 270 } 271 } 272 } 273 274 // TODO: Temporary for backwards compatibility. Remove. 275 /** 276 * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)} 277 */ 278 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) setDataFetchOperation(long configKey, PendingIntent pendingIntent)279 public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) { 280 try { 281 setFetchReportsOperation(pendingIntent, configKey); 282 return true; 283 } catch (StatsUnavailableException e) { 284 return false; 285 } 286 } 287 288 /** 289 * Request the data collected for the given configKey. 290 * This getter is destructive - it also clears the retrieved metrics from statsd's memory. 291 * 292 * @param configKey Configuration key to retrieve data from. 293 * @return Serialized ConfigMetricsReportList proto. 294 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 295 */ 296 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) getReports(long configKey)297 public byte[] getReports(long configKey) throws StatsUnavailableException { 298 synchronized (this) { 299 try { 300 IStatsManager service = getIStatsManagerLocked(); 301 return service.getData(configKey, mContext.getOpPackageName()); 302 } catch (RemoteException e) { 303 Slog.e(TAG, "Failed to connect to statsd when getting data"); 304 throw new StatsUnavailableException("could not connect", e); 305 } catch (SecurityException e) { 306 throw new StatsUnavailableException(e.getMessage(), e); 307 } 308 } 309 } 310 311 // TODO: Temporary for backwards compatibility. Remove. 312 /** 313 * @deprecated Use {@link #getReports(long)} 314 */ 315 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) getData(long configKey)316 public @Nullable byte[] getData(long configKey) { 317 try { 318 return getReports(configKey); 319 } catch (StatsUnavailableException e) { 320 return null; 321 } 322 } 323 324 /** 325 * Clients can request metadata for statsd. Will contain stats across all configurations but not 326 * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}. 327 * This getter is not destructive and will not reset any metrics/counters. 328 * 329 * @return Serialized StatsdStatsReport proto. 330 * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service 331 */ 332 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) getStatsMetadata()333 public byte[] getStatsMetadata() throws StatsUnavailableException { 334 synchronized (this) { 335 try { 336 IStatsManager service = getIStatsManagerLocked(); 337 return service.getMetadata(mContext.getOpPackageName()); 338 } catch (RemoteException e) { 339 Slog.e(TAG, "Failed to connect to statsd when getting metadata"); 340 throw new StatsUnavailableException("could not connect", e); 341 } catch (SecurityException e) { 342 throw new StatsUnavailableException(e.getMessage(), e); 343 } 344 } 345 } 346 347 // TODO: Temporary for backwards compatibility. Remove. 348 /** 349 * @deprecated Use {@link #getStatsMetadata()} 350 */ 351 @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) getMetadata()352 public @Nullable byte[] getMetadata() { 353 try { 354 return getStatsMetadata(); 355 } catch (StatsUnavailableException e) { 356 return null; 357 } 358 } 359 360 private class StatsdDeathRecipient implements IBinder.DeathRecipient { 361 @Override binderDied()362 public void binderDied() { 363 synchronized (this) { 364 mService = null; 365 } 366 } 367 } 368 getIStatsManagerLocked()369 private IStatsManager getIStatsManagerLocked() throws StatsUnavailableException { 370 if (mService != null) { 371 return mService; 372 } 373 mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats")); 374 if (mService == null) { 375 throw new StatsUnavailableException("could not be found"); 376 } 377 try { 378 mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0); 379 } catch (RemoteException e) { 380 throw new StatsUnavailableException("could not connect when linkToDeath", e); 381 } 382 return mService; 383 } 384 385 /** 386 * Exception thrown when communication with the stats service fails (eg if it is not available). 387 * This might be thrown early during boot before the stats service has started or if it crashed. 388 */ 389 public static class StatsUnavailableException extends AndroidException { StatsUnavailableException(String reason)390 public StatsUnavailableException(String reason) { 391 super("Failed to connect to statsd: " + reason); 392 } 393 StatsUnavailableException(String reason, Throwable e)394 public StatsUnavailableException(String reason, Throwable e) { 395 super("Failed to connect to statsd: " + reason, e); 396 } 397 } 398 } 399