| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.telephony.metrics; |
| |
| import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_HANDOVER; |
| import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_NORMAL; |
| import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_RADIO_OFF; |
| import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_UNKNOWN; |
| import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__IP_TYPE__APN_PROTOCOL_IPV4; |
| |
| import android.annotation.Nullable; |
| import android.os.SystemClock; |
| import android.telephony.Annotation.ApnType; |
| import android.telephony.Annotation.DataFailureCause; |
| import android.telephony.Annotation.NetworkType; |
| import android.telephony.DataFailCause; |
| import android.telephony.ServiceState; |
| import android.telephony.TelephonyManager; |
| import android.telephony.data.ApnSetting.ProtocolType; |
| import android.telephony.data.DataCallResponse; |
| import android.telephony.data.DataService; |
| import android.telephony.data.DataService.DeactivateDataReason; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.telephony.Phone; |
| import com.android.internal.telephony.PhoneFactory; |
| import com.android.internal.telephony.ServiceStateTracker; |
| import com.android.internal.telephony.SubscriptionController; |
| import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession; |
| import com.android.internal.telephony.subscription.SubscriptionInfoInternal; |
| import com.android.internal.telephony.subscription.SubscriptionManagerService; |
| import com.android.telephony.Rlog; |
| |
| import java.util.Arrays; |
| import java.util.Random; |
| |
| /** Collects data call change events per DataConnection for the pulled atom. */ |
| public class DataCallSessionStats { |
| private static final String TAG = DataCallSessionStats.class.getSimpleName(); |
| |
| private final Phone mPhone; |
| private long mStartTime; |
| @Nullable private DataCallSession mDataCallSession; |
| |
| private final PersistAtomsStorage mAtomsStorage = |
| PhoneFactory.getMetricsCollector().getAtomsStorage(); |
| |
| private static final Random RANDOM = new Random(); |
| |
| public static final int SIZE_LIMIT_HANDOVER_FAILURES = 15; |
| |
| public DataCallSessionStats(Phone phone) { |
| mPhone = phone; |
| } |
| |
| /** Creates a new ongoing atom when data call is set up. */ |
| public synchronized void onSetupDataCall(@ApnType int apnTypeBitMask) { |
| mDataCallSession = getDefaultProto(apnTypeBitMask); |
| mStartTime = getTimeMillis(); |
| PhoneFactory.getMetricsCollector().registerOngoingDataCallStat(this); |
| } |
| |
| /** |
| * Updates the ongoing dataCall's atom for data call response event. |
| * |
| * @param response setup Data call response |
| * @param currentRat The data call current Network Type |
| * @param apnTypeBitmask APN type bitmask |
| * @param protocol Data connection protocol |
| * @param failureCause The raw failure cause from modem/IWLAN data service. |
| */ |
| public synchronized void onSetupDataCallResponse( |
| @Nullable DataCallResponse response, |
| @NetworkType int currentRat, |
| @ApnType int apnTypeBitmask, |
| @ProtocolType int protocol, |
| int failureCause) { |
| // there should've been a call to onSetupDataCall to initiate the atom, |
| // so this method is being called out of order -> no metric will be logged |
| if (mDataCallSession == null) { |
| loge("onSetupDataCallResponse: no DataCallSession atom has been initiated."); |
| return; |
| } |
| |
| if (currentRat != TelephonyManager.NETWORK_TYPE_UNKNOWN) { |
| mDataCallSession.ratAtEnd = currentRat; |
| mDataCallSession.bandAtEnd = |
| (currentRat == TelephonyManager.NETWORK_TYPE_IWLAN) |
| ? 0 |
| : ServiceStateStats.getBand(mPhone); |
| } |
| |
| // only set if apn hasn't been set during setup |
| if (mDataCallSession.apnTypeBitmask == 0) { |
| mDataCallSession.apnTypeBitmask = apnTypeBitmask; |
| } |
| |
| mDataCallSession.ipType = protocol; |
| mDataCallSession.failureCause = failureCause; |
| if (response != null) { |
| mDataCallSession.suggestedRetryMillis = |
| (int) Math.min(response.getRetryDurationMillis(), Integer.MAX_VALUE); |
| // If setup has failed, then store the atom |
| if (failureCause != DataFailCause.NONE) { |
| mDataCallSession.setupFailed = true; |
| endDataCallSession(); |
| } |
| } |
| } |
| |
| /** |
| * Updates the dataCall atom when data call is deactivated. |
| * |
| * @param reason Deactivate reason |
| */ |
| public synchronized void setDeactivateDataCallReason(@DeactivateDataReason int reason) { |
| // there should've been another call to initiate the atom, |
| // so this method is being called out of order -> no metric will be logged |
| if (mDataCallSession == null) { |
| loge("setDeactivateDataCallReason: no DataCallSession atom has been initiated."); |
| return; |
| } |
| switch (reason) { |
| case DataService.REQUEST_REASON_NORMAL: |
| mDataCallSession.deactivateReason = |
| DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_NORMAL; |
| break; |
| case DataService.REQUEST_REASON_SHUTDOWN: |
| mDataCallSession.deactivateReason = |
| DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_RADIO_OFF; |
| break; |
| case DataService.REQUEST_REASON_HANDOVER: |
| mDataCallSession.deactivateReason = |
| DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_HANDOVER; |
| break; |
| default: |
| mDataCallSession.deactivateReason = |
| DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_UNKNOWN; |
| break; |
| } |
| } |
| |
| /** |
| * Stores the atom when DataConnection reaches DISCONNECTED state. |
| * |
| * @param failureCause failure cause as per android.telephony.DataFailCause |
| */ |
| public synchronized void onDataCallDisconnected(@DataFailureCause int failureCause) { |
| // there should've been another call to initiate the atom, |
| // so this method is being called out of order -> no atom will be saved |
| // this also happens when DataConnection is created, which is expected |
| if (mDataCallSession == null) { |
| logi("onDataCallDisconnected: no DataCallSession atom has been initiated."); |
| return; |
| } |
| mDataCallSession.failureCause = failureCause; |
| mDataCallSession.durationMinutes = convertMillisToMinutes(getTimeMillis() - mStartTime); |
| endDataCallSession(); |
| } |
| |
| /** |
| * Updates the atom when a handover fails. Note we only record distinct failure causes, as in |
| * most cases retry failures are due to the same cause. |
| * |
| * @param failureCause failure cause as per android.telephony.DataFailCause |
| */ |
| public synchronized void onHandoverFailure(@DataFailureCause int failureCause, |
| @NetworkType int sourceRat, @NetworkType int targetRat) { |
| if (mDataCallSession != null |
| && mDataCallSession.handoverFailureCauses.length |
| < SIZE_LIMIT_HANDOVER_FAILURES) { |
| |
| int[] failureCauses = mDataCallSession.handoverFailureCauses; |
| int[] handoverFailureRats = mDataCallSession.handoverFailureRat; |
| int failureDirection = sourceRat | (targetRat << 16); |
| |
| for (int i = 0; i < failureCauses.length; i++) { |
| if (failureCauses[i] == failureCause |
| && handoverFailureRats[i] == failureDirection) { |
| return; |
| } |
| } |
| |
| mDataCallSession.handoverFailureCauses = Arrays.copyOf( |
| failureCauses, failureCauses.length + 1); |
| mDataCallSession.handoverFailureCauses[failureCauses.length] = failureCause; |
| |
| mDataCallSession.handoverFailureRat = Arrays.copyOf(handoverFailureRats, |
| handoverFailureRats.length + 1); |
| mDataCallSession.handoverFailureRat[handoverFailureRats.length] = failureDirection; |
| } |
| } |
| |
| /** |
| * Updates the atom when data registration state or RAT changes. |
| * |
| * <p>NOTE: in {@link ServiceStateTracker}, change of channel number will trigger data |
| * registration state change. |
| */ |
| public synchronized void onDrsOrRatChanged(@NetworkType int currentRat) { |
| if (mDataCallSession != null && currentRat != TelephonyManager.NETWORK_TYPE_UNKNOWN) { |
| if (mDataCallSession.ratAtEnd != currentRat) { |
| mDataCallSession.ratSwitchCount++; |
| mDataCallSession.ratAtEnd = currentRat; |
| } |
| // band may have changed even if RAT was the same |
| mDataCallSession.bandAtEnd = |
| (currentRat == TelephonyManager.NETWORK_TYPE_IWLAN) |
| ? 0 |
| : ServiceStateStats.getBand(mPhone); |
| } |
| } |
| |
| /** Stores the current unmetered network types information in permanent storage. */ |
| public void onUnmeteredUpdate(@NetworkType int networkType) { |
| mAtomsStorage |
| .addUnmeteredNetworks( |
| mPhone.getPhoneId(), |
| mPhone.getCarrierId(), |
| TelephonyManager.getBitMaskForNetworkType(networkType)); |
| } |
| |
| /** |
| * Take a snapshot of the on-going data call segment to add to the atom storage. |
| * |
| * Note the following fields are reset after the snapshot: |
| * - rat switch count |
| * - handover failure causes |
| * - handover failure rats |
| */ |
| public synchronized void conclude() { |
| if (mDataCallSession != null) { |
| DataCallSession call = copyOf(mDataCallSession); |
| long nowMillis = getTimeMillis(); |
| call.durationMinutes = convertMillisToMinutes(nowMillis - mStartTime); |
| mStartTime = nowMillis; |
| mDataCallSession.ratSwitchCount = 0L; |
| mDataCallSession.handoverFailureCauses = new int[0]; |
| mDataCallSession.handoverFailureRat = new int[0]; |
| mAtomsStorage.addDataCallSession(call); |
| } |
| } |
| |
| /** Put the current data call to an end after being uploaded to AtomStorage. */ |
| private void endDataCallSession() { |
| mDataCallSession.oosAtEnd = getIsOos(); |
| mDataCallSession.ongoing = false; |
| // store for the data call list event, after DataCall is disconnected and entered into |
| // inactive mode |
| PhoneFactory.getMetricsCollector().unregisterOngoingDataCallStat(this); |
| mAtomsStorage.addDataCallSession(mDataCallSession); |
| mDataCallSession = null; |
| } |
| |
| private static long convertMillisToMinutes(long millis) { |
| return Math.round(millis / 60000.0); |
| } |
| |
| private static DataCallSession copyOf(DataCallSession call) { |
| DataCallSession copy = new DataCallSession(); |
| copy.dimension = call.dimension; |
| copy.isMultiSim = call.isMultiSim; |
| copy.isEsim = call.isEsim; |
| copy.apnTypeBitmask = call.apnTypeBitmask; |
| copy.carrierId = call.carrierId; |
| copy.isRoaming = call.isRoaming; |
| copy.ratAtEnd = call.ratAtEnd; |
| copy.oosAtEnd = call.oosAtEnd; |
| copy.ratSwitchCount = call.ratSwitchCount; |
| copy.isOpportunistic = call.isOpportunistic; |
| copy.ipType = call.ipType; |
| copy.setupFailed = call.setupFailed; |
| copy.failureCause = call.failureCause; |
| copy.suggestedRetryMillis = call.suggestedRetryMillis; |
| copy.deactivateReason = call.deactivateReason; |
| copy.durationMinutes = call.durationMinutes; |
| copy.ongoing = call.ongoing; |
| copy.bandAtEnd = call.bandAtEnd; |
| copy.handoverFailureCauses = Arrays.copyOf(call.handoverFailureCauses, |
| call.handoverFailureCauses.length); |
| copy.handoverFailureRat = Arrays.copyOf(call.handoverFailureRat, |
| call.handoverFailureRat.length); |
| return copy; |
| } |
| |
| /** Creates a proto for a normal {@code DataCallSession} with default values. */ |
| private DataCallSession getDefaultProto(@ApnType int apnTypeBitmask) { |
| DataCallSession proto = new DataCallSession(); |
| proto.dimension = RANDOM.nextInt(); |
| proto.isMultiSim = SimSlotState.isMultiSim(); |
| proto.isEsim = SimSlotState.isEsim(mPhone.getPhoneId()); |
| proto.apnTypeBitmask = apnTypeBitmask; |
| proto.carrierId = mPhone.getCarrierId(); |
| proto.isRoaming = getIsRoaming(); |
| proto.oosAtEnd = false; |
| proto.ratSwitchCount = 0L; |
| proto.isOpportunistic = getIsOpportunistic(); |
| proto.ipType = DATA_CALL_SESSION__IP_TYPE__APN_PROTOCOL_IPV4; |
| proto.setupFailed = false; |
| proto.failureCause = DataFailCause.NONE; |
| proto.suggestedRetryMillis = 0; |
| proto.deactivateReason = DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_UNKNOWN; |
| proto.durationMinutes = 0; |
| proto.ongoing = true; |
| proto.handoverFailureCauses = new int[0]; |
| proto.handoverFailureRat = new int[0]; |
| return proto; |
| } |
| |
| private boolean getIsRoaming() { |
| ServiceStateTracker serviceStateTracker = mPhone.getServiceStateTracker(); |
| ServiceState serviceState = |
| serviceStateTracker != null ? serviceStateTracker.getServiceState() : null; |
| return serviceState != null && serviceState.getRoaming(); |
| } |
| |
| private boolean getIsOpportunistic() { |
| if (mPhone.isSubscriptionManagerServiceEnabled()) { |
| SubscriptionInfoInternal subInfo = SubscriptionManagerService.getInstance() |
| .getSubscriptionInfoInternal(mPhone.getSubId()); |
| return subInfo != null && subInfo.isOpportunistic(); |
| } |
| SubscriptionController subController = SubscriptionController.getInstance(); |
| return subController != null && subController.isOpportunistic(mPhone.getSubId()); |
| } |
| |
| private boolean getIsOos() { |
| ServiceStateTracker serviceStateTracker = mPhone.getServiceStateTracker(); |
| ServiceState serviceState = |
| serviceStateTracker != null ? serviceStateTracker.getServiceState() : null; |
| return serviceState != null |
| && serviceState.getDataRegistrationState() == ServiceState.STATE_OUT_OF_SERVICE; |
| } |
| |
| private void logi(String format, Object... args) { |
| Rlog.i(TAG, "[" + mPhone.getPhoneId() + "]" + String.format(format, args)); |
| } |
| |
| private void loge(String format, Object... args) { |
| Rlog.e(TAG, "[" + mPhone.getPhoneId() + "]" + String.format(format, args)); |
| } |
| |
| @VisibleForTesting |
| protected long getTimeMillis() { |
| return SystemClock.elapsedRealtime(); |
| } |
| } |