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