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 com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__IP_TYPE__APN_PROTOCOL_IPV4;
20 
21 import android.annotation.Nullable;
22 import android.net.NetworkCapabilities;
23 import android.os.SystemClock;
24 import android.telephony.Annotation.ApnType;
25 import android.telephony.Annotation.DataFailureCause;
26 import android.telephony.Annotation.NetworkType;
27 import android.telephony.DataFailCause;
28 import android.telephony.NetworkRegistrationInfo;
29 import android.telephony.ServiceState;
30 import android.telephony.SubscriptionInfo;
31 import android.telephony.SubscriptionManager;
32 import android.telephony.TelephonyManager;
33 import android.telephony.data.ApnSetting;
34 import android.telephony.data.ApnSetting.ProtocolType;
35 import android.telephony.data.DataCallResponse;
36 import android.telephony.data.DataService;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.telephony.Phone;
40 import com.android.internal.telephony.PhoneFactory;
41 import com.android.internal.telephony.ServiceStateTracker;
42 import com.android.internal.telephony.data.DataNetwork;
43 import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession;
44 import com.android.internal.telephony.satellite.SatelliteController;
45 import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
46 import com.android.internal.telephony.subscription.SubscriptionManagerService;
47 import com.android.telephony.Rlog;
48 
49 import java.util.Arrays;
50 import java.util.Random;
51 
52 /** Collects data call change events per DataConnection for the pulled atom. */
53 public class DataCallSessionStats {
54     private static final String TAG = DataCallSessionStats.class.getSimpleName();
55 
56     private final Phone mPhone;
57     private long mStartTime;
58     @Nullable private DataCallSession mDataCallSession;
59 
60     private final PersistAtomsStorage mAtomsStorage =
61             PhoneFactory.getMetricsCollector().getAtomsStorage();
62 
63     private static final Random RANDOM = new Random();
64 
65     public static final int SIZE_LIMIT_HANDOVER_FAILURES = 15;
66 
67     private final DefaultNetworkMonitor mDefaultNetworkMonitor;
68     private final SatelliteController mSatelliteController;
69 
DataCallSessionStats(Phone phone)70     public DataCallSessionStats(Phone phone) {
71         mPhone = phone;
72         mDefaultNetworkMonitor = PhoneFactory.getMetricsCollector().getDefaultNetworkMonitor();
73         mSatelliteController = SatelliteController.getInstance();
74     }
75 
isSystemDefaultNetworkMobile()76     private boolean isSystemDefaultNetworkMobile() {
77         NetworkCapabilities nc = mDefaultNetworkMonitor.getNetworkCapabilities();
78         return nc != null && nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
79     }
80 
81     /** Creates a new ongoing atom when data call is set up. */
onSetupDataCall(@pnType int apnTypeBitMask, boolean isSatellite)82     public synchronized void onSetupDataCall(@ApnType int apnTypeBitMask,
83             boolean isSatellite) {
84         mDataCallSession = getDefaultProto(apnTypeBitMask, isSatellite);
85         mStartTime = getTimeMillis();
86         PhoneFactory.getMetricsCollector().registerOngoingDataCallStat(this);
87     }
88 
89     /**
90      * Updates the ongoing dataCall's atom for data call response event.
91      *
92      * @param response setup Data call response
93      * @param currentRat The data call current Network Type
94      * @param apnTypeBitmask APN type bitmask
95      * @param protocol Data connection protocol
96      * @param failureCause The raw failure cause from modem/IWLAN data service.
97      */
onSetupDataCallResponse( @ullable DataCallResponse response, @NetworkType int currentRat, @ApnType int apnTypeBitmask, @ProtocolType int protocol, int failureCause)98     public synchronized void onSetupDataCallResponse(
99             @Nullable DataCallResponse response,
100             @NetworkType int currentRat,
101             @ApnType int apnTypeBitmask,
102             @ProtocolType int protocol,
103             int failureCause) {
104         // there should've been a call to onSetupDataCall to initiate the atom,
105         // so this method is being called out of order -> no metric will be logged
106         if (mDataCallSession == null) {
107             loge("onSetupDataCallResponse: no DataCallSession atom has been initiated.");
108             return;
109         }
110 
111         if (currentRat != TelephonyManager.NETWORK_TYPE_UNKNOWN) {
112             mDataCallSession.ratAtEnd = currentRat;
113             mDataCallSession.bandAtEnd =
114                     (currentRat == TelephonyManager.NETWORK_TYPE_IWLAN)
115                             ? 0
116                             : ServiceStateStats.getBand(mPhone);
117             // Limitation: Will not capture IKE mobility between Backup Calling <-> WiFi Calling.
118             mDataCallSession.isIwlanCrossSim = currentRat == TelephonyManager.NETWORK_TYPE_IWLAN
119                     && isSystemDefaultNetworkMobile();
120         }
121 
122         // only set if apn hasn't been set during setup
123         if (mDataCallSession.apnTypeBitmask == 0) {
124             mDataCallSession.apnTypeBitmask = apnTypeBitmask;
125         }
126 
127         mDataCallSession.ipType = protocol;
128         mDataCallSession.failureCause = failureCause;
129         if (response != null) {
130             mDataCallSession.suggestedRetryMillis =
131                     (int) Math.min(response.getRetryDurationMillis(), Integer.MAX_VALUE);
132             // If setup has failed, then store the atom
133             if (failureCause != DataFailCause.NONE) {
134                 mDataCallSession.setupFailed = true;
135                 endDataCallSession();
136             }
137         }
138     }
139 
140     /**
141      * Updates the dataCall atom when data call is deactivated.
142      *
143      * @param reason Tear down reason
144      */
setDeactivateDataCallReason(@ataNetwork.TearDownReason int reason)145     public synchronized void setDeactivateDataCallReason(@DataNetwork.TearDownReason int reason) {
146         // there should've been another call to initiate the atom,
147         // so this method is being called out of order -> no metric will be logged
148         if (mDataCallSession == null) {
149             loge("setDeactivateDataCallReason: no DataCallSession atom has been initiated.");
150             return;
151         }
152         // Skip the pre-U enum. See enum DataDeactivateReasonEnum in enums.proto
153         mDataCallSession.deactivateReason = reason + DataService.REQUEST_REASON_HANDOVER + 1;
154     }
155 
156     /**
157      * Stores the atom when DataConnection reaches DISCONNECTED state.
158      *
159      * @param failureCause failure cause as per android.telephony.DataFailCause
160      */
onDataCallDisconnected(@ataFailureCause int failureCause)161     public synchronized void onDataCallDisconnected(@DataFailureCause int failureCause) {
162         // there should've been another call to initiate the atom,
163         // so this method is being called out of order -> no atom will be saved
164         // this also happens when DataConnection is created, which is expected
165         if (mDataCallSession == null) {
166             logi("onDataCallDisconnected: no DataCallSession atom has been initiated.");
167             return;
168         }
169         mDataCallSession.failureCause = failureCause;
170         mDataCallSession.durationMinutes = convertMillisToMinutes(getTimeMillis() - mStartTime);
171         endDataCallSession();
172     }
173 
174     /**
175      * Updates the atom when a handover fails. Note we only record distinct failure causes, as in
176      * most cases retry failures are due to the same cause.
177      *
178      * @param failureCause failure cause as per android.telephony.DataFailCause
179      */
onHandoverFailure(@ataFailureCause int failureCause, @NetworkType int sourceRat, @NetworkType int targetRat)180     public synchronized void onHandoverFailure(@DataFailureCause int failureCause,
181             @NetworkType int sourceRat, @NetworkType int targetRat) {
182         if (mDataCallSession != null
183                 && mDataCallSession.handoverFailureCauses.length
184                 < SIZE_LIMIT_HANDOVER_FAILURES) {
185 
186             int[] failureCauses = mDataCallSession.handoverFailureCauses;
187             int[] handoverFailureRats = mDataCallSession.handoverFailureRat;
188             int failureDirection = sourceRat | (targetRat << 16);
189 
190             for (int i = 0; i < failureCauses.length; i++) {
191                 if (failureCauses[i] == failureCause
192                         && handoverFailureRats[i] == failureDirection) {
193                     return;
194                 }
195             }
196 
197             mDataCallSession.handoverFailureCauses = Arrays.copyOf(
198                     failureCauses, failureCauses.length + 1);
199             mDataCallSession.handoverFailureCauses[failureCauses.length] = failureCause;
200 
201             mDataCallSession.handoverFailureRat = Arrays.copyOf(handoverFailureRats,
202                     handoverFailureRats.length + 1);
203             mDataCallSession.handoverFailureRat[handoverFailureRats.length] = failureDirection;
204         }
205     }
206 
207     /**
208      * Updates the atom when data registration state or RAT changes.
209      *
210      * <p>NOTE: in {@link ServiceStateTracker}, change of channel number will trigger data
211      * registration state change.
212      */
onDrsOrRatChanged(@etworkType int currentRat)213     public synchronized void onDrsOrRatChanged(@NetworkType int currentRat) {
214         if (mDataCallSession != null && currentRat != TelephonyManager.NETWORK_TYPE_UNKNOWN) {
215             if (mDataCallSession.ratAtEnd != currentRat) {
216                 mDataCallSession.ratSwitchCount++;
217                 mDataCallSession.ratAtEnd = currentRat;
218                 mDataCallSession.isIwlanCrossSim = currentRat == TelephonyManager.NETWORK_TYPE_IWLAN
219                         && isSystemDefaultNetworkMobile();
220             }
221             // band may have changed even if RAT was the same
222             mDataCallSession.bandAtEnd =
223                     (currentRat == TelephonyManager.NETWORK_TYPE_IWLAN)
224                             ? 0
225                             : ServiceStateStats.getBand(mPhone);
226         }
227     }
228 
229     /** Stores the current unmetered network types information in permanent storage. */
onUnmeteredUpdate(@etworkType int networkType)230     public void onUnmeteredUpdate(@NetworkType int networkType) {
231         mAtomsStorage
232                 .addUnmeteredNetworks(
233                         mPhone.getPhoneId(),
234                         mPhone.getCarrierId(),
235                         TelephonyManager.getBitMaskForNetworkType(networkType));
236     }
237 
238     /**
239      * Take a snapshot of the on-going data call segment to add to the atom storage.
240      *
241      * Note the following fields are reset after the snapshot:
242      * - rat switch count
243      * - handover failure causes
244      * - handover failure rats
245      */
conclude()246     public synchronized void conclude() {
247         if (mDataCallSession != null) {
248             DataCallSession call = copyOf(mDataCallSession);
249             long nowMillis = getTimeMillis();
250             call.durationMinutes = convertMillisToMinutes(nowMillis - mStartTime);
251             mStartTime = nowMillis;
252             mDataCallSession.ratSwitchCount = 0L;
253             mDataCallSession.handoverFailureCauses = new int[0];
254             mDataCallSession.handoverFailureRat = new int[0];
255             mAtomsStorage.addDataCallSession(call);
256         }
257     }
258 
259     /** Put the current data call to an end after being uploaded to AtomStorage. */
endDataCallSession()260     private void endDataCallSession() {
261         mDataCallSession.oosAtEnd = getIsOos();
262         mDataCallSession.ongoing = false;
263         // set if this data call is established for internet on the non-Dds
264         SubscriptionInfo subInfo = SubscriptionManagerService.getInstance()
265                 .getSubscriptionInfo(mPhone.getSubId());
266         if (mPhone.getSubId() != SubscriptionManager.getDefaultDataSubscriptionId()
267                 && ((mDataCallSession.apnTypeBitmask & ApnSetting.TYPE_DEFAULT)
268                 == ApnSetting.TYPE_DEFAULT)
269                 && subInfo != null && !subInfo.isOpportunistic()) {
270             mDataCallSession.isNonDds = true;
271         }
272 
273         // store for the data call list event, after DataCall is disconnected and entered into
274         // inactive mode
275         PhoneFactory.getMetricsCollector().unregisterOngoingDataCallStat(this);
276         mAtomsStorage.addDataCallSession(mDataCallSession);
277         mDataCallSession = null;
278     }
279 
convertMillisToMinutes(long millis)280     private static long convertMillisToMinutes(long millis) {
281         return Math.round(millis / 60000.0);
282     }
283 
copyOf(DataCallSession call)284     private static DataCallSession copyOf(DataCallSession call) {
285         DataCallSession copy = new DataCallSession();
286         copy.dimension = call.dimension;
287         copy.isMultiSim = call.isMultiSim;
288         copy.isEsim = call.isEsim;
289         copy.apnTypeBitmask = call.apnTypeBitmask;
290         copy.carrierId = call.carrierId;
291         copy.isRoaming = call.isRoaming;
292         copy.ratAtEnd = call.ratAtEnd;
293         copy.oosAtEnd = call.oosAtEnd;
294         copy.ratSwitchCount = call.ratSwitchCount;
295         copy.isOpportunistic = call.isOpportunistic;
296         copy.ipType = call.ipType;
297         copy.setupFailed = call.setupFailed;
298         copy.failureCause = call.failureCause;
299         copy.suggestedRetryMillis = call.suggestedRetryMillis;
300         copy.deactivateReason = call.deactivateReason;
301         copy.durationMinutes = call.durationMinutes;
302         copy.ongoing = call.ongoing;
303         copy.bandAtEnd = call.bandAtEnd;
304         copy.handoverFailureCauses = Arrays.copyOf(call.handoverFailureCauses,
305                 call.handoverFailureCauses.length);
306         copy.handoverFailureRat = Arrays.copyOf(call.handoverFailureRat,
307                 call.handoverFailureRat.length);
308         copy.isNonDds = call.isNonDds;
309         copy.isIwlanCrossSim = call.isIwlanCrossSim;
310         copy.isNtn = call.isNtn;
311         copy.isSatelliteTransport = call.isSatelliteTransport;
312         copy.isProvisioningProfile = call.isProvisioningProfile;
313         return copy;
314     }
315 
316     /** Creates a proto for a normal {@code DataCallSession} with default values. */
getDefaultProto(@pnType int apnTypeBitmask, boolean isSatellite)317     private DataCallSession getDefaultProto(@ApnType int apnTypeBitmask,
318             boolean isSatellite) {
319         DataCallSession proto = new DataCallSession();
320         proto.dimension = RANDOM.nextInt();
321         proto.isMultiSim = SimSlotState.isMultiSim();
322         proto.isEsim = SimSlotState.isEsim(mPhone.getPhoneId());
323         proto.apnTypeBitmask = apnTypeBitmask;
324         proto.carrierId = mPhone.getCarrierId();
325         proto.isRoaming = getIsRoaming();
326         proto.oosAtEnd = false;
327         proto.ratSwitchCount = 0L;
328         proto.isOpportunistic = getIsOpportunistic();
329         proto.ipType = DATA_CALL_SESSION__IP_TYPE__APN_PROTOCOL_IPV4;
330         proto.setupFailed = false;
331         proto.failureCause = DataFailCause.NONE;
332         proto.suggestedRetryMillis = 0;
333         proto.deactivateReason = DataNetwork.TEAR_DOWN_REASON_NONE;
334         proto.durationMinutes = 0;
335         proto.ongoing = true;
336         proto.handoverFailureCauses = new int[0];
337         proto.handoverFailureRat = new int[0];
338         proto.isNonDds = false;
339         proto.isIwlanCrossSim = false;
340         proto.isNtn = mSatelliteController != null
341                 ? mSatelliteController.isInSatelliteModeForCarrierRoaming(mPhone) : false;
342         proto.isSatelliteTransport = isSatellite;
343         proto.isProvisioningProfile = getIsProvisioningProfile();
344         return proto;
345     }
346 
getIsRoaming()347     private boolean getIsRoaming() {
348         ServiceStateTracker serviceStateTracker = mPhone.getServiceStateTracker();
349         ServiceState serviceState =
350                 serviceStateTracker != null ? serviceStateTracker.getServiceState() : null;
351         return ServiceStateStats.isNetworkRoaming(serviceState, NetworkRegistrationInfo.DOMAIN_PS);
352     }
353 
getIsOpportunistic()354     private boolean getIsOpportunistic() {
355         SubscriptionInfoInternal subInfo = SubscriptionManagerService.getInstance()
356                 .getSubscriptionInfoInternal(mPhone.getSubId());
357         return subInfo != null && subInfo.isOpportunistic();
358     }
359 
getIsProvisioningProfile()360     private boolean getIsProvisioningProfile() {
361         SubscriptionInfoInternal subInfo = SubscriptionManagerService.getInstance()
362                 .getSubscriptionInfoInternal(mPhone.getSubId());
363         try {
364             return subInfo.getProfileClass() == SubscriptionManager.PROFILE_CLASS_PROVISIONING;
365         } catch (Exception ex) {
366             loge("getIsProvisioningProfile: " + ex.getMessage());
367             return false;
368         }
369     }
370 
getIsOos()371     private boolean getIsOos() {
372         ServiceStateTracker serviceStateTracker = mPhone.getServiceStateTracker();
373         ServiceState serviceState =
374                 serviceStateTracker != null ? serviceStateTracker.getServiceState() : null;
375         return serviceState != null
376                 && serviceState.getDataRegistrationState() == ServiceState.STATE_OUT_OF_SERVICE;
377     }
378 
logi(String format, Object... args)379     private void logi(String format, Object... args) {
380         Rlog.i(TAG, "[" + mPhone.getPhoneId() + "]" + String.format(format, args));
381     }
382 
loge(String format, Object... args)383     private void loge(String format, Object... args) {
384         Rlog.e(TAG, "[" + mPhone.getPhoneId() + "]" + String.format(format, args));
385     }
386 
387     @VisibleForTesting
getTimeMillis()388     protected long getTimeMillis() {
389         return SystemClock.elapsedRealtime();
390     }
391 }
392