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