1 /*
2  * Copyright (C) 2018 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 
17 package com.android.helpers;
18 
19 import android.content.Context;
20 import android.app.StatsManager;
21 import android.app.StatsManager.StatsUnavailableException;
22 import android.os.SystemClock;
23 import android.util.Log;
24 import android.util.StatsLog;
25 import androidx.test.InstrumentationRegistry;
26 
27 import com.android.internal.os.StatsdConfigProto.AtomMatcher;
28 import com.android.internal.os.StatsdConfigProto.EventMetric;
29 import com.android.internal.os.StatsdConfigProto.FieldFilter;
30 import com.android.internal.os.StatsdConfigProto.GaugeMetric;
31 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
32 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
33 import com.android.internal.os.StatsdConfigProto.TimeUnit;
34 import com.android.os.AtomsProto.Atom;
35 import com.android.os.StatsLog.ConfigMetricsReport;
36 import com.android.os.StatsLog.ConfigMetricsReportList;
37 import com.android.os.StatsLog.EventMetricData;
38 import com.android.os.StatsLog.GaugeMetricData;
39 import com.android.os.StatsLog.StatsLogReport;
40 
41 import com.google.protobuf.InvalidProtocolBufferException;
42 
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.UUID;
46 
47 /**
48  * StatsdHelper consist of basic utilities that will be used to setup statsd
49  * config, parse the collected information and remove the statsd config.
50  */
51 public class StatsdHelper {
52     private static final String LOG_TAG = StatsdHelper.class.getSimpleName();
53     private static final long MAX_ATOMS = 2000;
54     private static final long METRIC_DELAY_MS = 3000;
55     private long mConfigId = -1;
56     private StatsManager mStatsManager;
57 
58     /**
59      * Add simple event configurations using a list of atom ids.
60      *
61      * @param atomIdList uniquely identifies the information that we need to track by statsManager.
62      * @return true if the configuration is added successfully other wise false.
63      */
addEventConfig(List<Integer> atomIdList)64     public boolean addEventConfig(List<Integer> atomIdList) {
65         long configId = System.currentTimeMillis();
66         StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
67 
68         for (Integer atomId : atomIdList) {
69             int atomUniqueId = getUniqueId();
70             statsConfigBuilder
71                     .addEventMetric(
72                             EventMetric.newBuilder()
73                                     .setId(getUniqueId())
74                                     .setWhat(atomUniqueId))
75                     .addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId));
76         }
77         try {
78             adoptShellIdentity();
79             getStatsManager().addConfig(configId, statsConfigBuilder.build().toByteArray());
80             dropShellIdentity();
81         } catch (Exception e) {
82             Log.e(LOG_TAG, "Not able to setup the event config.", e);
83             return false;
84         }
85         Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
86         setConfigId(configId);
87         return true;
88     }
89 
90     /**
91      * Build gauge metric config based on trigger events (i.e AppBreadCrumbReported).
92      * Whenever the events are triggered via StatsLog.logEvent() collect the gauge metrics.
93      * It doesn't matter what the log event is. It could be 0 or 1.
94      * In order to capture the usage during the test take the difference of gauge metrics
95      * before and after the test.
96      *
97      * @param atomIdList List of atoms to be collected in gauge metrics.
98      * @return if the config is added successfully otherwise false.
99      */
addGaugeConfig(List<Integer> atomIdList)100     public boolean addGaugeConfig(List<Integer> atomIdList) {
101         long configId = System.currentTimeMillis();
102         StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
103         int appBreadCrumbUniqueId = getUniqueId();
104 
105         // Needed for collecting gauge metric based on trigger events.
106         statsConfigBuilder.addAtomMatcher(getSimpleAtomMatcher(appBreadCrumbUniqueId,
107                 Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
108 
109         for (Integer atomId : atomIdList) {
110             int atomUniqueId = getUniqueId();
111             // Build Gauge metric config.
112             GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
113                     .setId(getUniqueId())
114                     .setWhat(atomUniqueId)
115                     .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
116                     .setMaxNumGaugeAtomsPerBucket(MAX_ATOMS)
117                     .setSamplingType(GaugeMetric.SamplingType.FIRST_N_SAMPLES)
118                     .setTriggerEvent(appBreadCrumbUniqueId)
119                     .setBucket(TimeUnit.CTS);
120 
121             // add the gauge config.
122             statsConfigBuilder.addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId))
123                     .addGaugeMetric(gaugeMetric.build());
124         }
125 
126         try {
127             adoptShellIdentity();
128             getStatsManager().addConfig(configId,
129                     statsConfigBuilder.build().toByteArray());
130             StatsLog.logEvent(0);
131             // Dump the counters before the test started.
132             SystemClock.sleep(METRIC_DELAY_MS);
133             dropShellIdentity();
134         } catch (Exception e) {
135             Log.e(LOG_TAG, "Not able to setup the gauge config.", e);
136             return false;
137         }
138 
139         Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
140         setConfigId(configId);
141         return true;
142     }
143 
144     /**
145      * Create simple atom matcher with the given id and the field id.
146      *
147      * @param id
148      * @param fieldId
149      * @return
150      */
getSimpleAtomMatcher(int id, int fieldId)151     private AtomMatcher.Builder getSimpleAtomMatcher(int id, int fieldId) {
152         return AtomMatcher.newBuilder()
153                 .setId(id)
154                 .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
155                         .setAtomId(fieldId));
156     }
157 
158     /**
159      * List of authorized source that can write the information into statsd.
160      *
161      * @param configId unique id of the configuration tracked by StatsManager.
162      * @return
163      */
getSimpleSources(long configId)164     private static StatsdConfig.Builder getSimpleSources(long configId) {
165         return StatsdConfig.newBuilder().setId(configId)
166                 .addAllowedLogSource("AID_ROOT")
167                 .addAllowedLogSource("AID_SYSTEM")
168                 .addAllowedLogSource("AID_RADIO")
169                 .addAllowedLogSource("AID_BLUETOOTH")
170                 .addAllowedLogSource("AID_GRAPHICS")
171                 .addAllowedLogSource("AID_STATSD")
172                 .addAllowedLogSource("AID_INCIENTD");
173     }
174 
175     /**
176      * Returns the list of EventMetricData tracked under the config.
177      */
getEventMetrics()178     public List<EventMetricData> getEventMetrics() {
179         ConfigMetricsReportList reportList = null;
180         List<EventMetricData> eventData = new ArrayList<>();
181         try {
182             if (getConfigId() != -1) {
183                 adoptShellIdentity();
184                 reportList = ConfigMetricsReportList.parser()
185                         .parseFrom(getStatsManager().getReports(getConfigId()));
186                 dropShellIdentity();
187             }
188         } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
189             Log.e(LOG_TAG, "Retreiving event metrics failed.", se);
190             return eventData;
191         }
192 
193         if (reportList != null) {
194             ConfigMetricsReport configReport = reportList.getReports(0);
195             for (StatsLogReport metric : configReport.getMetricsList()) {
196                 eventData.addAll(metric.getEventMetrics().getDataList());
197             }
198         }
199         Log.i(LOG_TAG, "Number of events: " + eventData.size());
200         return eventData;
201     }
202 
203     /**
204      * Returns the list of GaugeMetric data tracked under the config.
205      */
getGaugeMetrics()206     public List<GaugeMetricData> getGaugeMetrics() {
207         ConfigMetricsReportList reportList = null;
208         List<GaugeMetricData> gaugeData = new ArrayList<>();
209         try {
210             if (getConfigId() != -1) {
211                 adoptShellIdentity();
212                 StatsLog.logEvent(0);
213                 // Dump the the counters after the test completed.
214                 SystemClock.sleep(METRIC_DELAY_MS);
215                 reportList = ConfigMetricsReportList.parser()
216                         .parseFrom(getStatsManager().getReports(getConfigId()));
217                 dropShellIdentity();
218             }
219         } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
220             Log.e(LOG_TAG, "Retreiving gauge metrics failed.", se);
221             return gaugeData;
222         }
223 
224         if (reportList != null) {
225             ConfigMetricsReport configReport = reportList.getReports(0);
226             for (StatsLogReport metric : configReport.getMetricsList()) {
227                 gaugeData.addAll(metric.getGaugeMetrics().getDataList());
228             }
229         }
230         Log.i(LOG_TAG, "Number of Gauge data: " + gaugeData.size());
231         return gaugeData;
232     }
233 
234     /**
235      * Remove the existing config tracked in the statsd.
236      *
237      * @return true if the config is removed successfully otherwise false.
238      */
removeStatsConfig()239     public boolean removeStatsConfig() {
240         Log.i(LOG_TAG, "Removing statsd config-id: " + getConfigId());
241         try {
242             adoptShellIdentity();
243             getStatsManager().removeConfig(getConfigId());
244             dropShellIdentity();
245             Log.i(LOG_TAG, "Successfully removed config-id: " + getConfigId());
246             return true;
247         } catch (StatsUnavailableException e) {
248             Log.e(LOG_TAG, String.format("Not able to remove the config-id: %d due to %s ",
249                     getConfigId(), e.getMessage()));
250             return false;
251         }
252     }
253 
254     /**
255      * StatsManager used to configure, collect and remove the statsd config.
256      *
257      * @return StatsManager
258      */
getStatsManager()259     private StatsManager getStatsManager() {
260         if (mStatsManager == null) {
261             mStatsManager = (StatsManager) InstrumentationRegistry.getTargetContext().
262                     getSystemService(Context.STATS_MANAGER);
263         }
264         return mStatsManager;
265     }
266 
267     /**
268      * Returns the package name for the UID if it is available. Otherwise return null.
269      *
270      * @param uid
271      * @return
272      */
getPackageName(int uid)273     public String getPackageName(int uid) {
274         String pkgName = InstrumentationRegistry.getTargetContext().getPackageManager()
275                 .getNameForUid(uid);
276         // Remove the UID appended at the end of the package name.
277         if (pkgName != null) {
278             String pkgNameSplit[] = pkgName.split(String.format("\\:%d", uid));
279             return pkgNameSplit[0];
280         }
281         return pkgName;
282     }
283 
284     /**
285      * Set the config id tracked in the statsd.
286      */
setConfigId(long configId)287     private void setConfigId(long configId) {
288         mConfigId = configId;
289     }
290 
291     /**
292      * Return the config id tracked in the statsd.
293      */
getConfigId()294     private long getConfigId() {
295         return mConfigId;
296     }
297 
298     /**
299      * Returns the unique id
300      */
getUniqueId()301     private int getUniqueId() {
302         return UUID.randomUUID().hashCode();
303     }
304 
305     /**
306      * Adopts shell permission identity needed to access StatsManager service
307      */
adoptShellIdentity()308     public static void adoptShellIdentity() {
309         InstrumentationRegistry.getInstrumentation().getUiAutomation()
310                 .adoptShellPermissionIdentity();
311     }
312 
313     /**
314      * Drop shell permission identity
315      */
dropShellIdentity()316     public static void dropShellIdentity() {
317         InstrumentationRegistry.getInstrumentation().getUiAutomation()
318                 .dropShellPermissionIdentity();
319     }
320 
321 }
322