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