blob: 9b988add1b797cf0334cd77af8ab3a2b933e01d3 [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.VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_UNKNOWN;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MO;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MT;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_FULLBAND;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_WIDEBAND;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_SLOW;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_FAST;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_NORMAL;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_SLOW;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_SLOW;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_FAST;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_SLOW;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_GREAT;
import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
import android.annotation.Nullable;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.SystemClock;
import android.telecom.VideoProfile;
import android.telecom.VideoProfile.VideoState;
import android.telephony.Annotation.NetworkType;
import android.telephony.DisconnectCause;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ImsStreamMediaProfile;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.DriverCall;
import com.android.internal.telephony.GsmCdmaConnection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.ServiceStateTracker;
import com.android.internal.telephony.imsphone.ImsPhoneConnection;
import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.AudioCodec;
import com.android.internal.telephony.uicc.UiccController;
import com.android.telephony.Rlog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/** Collects voice call events per phone ID for the pulled atom. */
public class VoiceCallSessionStats {
private static final String TAG = VoiceCallSessionStats.class.getSimpleName();
/** Upper bounds of each call setup duration category in milliseconds. */
private static final int CALL_SETUP_DURATION_UNKNOWN = 0;
private static final int CALL_SETUP_DURATION_EXTREMELY_FAST = 400;
private static final int CALL_SETUP_DURATION_ULTRA_FAST = 700;
private static final int CALL_SETUP_DURATION_VERY_FAST = 1000;
private static final int CALL_SETUP_DURATION_FAST = 1500;
private static final int CALL_SETUP_DURATION_NORMAL = 2500;
private static final int CALL_SETUP_DURATION_SLOW = 4000;
private static final int CALL_SETUP_DURATION_VERY_SLOW = 6000;
private static final int CALL_SETUP_DURATION_ULTRA_SLOW = 10000;
// CALL_SETUP_DURATION_EXTREMELY_SLOW has no upper bound (it includes everything above 10000)
/** Number of buckets for codec quality, from UNKNOWN to FULLBAND. */
private static final int CODEC_QUALITY_COUNT = 5;
/**
* Threshold to calculate the main audio codec quality of the call.
*
* The audio codec quality was equal to or greater than the main audio codec quality for
* at least 70% of the call.
*/
private static final int MAIN_CODEC_QUALITY_THRESHOLD = 70;
/** Holds the audio codec value for CS calls. */
private static final SparseIntArray CS_CODEC_MAP = buildGsmCdmaCodecMap();
/** Holds the audio codec value for IMS calls. */
private static final SparseIntArray IMS_CODEC_MAP = buildImsCodecMap();
/** Holds setup duration buckets with values as their upper bounds in milliseconds. */
private static final SparseIntArray CALL_SETUP_DURATION_MAP = buildCallSetupDurationMap();
/**
* Tracks statistics for each call connection, indexed with ID returned by {@link
* #getConnectionId}.
*/
private final SparseArray<VoiceCallSession> mCallProtos = new SparseArray<>();
/**
* Tracks usage of codecs for each call. The outer array is used to map each connection id to
* the corresponding codec usage. The inner array is used to map timestamp (key) with the
* codec in use (value).
*/
private final SparseArray<LongSparseArray<Integer>> mCodecUsage = new SparseArray<>();
/**
* Tracks call RAT usage.
*
* <p>RAT usage is mainly tied to phones rather than calls, since each phone can have multiple
* concurrent calls, and we do not want to count the RAT duration multiple times.
*/
private final VoiceCallRatTracker mRatUsage = new VoiceCallRatTracker();
private final int mPhoneId;
private final Phone mPhone;
private final PersistAtomsStorage mAtomsStorage =
PhoneFactory.getMetricsCollector().getAtomsStorage();
private final UiccController mUiccController = UiccController.getInstance();
public VoiceCallSessionStats(int phoneId, Phone phone) {
mPhoneId = phoneId;
mPhone = phone;
}
/* CS calls */
/** Updates internal states when previous CS calls are accepted to track MT call setup time. */
public synchronized void onRilAcceptCall(List<Connection> connections) {
for (Connection conn : connections) {
addCall(conn);
}
}
/** Updates internal states when a CS MO call is created. */
public synchronized void onRilDial(Connection conn) {
addCall(conn);
}
/**
* Updates internal states when CS calls are created or terminated, or CS call state is changed.
*/
public synchronized void onRilCallListChanged(List<GsmCdmaConnection> connections) {
for (Connection conn : connections) {
int id = getConnectionId(conn);
if (!mCallProtos.contains(id)) {
// handle new connections
if (conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
addCall(conn);
checkCallSetup(conn, mCallProtos.get(id));
} else {
logd("onRilCallListChanged: skip adding disconnected connection");
}
} else {
VoiceCallSession proto = mCallProtos.get(id);
// handle call state change
checkCallSetup(conn, proto);
// handle terminated connections
if (conn.getDisconnectCause() != DisconnectCause.NOT_DISCONNECTED) {
proto.bearerAtEnd = getBearer(conn); // should be CS
proto.disconnectReasonCode = conn.getDisconnectCause();
proto.disconnectExtraCode = conn.getPreciseDisconnectCause();
proto.disconnectExtraMessage = conn.getVendorDisconnectCause();
finishCall(id);
}
}
}
// NOTE: we cannot check stray connections (CS call in our list but not in RIL), as
// GsmCdmaCallTracker can call this with a partial list
}
/* IMS calls */
/** Updates internal states when an IMS MO call is created. */
public synchronized void onImsDial(ImsPhoneConnection conn) {
addCall(conn);
if (conn.hasRttTextStream()) {
setRttStarted(conn);
}
}
/** Updates internal states when an IMS MT call is created. */
public synchronized void onImsCallReceived(ImsPhoneConnection conn) {
addCall(conn);
if (conn.hasRttTextStream()) {
setRttStarted(conn);
}
}
/** Updates internal states when previous IMS calls are accepted to track MT call setup time. */
public synchronized void onImsAcceptCall(List<Connection> connections) {
for (Connection conn : connections) {
addCall(conn);
}
}
/** Updates internal states when an IMS call is terminated. */
public synchronized void onImsCallTerminated(
@Nullable ImsPhoneConnection conn, ImsReasonInfo reasonInfo) {
if (conn == null) {
List<Integer> imsConnIds = getImsConnectionIds();
if (imsConnIds.size() == 1) {
loge("onImsCallTerminated: ending IMS call w/ conn=null");
finishImsCall(imsConnIds.get(0), reasonInfo);
} else {
loge("onImsCallTerminated: %d IMS calls w/ conn=null", imsConnIds.size());
}
} else {
int id = getConnectionId(conn);
if (mCallProtos.contains(id)) {
finishImsCall(id, reasonInfo);
} else {
loge("onImsCallTerminated: untracked connection");
// fake a call so at least some info can be tracked
addCall(conn);
finishImsCall(id, reasonInfo);
}
}
}
/** Updates internal states when RTT is started on an IMS call. */
public synchronized void onRttStarted(ImsPhoneConnection conn) {
setRttStarted(conn);
}
/* general & misc. */
/** Updates internal states when audio codec for a call is changed. */
public synchronized void onAudioCodecChanged(Connection conn, int audioQuality) {
int id = getConnectionId(conn);
VoiceCallSession proto = mCallProtos.get(id);
if (proto == null) {
loge("onAudioCodecChanged: untracked connection");
return;
}
int codec = audioQualityToCodec(proto.bearerAtEnd, audioQuality);
proto.codecBitmask |= (1L << codec);
if (mCodecUsage.contains(id)) {
mCodecUsage.get(id).append(getTimeMillis(), codec);
} else {
LongSparseArray<Integer> arr = new LongSparseArray<>();
arr.append(getTimeMillis(), codec);
mCodecUsage.put(id, arr);
}
}
/** Updates internal states when video state changes. */
public synchronized void onVideoStateChange(
ImsPhoneConnection conn, @VideoState int videoState) {
int id = getConnectionId(conn);
VoiceCallSession proto = mCallProtos.get(id);
if (proto == null) {
loge("onVideoStateChange: untracked connection");
return;
}
logd("Video state = " + videoState);
if (videoState != VideoProfile.STATE_AUDIO_ONLY) {
proto.videoEnabled = true;
}
}
/** Updates internal states when multiparty state changes. */
public synchronized void onMultipartyChange(ImsPhoneConnection conn, boolean isMultiParty) {
int id = getConnectionId(conn);
VoiceCallSession proto = mCallProtos.get(id);
if (proto == null) {
loge("onMultipartyChange: untracked connection");
return;
}
logd("Multiparty = " + isMultiParty);
if (isMultiParty) {
proto.isMultiparty = true;
}
}
/**
* Updates internal states when a call changes state to track setup time and status.
*
* <p>This is currently mainly used by IMS since CS call states are updated through {@link
* #onRilCallListChanged}.
*/
public synchronized void onCallStateChanged(Call call) {
for (Connection conn : call.getConnections()) {
VoiceCallSession proto = mCallProtos.get(getConnectionId(conn));
if (proto != null) {
checkCallSetup(conn, proto);
} else {
loge("onCallStateChanged: untracked connection");
}
}
}
/** Updates internal states when an IMS call is handover to a CS call. */
public synchronized void onRilSrvccStateChanged(int state) {
List<Connection> handoverConnections = null;
if (mPhone.getImsPhone() != null) {
loge("onRilSrvccStateChanged: ImsPhone is null");
} else {
handoverConnections = mPhone.getImsPhone().getHandoverConnection();
}
List<Integer> imsConnIds;
if (handoverConnections == null) {
imsConnIds = getImsConnectionIds();
loge("onRilSrvccStateChanged: ImsPhone has no handover, we have %d", imsConnIds.size());
} else {
imsConnIds =
handoverConnections.stream()
.map(VoiceCallSessionStats::getConnectionId)
.collect(Collectors.toList());
}
switch (state) {
case TelephonyManager.SRVCC_STATE_HANDOVER_COMPLETED:
// connection will now be CS
for (int id : imsConnIds) {
VoiceCallSession proto = mCallProtos.get(id);
proto.srvccCompleted = true;
proto.bearerAtEnd = VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
}
break;
case TelephonyManager.SRVCC_STATE_HANDOVER_FAILED:
for (int id : imsConnIds) {
mCallProtos.get(id).srvccFailureCount++;
}
break;
case TelephonyManager.SRVCC_STATE_HANDOVER_CANCELED:
for (int id : imsConnIds) {
mCallProtos.get(id).srvccCancellationCount++;
}
break;
default: // including STARTED and NONE, do nothing
}
}
/** Updates internal states when RAT changes. */
public synchronized void onServiceStateChanged(ServiceState state) {
if (hasCalls()) {
updateRatTracker(state);
}
}
/* internal */
/**
* Adds a call connection.
*
* <p>Should be called when the call is created, and when setup begins (upon {@code
* RilRequest.RIL_REQUEST_ANSWER} or {@code ImsCommand.IMS_CMD_ACCEPT}).
*/
private void addCall(Connection conn) {
int id = getConnectionId(conn);
if (mCallProtos.contains(id)) {
// mostly handles ringing MT call getting accepted (MT call setup begins)
logd("addCall: resetting setup info");
VoiceCallSession proto = mCallProtos.get(id);
proto.setupBeginMillis = getTimeMillis();
proto.setupDuration = VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
} else {
int bearer = getBearer(conn);
ServiceState serviceState = getServiceState();
@NetworkType int rat = getRat(serviceState);
VoiceCallSession proto = new VoiceCallSession();
proto.bearerAtStart = bearer;
proto.bearerAtEnd = bearer;
proto.direction = getDirection(conn);
proto.setupDuration = VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
proto.setupFailed = true;
proto.disconnectReasonCode = conn.getDisconnectCause();
proto.disconnectExtraCode = conn.getPreciseDisconnectCause();
proto.disconnectExtraMessage = conn.getVendorDisconnectCause();
proto.ratAtStart = rat;
proto.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
proto.ratAtEnd = rat;
proto.ratSwitchCount = 0L;
proto.codecBitmask = 0L;
proto.simSlotIndex = mPhoneId;
proto.isMultiSim = SimSlotState.isMultiSim();
proto.isEsim = SimSlotState.isEsim(mPhoneId);
proto.carrierId = mPhone.getCarrierId();
proto.srvccCompleted = false;
proto.srvccFailureCount = 0L;
proto.srvccCancellationCount = 0L;
proto.rttEnabled = false;
proto.isEmergency = conn.isEmergencyCall();
proto.isRoaming = serviceState != null ? serviceState.getVoiceRoaming() : false;
proto.isMultiparty = conn.isMultiparty();
// internal fields for tracking
proto.setupBeginMillis = getTimeMillis();
proto.concurrentCallCountAtStart = mCallProtos.size();
mCallProtos.put(id, proto);
// RAT call count needs to be updated
updateRatTracker(serviceState);
}
}
/** Sends the call metrics to persist storage when it is finished. */
private void finishCall(int connectionId) {
VoiceCallSession proto = mCallProtos.get(connectionId);
if (proto == null) {
loge("finishCall: could not find call to be removed");
return;
}
mCallProtos.delete(connectionId);
proto.concurrentCallCountAtEnd = mCallProtos.size();
// Calculate signal strength at the end of the call
proto.signalStrengthAtEnd = getSignalStrength(proto.ratAtEnd);
// Calculate main codec quality
proto.mainCodecQuality = finalizeMainCodecQuality(connectionId);
// ensure internal fields are cleared
proto.setupBeginMillis = 0L;
// sanitize for javanano & StatsEvent
if (proto.disconnectExtraMessage == null) {
proto.disconnectExtraMessage = "";
}
// Retry populating carrier ID if it was invalid
if (proto.carrierId <= 0) {
proto.carrierId = mPhone.getCarrierId();
}
mAtomsStorage.addVoiceCallSession(proto);
// merge RAT usages to PersistPullers when the call session ends (i.e. no more active calls)
if (!hasCalls()) {
mRatUsage.conclude(getTimeMillis());
mAtomsStorage.addVoiceCallRatUsage(mRatUsage);
mRatUsage.clear();
}
}
private void setRttStarted(ImsPhoneConnection conn) {
VoiceCallSession proto = mCallProtos.get(getConnectionId(conn));
if (proto == null) {
loge("onRttStarted: untracked connection");
return;
}
// should be IMS w/o SRVCC
if (proto.bearerAtStart != getBearer(conn) || proto.bearerAtEnd != getBearer(conn)) {
loge("onRttStarted: connection bearer mismatch but proceeding");
}
proto.rttEnabled = true;
}
/** Returns a {@link Set} of Connection IDs so RAT usage can be correctly tracked. */
private Set<Integer> getConnectionIds() {
Set<Integer> ids = new HashSet<>();
for (int i = 0; i < mCallProtos.size(); i++) {
ids.add(mCallProtos.keyAt(i));
}
return ids;
}
private List<Integer> getImsConnectionIds() {
List<Integer> imsConnIds = new ArrayList<>(mCallProtos.size());
for (int i = 0; i < mCallProtos.size(); i++) {
if (mCallProtos.valueAt(i).bearerAtEnd
== VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS) {
imsConnIds.add(mCallProtos.keyAt(i));
}
}
return imsConnIds;
}
private boolean hasCalls() {
return mCallProtos.size() > 0;
}
private void checkCallSetup(Connection conn, VoiceCallSession proto) {
if (proto.setupBeginMillis != 0L && isSetupFinished(conn.getCall())) {
proto.setupDurationMillis = (int) (getTimeMillis() - proto.setupBeginMillis);
proto.setupDuration = classifySetupDuration(proto.setupDurationMillis);
proto.setupBeginMillis = 0L;
}
// Clear setupFailed if call now active, but otherwise leave it unchanged
// This block is executed only once, when call becomes active for the first time.
if (proto.setupFailed && conn.getState() == Call.State.ACTIVE) {
proto.setupFailed = false;
// Track RAT when voice call is connected.
ServiceState serviceState = getServiceState();
proto.ratAtConnected = getRat(serviceState);
// Reset list of codecs with the last codec at the present time. In this way, we
// track codec quality only after call is connected and not while ringing.
resetCodecList(conn);
}
}
private void updateRatTracker(ServiceState state) {
@NetworkType int rat = getRat(state);
int band = ServiceStateStats.getBand(state, rat);
mRatUsage.add(mPhone.getCarrierId(), rat, getTimeMillis(), getConnectionIds());
for (int i = 0; i < mCallProtos.size(); i++) {
VoiceCallSession proto = mCallProtos.valueAt(i);
if (proto.ratAtEnd != rat) {
proto.ratSwitchCount++;
proto.ratAtEnd = rat;
}
proto.bandAtEnd = band;
// assuming that SIM carrier ID does not change during the call
}
}
private void finishImsCall(int id, ImsReasonInfo reasonInfo) {
VoiceCallSession proto = mCallProtos.get(id);
proto.bearerAtEnd = VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS;
proto.disconnectReasonCode = reasonInfo.mCode;
proto.disconnectExtraCode = reasonInfo.mExtraCode;
proto.disconnectExtraMessage = ImsStats.filterExtraMessage(reasonInfo.mExtraMessage);
finishCall(id);
}
private @Nullable ServiceState getServiceState() {
ServiceStateTracker tracker = mPhone.getServiceStateTracker();
return tracker != null ? tracker.getServiceState() : null;
}
private static int getDirection(Connection conn) {
return conn.isIncoming()
? VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MT
: VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MO;
}
private static int getBearer(Connection conn) {
int phoneType = conn.getPhoneType();
switch (phoneType) {
case PhoneConstants.PHONE_TYPE_GSM:
case PhoneConstants.PHONE_TYPE_CDMA:
return VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
case PhoneConstants.PHONE_TYPE_IMS:
return VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS;
default:
loge("getBearer: unknown phoneType=%d", phoneType);
return VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_UNKNOWN;
}
}
private @NetworkType int getRat(@Nullable ServiceState state) {
if (state == null) {
return TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
boolean isWifiCall =
mPhone.getImsPhone() != null
&& mPhone.getImsPhone().isWifiCallingEnabled()
&& state.getDataNetworkType() == TelephonyManager.NETWORK_TYPE_IWLAN;
return isWifiCall ? TelephonyManager.NETWORK_TYPE_IWLAN : state.getVoiceNetworkType();
}
/** Returns the signal strength. */
private int getSignalStrength(@NetworkType int rat) {
if (rat == TelephonyManager.NETWORK_TYPE_IWLAN) {
return getSignalStrengthWifi();
} else {
return getSignalStrengthCellular();
}
}
/** Returns the signal strength of WiFi. */
private int getSignalStrengthWifi() {
WifiManager wifiManager =
(WifiManager) mPhone.getContext().getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int result = VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
if (wifiInfo != null) {
int level = wifiManager.calculateSignalLevel(wifiInfo.getRssi());
int max = wifiManager.getMaxSignalLevel();
// Scale result into 0 to 4 range.
result = VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_GREAT
* level / max;
logd("WiFi level: " + result + " (" + level + "/" + max + ")");
}
return result;
}
/** Returns the signal strength of cellular RAT. */
private int getSignalStrengthCellular() {
return mPhone.getSignalStrength().getLevel();
}
/** Resets the list of codecs used for the connection with only the codec currently in use. */
private void resetCodecList(Connection conn) {
int id = getConnectionId(conn);
LongSparseArray<Integer> codecUsage = mCodecUsage.get(id);
if (codecUsage != null) {
int lastCodec = codecUsage.valueAt(codecUsage.size() - 1);
LongSparseArray<Integer> arr = new LongSparseArray<>();
arr.append(getTimeMillis(), lastCodec);
mCodecUsage.put(id, arr);
}
}
/** Returns the main codec quality used during the call. */
private int finalizeMainCodecQuality(int connectionId) {
// Retrieve information about codec usage for this call and remove it from main array.
if (!mCodecUsage.contains(connectionId)) {
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
}
LongSparseArray<Integer> codecUsage = mCodecUsage.get(connectionId);
mCodecUsage.delete(connectionId);
// Append fake entry at the end, to facilitate the calculation of time for each codec.
codecUsage.put(getTimeMillis(), AudioCodec.AUDIO_CODEC_UNKNOWN);
// Calculate array with time for each quality
int totalTime = 0;
long[] timePerQuality = new long[CODEC_QUALITY_COUNT];
for (int i = 0; i < codecUsage.size() - 1; i++) {
long time = codecUsage.keyAt(i + 1) - codecUsage.keyAt(i);
int quality = getCodecQuality(codecUsage.valueAt(i));
timePerQuality[quality] += time;
totalTime += time;
}
logd("Time per codec quality = " + Arrays.toString(timePerQuality));
// We calculate 70% duration of the call as the threshold for the main audio codec quality
// and iterate on all codec qualities. As soon as the sum of codec duration is greater than
// the threshold, we have identified the main codec quality.
long timeAtMinimumQuality = 0;
long timeThreshold = totalTime * MAIN_CODEC_QUALITY_THRESHOLD / 100;
for (int i = CODEC_QUALITY_COUNT - 1; i >= 0; i--) {
timeAtMinimumQuality += timePerQuality[i];
if (timeAtMinimumQuality >= timeThreshold) {
return i;
}
}
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
}
private int getCodecQuality(int codec) {
switch(codec) {
case AudioCodec.AUDIO_CODEC_AMR:
case AudioCodec.AUDIO_CODEC_QCELP13K:
case AudioCodec.AUDIO_CODEC_EVRC:
case AudioCodec.AUDIO_CODEC_EVRC_B:
case AudioCodec.AUDIO_CODEC_EVRC_NW:
case AudioCodec.AUDIO_CODEC_GSM_EFR:
case AudioCodec.AUDIO_CODEC_GSM_FR:
case AudioCodec.AUDIO_CODEC_GSM_HR:
case AudioCodec.AUDIO_CODEC_G711U:
case AudioCodec.AUDIO_CODEC_G723:
case AudioCodec.AUDIO_CODEC_G711A:
case AudioCodec.AUDIO_CODEC_G722:
case AudioCodec.AUDIO_CODEC_G711AB:
case AudioCodec.AUDIO_CODEC_G729:
case AudioCodec.AUDIO_CODEC_EVS_NB:
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
case AudioCodec.AUDIO_CODEC_AMR_WB:
case AudioCodec.AUDIO_CODEC_EVS_WB:
case AudioCodec.AUDIO_CODEC_EVRC_WB:
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_WIDEBAND;
case AudioCodec.AUDIO_CODEC_EVS_SWB:
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
case AudioCodec.AUDIO_CODEC_EVS_FB:
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_FULLBAND;
default:
return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
}
}
private static boolean isSetupFinished(@Nullable Call call) {
// NOTE: when setup is finished for MO calls, it is not successful yet.
if (call != null) {
switch (call.getState()) {
case ACTIVE: // MT setup: accepted to ACTIVE
case ALERTING: // MO setup: dial to ALERTING
return true;
default: // do nothing
}
}
return false;
}
private static int audioQualityToCodec(int bearer, int audioQuality) {
switch (bearer) {
case VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS:
return CS_CODEC_MAP.get(audioQuality, AudioCodec.AUDIO_CODEC_UNKNOWN);
case VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS:
return IMS_CODEC_MAP.get(audioQuality, AudioCodec.AUDIO_CODEC_UNKNOWN);
default:
loge("audioQualityToCodec: unknown bearer %d", bearer);
return AudioCodec.AUDIO_CODEC_UNKNOWN;
}
}
private static int classifySetupDuration(int durationMillis) {
// keys in CALL_SETUP_DURATION_MAP are upper bounds in ascending order
for (int i = 0; i < CALL_SETUP_DURATION_MAP.size(); i++) {
if (durationMillis < CALL_SETUP_DURATION_MAP.keyAt(i)) {
return CALL_SETUP_DURATION_MAP.valueAt(i);
}
}
return VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_SLOW;
}
/**
* Generates an ID for each connection, which should be the same for IMS and CS connections
* involved in the same SRVCC.
*
* <p>Among the fields copied from ImsPhoneConnection to GsmCdmaConnection during SRVCC, the
* Connection's create time seems to be the best choice for ID (assuming no multiple calls in a
* millisecond). The 64-bit time is truncated to 32-bit so it can be used as an index in various
* data structures, which is good for calls shorter than 49 days.
*/
private static int getConnectionId(Connection conn) {
return conn == null ? 0 : (int) conn.getCreateTime();
}
@VisibleForTesting
protected long getTimeMillis() {
return SystemClock.elapsedRealtime();
}
private static void logd(String format, Object... args) {
Rlog.d(TAG, String.format(format, args));
}
private static void loge(String format, Object... args) {
Rlog.e(TAG, String.format(format, args));
}
private static SparseIntArray buildGsmCdmaCodecMap() {
SparseIntArray map = new SparseIntArray();
map.put(DriverCall.AUDIO_QUALITY_AMR, AudioCodec.AUDIO_CODEC_AMR);
map.put(DriverCall.AUDIO_QUALITY_AMR_WB, AudioCodec.AUDIO_CODEC_AMR_WB);
map.put(DriverCall.AUDIO_QUALITY_GSM_EFR, AudioCodec.AUDIO_CODEC_GSM_EFR);
map.put(DriverCall.AUDIO_QUALITY_GSM_FR, AudioCodec.AUDIO_CODEC_GSM_FR);
map.put(DriverCall.AUDIO_QUALITY_GSM_HR, AudioCodec.AUDIO_CODEC_GSM_HR);
map.put(DriverCall.AUDIO_QUALITY_EVRC, AudioCodec.AUDIO_CODEC_EVRC);
map.put(DriverCall.AUDIO_QUALITY_EVRC_B, AudioCodec.AUDIO_CODEC_EVRC_B);
map.put(DriverCall.AUDIO_QUALITY_EVRC_WB, AudioCodec.AUDIO_CODEC_EVRC_WB);
map.put(DriverCall.AUDIO_QUALITY_EVRC_NW, AudioCodec.AUDIO_CODEC_EVRC_NW);
return map;
}
private static SparseIntArray buildImsCodecMap() {
SparseIntArray map = new SparseIntArray();
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_AMR, AudioCodec.AUDIO_CODEC_AMR);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB, AudioCodec.AUDIO_CODEC_AMR_WB);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_QCELP13K, AudioCodec.AUDIO_CODEC_QCELP13K);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC, AudioCodec.AUDIO_CODEC_EVRC);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_B, AudioCodec.AUDIO_CODEC_EVRC_B);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB, AudioCodec.AUDIO_CODEC_EVRC_WB);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_NW, AudioCodec.AUDIO_CODEC_EVRC_NW);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_EFR, AudioCodec.AUDIO_CODEC_GSM_EFR);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_FR, AudioCodec.AUDIO_CODEC_GSM_FR);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_HR, AudioCodec.AUDIO_CODEC_GSM_HR);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711U, AudioCodec.AUDIO_CODEC_G711U);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G723, AudioCodec.AUDIO_CODEC_G723);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711A, AudioCodec.AUDIO_CODEC_G711A);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G722, AudioCodec.AUDIO_CODEC_G722);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711AB, AudioCodec.AUDIO_CODEC_G711AB);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G729, AudioCodec.AUDIO_CODEC_G729);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_NB, AudioCodec.AUDIO_CODEC_EVS_NB);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB, AudioCodec.AUDIO_CODEC_EVS_WB);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB, AudioCodec.AUDIO_CODEC_EVS_SWB);
map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB, AudioCodec.AUDIO_CODEC_EVS_FB);
return map;
}
private static SparseIntArray buildCallSetupDurationMap() {
SparseIntArray map = new SparseIntArray();
map.put(
CALL_SETUP_DURATION_UNKNOWN,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN);
map.put(
CALL_SETUP_DURATION_EXTREMELY_FAST,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST);
map.put(
CALL_SETUP_DURATION_ULTRA_FAST,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST);
map.put(
CALL_SETUP_DURATION_VERY_FAST,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_FAST);
map.put(
CALL_SETUP_DURATION_FAST,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_FAST);
map.put(
CALL_SETUP_DURATION_NORMAL,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_NORMAL);
map.put(
CALL_SETUP_DURATION_SLOW,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_SLOW);
map.put(
CALL_SETUP_DURATION_VERY_SLOW,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_SLOW);
map.put(
CALL_SETUP_DURATION_ULTRA_SLOW,
VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_SLOW);
// anything above would be CALL_SETUP_DURATION_EXTREMELY_SLOW
return map;
}
}