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    // TODO: Temporary for backwards compatibility. Remove.
119    /**
120     * @deprecated Use {@link #addConfig(long, byte[])}
121     */
122    @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
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 })
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 })
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 })
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 })
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 })
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 })
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 })
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 })
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 })
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 })
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
362        public void binderDied() {
363            synchronized (this) {
364                mService = null;
365            }
366        }
367    }
368
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 {
390        public StatsUnavailableException(String reason) {
391            super("Failed to connect to statsd: " + reason);
392        }
393
394        public StatsUnavailableException(String reason, Throwable e) {
395            super("Failed to connect to statsd: " + reason, e);
396        }
397    }
398}
399