blob: 062a49ffb756a845e9cb04e12eb1165dd66108bf [file] [log] [blame]
/*
* 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();
}
}