1 /*
2  * Copyright (C) 2020 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.internal.telephony.metrics;
18 
19 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
20 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
21 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
22 
23 import static com.android.internal.telephony.TelephonyStatsLog.SIM_SLOT_STATE;
24 import static com.android.internal.telephony.TelephonyStatsLog.SUPPORTED_RADIO_ACCESS_FAMILY;
25 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_RAT_USAGE;
26 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION;
27 
28 import android.annotation.Nullable;
29 import android.app.StatsManager;
30 import android.content.Context;
31 import android.util.StatsEvent;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.telephony.Phone;
35 import com.android.internal.telephony.PhoneFactory;
36 import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
37 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
38 import com.android.internal.util.ConcurrentUtils;
39 import com.android.telephony.Rlog;
40 
41 import java.util.Arrays;
42 import java.util.Comparator;
43 import java.util.List;
44 import java.util.Random;
45 
46 /**
47  * Implements statsd pullers for Telephony.
48  *
49  * <p>This class registers pullers to statsd, which will be called once a day to obtain telephony
50  * statistics that cannot be sent to statsd in real time.
51  */
52 public class MetricsCollector implements StatsManager.StatsPullAtomCallback {
53     private static final String TAG = MetricsCollector.class.getSimpleName();
54 
55     /** Disables various restrictions to ease debugging during development. */
56     private static final boolean DBG = false; // STOPSHIP if true
57 
58     /**
59      * Sets atom pull cool down to 23 hours to help enforcing privacy requirement.
60      *
61      * <p>Applies to certain atoms. The interval of 23 hours leaves some margin for pull operations
62      * that occur once a day.
63      */
64     private static final long MIN_COOLDOWN_MILLIS =
65             DBG ? 10L * SECOND_IN_MILLIS : 23L * HOUR_IN_MILLIS;
66 
67     /**
68      * Buckets with less than these many calls will be dropped.
69      *
70      * <p>Applies to metrics with duration fields. Currently used by voice call RAT usages.
71      */
72     private static final long MIN_CALLS_PER_BUCKET = DBG ? 0L : 5L;
73 
74     /** Bucket size in milliseconds to round call durations into. */
75     private static final long DURATION_BUCKET_MILLIS =
76             DBG ? 2L * SECOND_IN_MILLIS : 5L * MINUTE_IN_MILLIS;
77 
78     private static final StatsManager.PullAtomMetadata POLICY_PULL_DAILY =
79             new StatsManager.PullAtomMetadata.Builder()
80                     .setCoolDownMillis(MIN_COOLDOWN_MILLIS)
81                     .build();
82 
83     private PersistAtomsStorage mStorage;
84     private final StatsManager mStatsManager;
85     private static final Random sRandom = new Random();
86 
MetricsCollector(Context context)87     public MetricsCollector(Context context) {
88         mStorage = new PersistAtomsStorage(context);
89         mStatsManager = (StatsManager) context.getSystemService(Context.STATS_MANAGER);
90         if (mStatsManager != null) {
91             registerAtom(SIM_SLOT_STATE, null);
92             registerAtom(SUPPORTED_RADIO_ACCESS_FAMILY, null);
93             registerAtom(VOICE_CALL_RAT_USAGE, POLICY_PULL_DAILY);
94             registerAtom(VOICE_CALL_SESSION, POLICY_PULL_DAILY);
95             Rlog.d(TAG, "registered");
96         } else {
97             Rlog.e(TAG, "could not get StatsManager, atoms not registered");
98         }
99     }
100 
101     /** Replaces the {@link PersistAtomsStorage} backing the puller. Used during unit tests. */
102     @VisibleForTesting
setPersistAtomsStorage(PersistAtomsStorage storage)103     public void setPersistAtomsStorage(PersistAtomsStorage storage) {
104         mStorage = storage;
105     }
106 
107     /**
108      * {@inheritDoc}
109      *
110      * @return {@link StatsManager#PULL_SUCCESS} with list of atoms (potentially empty) if pull
111      *     succeeded, {@link StatsManager#PULL_SKIP} if pull was too frequent or atom ID is
112      *     unexpected.
113      */
114     @Override
onPullAtom(int atomTag, List<StatsEvent> data)115     public int onPullAtom(int atomTag, List<StatsEvent> data) {
116         switch (atomTag) {
117             case SIM_SLOT_STATE:
118                 return pullSimSlotState(data);
119             case SUPPORTED_RADIO_ACCESS_FAMILY:
120                 return pullSupportedRadioAccessFamily(data);
121             case VOICE_CALL_RAT_USAGE:
122                 return pullVoiceCallRatUsages(data);
123             case VOICE_CALL_SESSION:
124                 return pullVoiceCallSessions(data);
125             default:
126                 Rlog.e(TAG, String.format("unexpected atom ID %d", atomTag));
127                 return StatsManager.PULL_SKIP;
128         }
129     }
130 
131     /** Returns the {@link PersistAtomsStorage} backing the puller. */
getAtomsStorage()132     public PersistAtomsStorage getAtomsStorage() {
133         return mStorage;
134     }
135 
pullSimSlotState(List<StatsEvent> data)136     private static int pullSimSlotState(List<StatsEvent> data) {
137         SimSlotState state;
138         try {
139             state = SimSlotState.getCurrentState();
140         } catch (RuntimeException e) {
141             // UiccController has not been made yet
142             return StatsManager.PULL_SKIP;
143         }
144 
145         StatsEvent e =
146                 StatsEvent.newBuilder()
147                         .setAtomId(SIM_SLOT_STATE)
148                         .writeInt(state.numActiveSlots)
149                         .writeInt(state.numActiveSims)
150                         .writeInt(state.numActiveEsims)
151                         .build();
152         data.add(e);
153         return StatsManager.PULL_SUCCESS;
154     }
155 
pullSupportedRadioAccessFamily(List<StatsEvent> data)156     private static int pullSupportedRadioAccessFamily(List<StatsEvent> data) {
157         long rafSupported = 0L;
158         try {
159             // The bitmask is defined in android.telephony.TelephonyManager.NetworkTypeBitMask
160             for (Phone phone : PhoneFactory.getPhones()) {
161                 rafSupported |= phone.getRadioAccessFamily();
162             }
163         } catch (IllegalStateException e) {
164             // Phones have not been made yet
165             return StatsManager.PULL_SKIP;
166         }
167 
168         StatsEvent e =
169                 StatsEvent.newBuilder()
170                         .setAtomId(SUPPORTED_RADIO_ACCESS_FAMILY)
171                         .writeLong(rafSupported)
172                         .build();
173         data.add(e);
174         return StatsManager.PULL_SUCCESS;
175     }
176 
pullVoiceCallRatUsages(List<StatsEvent> data)177     private int pullVoiceCallRatUsages(List<StatsEvent> data) {
178         RawVoiceCallRatUsage[] usages = mStorage.getVoiceCallRatUsages(MIN_COOLDOWN_MILLIS);
179         if (usages != null) {
180             // sort by carrier/RAT and remove buckets with insufficient number of calls
181             Arrays.stream(usages)
182                     .sorted(
183                             Comparator.comparingLong(
184                                     usage -> ((long) usage.carrierId << 32) | usage.rat))
185                     .filter(usage -> usage.callCount >= MIN_CALLS_PER_BUCKET)
186                     .forEach(usage -> data.add(buildStatsEvent(usage)));
187             Rlog.d(
188                     TAG,
189                     String.format(
190                             "%d out of %d VOICE_CALL_RAT_USAGE pulled",
191                             data.size(), usages.length));
192             return StatsManager.PULL_SUCCESS;
193         } else {
194             Rlog.w(TAG, "VOICE_CALL_RAT_USAGE pull too frequent, skipping");
195             return StatsManager.PULL_SKIP;
196         }
197     }
198 
pullVoiceCallSessions(List<StatsEvent> data)199     private int pullVoiceCallSessions(List<StatsEvent> data) {
200         VoiceCallSession[] calls = mStorage.getVoiceCallSessions(MIN_COOLDOWN_MILLIS);
201         if (calls != null) {
202             // call session list is already shuffled when calls inserted
203             Arrays.stream(calls).forEach(call -> data.add(buildStatsEvent(call)));
204             return StatsManager.PULL_SUCCESS;
205         } else {
206             Rlog.w(TAG, "VOICE_CALL_SESSION pull too frequent, skipping");
207             return StatsManager.PULL_SKIP;
208         }
209     }
210 
211     /** Registers a pulled atom ID {@code atomId} with optional {@code policy} for pulling. */
registerAtom(int atomId, @Nullable StatsManager.PullAtomMetadata policy)212     private void registerAtom(int atomId, @Nullable StatsManager.PullAtomMetadata policy) {
213         mStatsManager.setPullAtomCallback(atomId, policy, ConcurrentUtils.DIRECT_EXECUTOR, this);
214     }
215 
buildStatsEvent(RawVoiceCallRatUsage usage)216     private static StatsEvent buildStatsEvent(RawVoiceCallRatUsage usage) {
217         return StatsEvent.newBuilder()
218                 .setAtomId(VOICE_CALL_RAT_USAGE)
219                 .writeInt(usage.carrierId)
220                 .writeInt(usage.rat)
221                 .writeLong(
222                         round(usage.totalDurationMillis, DURATION_BUCKET_MILLIS) / SECOND_IN_MILLIS)
223                 .writeLong(usage.callCount)
224                 .build();
225     }
226 
buildStatsEvent(VoiceCallSession session)227     private static StatsEvent buildStatsEvent(VoiceCallSession session) {
228         return StatsEvent.newBuilder()
229                 .setAtomId(VOICE_CALL_SESSION)
230                 .writeInt(session.bearerAtStart)
231                 .writeInt(session.bearerAtEnd)
232                 .writeInt(session.direction)
233                 .writeInt(session.setupDuration)
234                 .writeBoolean(session.setupFailed)
235                 .writeInt(session.disconnectReasonCode)
236                 .writeInt(session.disconnectExtraCode)
237                 .writeString(session.disconnectExtraMessage)
238                 .writeInt(session.ratAtStart)
239                 .writeInt(session.ratAtEnd)
240                 .writeLong(session.ratSwitchCount)
241                 .writeLong(session.codecBitmask)
242                 .writeInt(session.concurrentCallCountAtStart)
243                 .writeInt(session.concurrentCallCountAtEnd)
244                 .writeInt(session.simSlotIndex)
245                 .writeBoolean(session.isMultiSim)
246                 .writeBoolean(session.isEsim)
247                 .writeInt(session.carrierId)
248                 .writeBoolean(session.srvccCompleted)
249                 .writeLong(session.srvccFailureCount)
250                 .writeLong(session.srvccCancellationCount)
251                 .writeBoolean(session.rttEnabled)
252                 .writeBoolean(session.isEmergency)
253                 .writeBoolean(session.isRoaming)
254                 // workaround: dimension required for keeping multiple pulled atoms
255                 .writeInt(sRandom.nextInt())
256                 .build();
257     }
258 
259     /** Returns the value rounded to the bucket. */
round(long value, long bucket)260     private static long round(long value, long bucket) {
261         return ((value + bucket / 2) / bucket) * bucket;
262     }
263 }
264