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