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