| /* |
| * Copyright (C) 2022 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.telephony.qns; |
| |
| import android.annotation.NonNull; |
| import android.net.NetworkCapabilities; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.telephony.AccessNetworkConstants; |
| import android.telephony.Annotation; |
| import android.telephony.CallQuality; |
| import android.telephony.CallState; |
| import android.telephony.PreciseCallState; |
| import android.telephony.PreciseDataConnectionState; |
| import android.telephony.TelephonyManager; |
| import android.telephony.ims.ImsCallProfile; |
| import android.telephony.ims.MediaQualityStatus; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.function.Consumer; |
| |
| /** |
| * Tracking IMS Call status and update call type changed event to ANE. |
| */ |
| public class QnsCallStatusTracker { |
| private final String mLogTag; |
| private QnsTelephonyListener mTelephonyListener; |
| private QnsCarrierConfigManager mConfigManager; |
| private List<CallState> mCallStates = new ArrayList<>(); |
| private QnsRegistrant mCallTypeChangedEventListener; |
| private QnsRegistrant mEmergencyCallTypeChangedEventListener; |
| private int mLastNormalCallType = QnsConstants.CALL_TYPE_IDLE; |
| private int mLastEmergencyCallType = QnsConstants.CALL_TYPE_IDLE; |
| private boolean mEmergencyOverIms; |
| private ActiveCallTracker mActiveCallTracker; |
| private Consumer<List<CallState>> mCallStatesConsumer = |
| callStateList -> updateCallState(callStateList); |
| private Consumer<Integer> mSrvccStateConsumer = state -> onSrvccStateChangedInternal(state); |
| private Consumer<MediaQualityStatus> mMediaQualityStatusConsumer = |
| status -> mActiveCallTracker.onMediaQualityStatusChanged(status); |
| |
| static class CallQualityBlock { |
| int mUpLinkLevel; |
| int mDownLinkLevel; |
| long mCreatedElapsedTime; |
| long mDurationMillis; |
| CallQualityBlock(int uplinkLevel, int downLinkLevel, long createdElapsedTime) { |
| mUpLinkLevel = uplinkLevel; |
| mDownLinkLevel = downLinkLevel; |
| mCreatedElapsedTime = createdElapsedTime; |
| } |
| |
| long getUpLinkQualityVolume() { |
| if (mDurationMillis > 0) { |
| return mUpLinkLevel * mDurationMillis; |
| } else { |
| long now = QnsUtils.getSystemElapsedRealTime(); |
| return (now - mCreatedElapsedTime) * mUpLinkLevel; |
| } |
| } |
| |
| long getDownLinkQualityVolume() { |
| if (mDurationMillis > 0) { |
| return mDownLinkLevel * mDurationMillis; |
| } else { |
| long now = QnsUtils.getSystemElapsedRealTime(); |
| return (now - mCreatedElapsedTime) * mDownLinkLevel; |
| } |
| } |
| } |
| |
| class ActiveCallTracker { |
| private static final int EVENT_DATA_CONNECTION_STATUS_CHANGED = 3300; |
| |
| @QnsConstants.QnsCallType |
| private int mCallType = QnsConstants.CALL_TYPE_IDLE; |
| @Annotation.NetCapability |
| private int mNetCapability = QnsConstants.INVALID_VALUE; |
| private QnsRegistrantList mLowMediaQualityListeners = new QnsRegistrantList(); |
| private int mAccessNetwork = AccessNetworkConstants.AccessNetworkType.UNKNOWN; |
| private int mTransportType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID; |
| private SparseArray<CallQuality> mCallQualities = new SparseArray(); |
| private TransportQuality mCurrentQuality; |
| /** A list of TransportQuality for each Transport type */ |
| private SparseArray<List<TransportQuality>> mTransportQualityArray = new SparseArray<>(); |
| private boolean mWwanAvailable = false; |
| private boolean mWlanAvailable = false; |
| |
| private boolean mMediaThresholdBreached = false; |
| private HandlerThread mHandlerThread; |
| private ActiveCallTrackerHandler mActiveCallHandler; |
| private MediaLowQualityHandler mLowQualityHandler; |
| private String mLogTag; |
| |
| private class ActiveCallTrackerHandler extends Handler { |
| ActiveCallTrackerHandler(Looper l) { |
| super(l); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| QnsAsyncResult ar; |
| int transportType; |
| Log.d(mLogTag, "handleMessage : " + message.what); |
| switch (message.what) { |
| case EVENT_DATA_CONNECTION_STATUS_CHANGED: |
| ar = (QnsAsyncResult) message.obj; |
| onDataConnectionStatusChanged( |
| (PreciseDataConnectionState) ar.mResult); |
| break; |
| |
| default: |
| Log.d(mLogTag, "unHandleMessage : " + message.what); |
| break; |
| } |
| |
| } |
| } |
| |
| /** Tracking low quality status */ |
| private class MediaLowQualityHandler extends Handler { |
| private static final int EVENT_MEDIA_QUALITY_CHANGED = 3401; |
| private static final int EVENT_PACKET_LOSS_TIMER_EXPIRED = 3402; |
| private static final int EVENT_HYSTERESIS_FOR_NORMAL_QUALITY = 3403; |
| private static final int EVENT_POLLING_CHECK_LOW_QUALITY = 3404; |
| private static final int EVENT_LOW_QUALITY_HANDLER_MAX = 3405; |
| |
| private static final int STATE_NORMAL_QUALITY = 0; |
| private static final int STATE_SUSPECT_LOW_QUALITY = 1; |
| private static final int STATE_LOW_QUALITY = 2; |
| |
| private static final int HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS = 3000; |
| private static final int LOW_QUALITY_CHECK_INTERVAL_MILLIS = 15000; |
| private static final int LOW_QUALITY_CHECK_AFTER_HO_MILLIS = 3000; |
| private static final int LOW_QUALITY_REPORTED_TIME_INITIAL_VALUE = -1; |
| |
| private int mState = STATE_NORMAL_QUALITY; |
| private MediaQualityStatus mMediaQualityStatus; |
| private String mTag; |
| |
| MediaLowQualityHandler(Looper l) { |
| super(l); |
| mTag = mLogTag + "_LQH"; |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| Log.d(mTag, "handleMessage : " + message.what); |
| switch (message.what) { |
| case EVENT_MEDIA_QUALITY_CHANGED: |
| MediaQualityStatus status = (MediaQualityStatus) message.obj; |
| onMediaQualityChanged(status); |
| break; |
| |
| case EVENT_PACKET_LOSS_TIMER_EXPIRED: |
| onPacketLossTimerExpired(message.arg1); |
| break; |
| |
| case EVENT_HYSTERESIS_FOR_NORMAL_QUALITY: |
| exitLowQualityState(); |
| break; |
| |
| case EVENT_POLLING_CHECK_LOW_QUALITY: |
| checkLowQuality(); |
| break; |
| |
| default: |
| Log.d(mLogTag, "unHandleMessage : " + message.what); |
| break; |
| } |
| } |
| |
| private void onMediaQualityChanged(MediaQualityStatus status) { |
| Log.d(mTag, "onMediaQualityChanged " + status); |
| int reason = thresholdBreached(status); |
| boolean needNotify = false; |
| if (reason == 0) { |
| // Threshold not breached. |
| mMediaQualityStatus = status; |
| if (mState == STATE_NORMAL_QUALITY) { |
| Log.d(mTag, "keeps normal quality."); |
| mMediaQualityStatus = status; |
| return; |
| } else { |
| // check normal quality is stable or not. |
| this.sendEmptyMessageDelayed(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY, |
| HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS); |
| } |
| } else { |
| // Threshold breached. |
| this.removeMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY); |
| switch (mState) { |
| case STATE_NORMAL_QUALITY: |
| case STATE_SUSPECT_LOW_QUALITY: |
| if (reason == (1 << QnsConstants.RTP_LOW_QUALITY_REASON_PACKET_LOSS)) { |
| int delayMillis = (mConfigManager.getRTPMetricsData()).mPktLossTime; |
| if (delayMillis > 0) { |
| if (mState == STATE_NORMAL_QUALITY) { |
| enterSuspectLowQualityState(delayMillis); |
| } |
| } else if (delayMillis == 0) { |
| needNotify = true; |
| } |
| } else { |
| removeMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED); |
| enterLowQualityState(status); |
| needNotify = true; |
| } |
| break; |
| |
| case STATE_LOW_QUALITY: |
| if (mMediaQualityStatus.getTransportType() == status.getTransportType() |
| && thresholdBreached(mMediaQualityStatus) |
| != thresholdBreached(status)) { |
| needNotify = true; |
| } |
| break; |
| } |
| mMediaQualityStatus = status; |
| } |
| if (needNotify) { |
| enterLowQualityState(status); |
| notifyLowMediaQuality(reason); |
| } |
| |
| } |
| |
| @VisibleForTesting |
| void enterLowQualityState(MediaQualityStatus status) { |
| Log.d(mTag, "enterLowQualityState " + status); |
| mState = STATE_LOW_QUALITY; |
| this.sendEmptyMessageDelayed( |
| EVENT_POLLING_CHECK_LOW_QUALITY, LOW_QUALITY_CHECK_INTERVAL_MILLIS); |
| } |
| |
| void enterSuspectLowQualityState(int delayMillis) { |
| Log.d(mTag, "enterSuspectLowQualityState."); |
| if (!this.hasMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED)) { |
| this.removeMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED); |
| } |
| Log.d(mTag, "Packet loss timer start. " + delayMillis); |
| Message msg = this.obtainMessage( |
| EVENT_PACKET_LOSS_TIMER_EXPIRED, mTransportType, 0); |
| this.sendMessageDelayed(msg, delayMillis); |
| mState = STATE_SUSPECT_LOW_QUALITY; |
| } |
| |
| void exitLowQualityState() { |
| mState = STATE_NORMAL_QUALITY; |
| for (int i = EVENT_PACKET_LOSS_TIMER_EXPIRED; |
| i < EVENT_LOW_QUALITY_HANDLER_MAX; i++) { |
| this.removeMessages(i); |
| } |
| notifyLowMediaQuality(0); |
| } |
| |
| void checkLowQuality() { |
| if (mState == STATE_NORMAL_QUALITY) { |
| Log.w(mTag, "checkLowQuality on unexpected state(normal state)."); |
| } else { |
| Log.d(mTag, "checkLowQuality"); |
| int reason = thresholdBreached(mMediaQualityStatus); |
| if (reason > 0) { |
| notifyLowMediaQuality(thresholdBreached(mMediaQualityStatus)); |
| } else if (this.hasMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY)) { |
| // hysteresis time to be normal state is running. let's check after that. |
| this.sendEmptyMessageDelayed(EVENT_POLLING_CHECK_LOW_QUALITY, |
| HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS); |
| } else { |
| Log.w(mTag, "Unexpected case."); |
| } |
| } |
| } |
| |
| void updateForHandover(int transportType) { |
| // restart timers that they need to be restarted on new transport type. |
| if (mState == STATE_SUSPECT_LOW_QUALITY) { |
| this.removeMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED); |
| Message msg = this.obtainMessage( |
| EVENT_PACKET_LOSS_TIMER_EXPIRED, transportType, 0); |
| this.sendMessageDelayed(msg, (mConfigManager.getRTPMetricsData()).mPktLossTime); |
| } |
| if (this.hasMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY)) { |
| this.removeMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY); |
| this.sendEmptyMessageDelayed(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY, |
| HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS); |
| } |
| if (mState == STATE_LOW_QUALITY) { |
| this.removeMessages(EVENT_POLLING_CHECK_LOW_QUALITY); |
| this.sendEmptyMessageDelayed(EVENT_POLLING_CHECK_LOW_QUALITY, |
| LOW_QUALITY_CHECK_AFTER_HO_MILLIS); |
| } |
| } |
| |
| private void onPacketLossTimerExpired(int transportType) { |
| if (mTransportType != transportType) { |
| Log.d(mTag, "onPacketLossTimerExpired transport type mismatched."); |
| if (mState == STATE_SUSPECT_LOW_QUALITY) { |
| mState = STATE_NORMAL_QUALITY; |
| } |
| return; |
| } |
| if (thresholdBreached(mMediaQualityStatus) |
| == (1 << QnsConstants.RTP_LOW_QUALITY_REASON_PACKET_LOSS)) { |
| enterLowQualityState(mMediaQualityStatus); |
| notifyLowMediaQuality(1 << QnsConstants.RTP_LOW_QUALITY_REASON_PACKET_LOSS); |
| } |
| } |
| |
| private void notifyLowMediaQuality(int reason) { |
| long now = QnsUtils.getSystemElapsedRealTime(); |
| TransportQuality tq = getLastTransportQuality(mTransportType); |
| if (tq != null) { |
| if (reason > 0) { |
| tq.mLowRtpQualityReportedTime = now; |
| } else { |
| tq.mLowRtpQualityReportedTime = LOW_QUALITY_REPORTED_TIME_INITIAL_VALUE; |
| } |
| } |
| Log.d(mTag, "notifyLowMediaQuality reason:" + reason + " transport type:" |
| + QnsConstants.transportTypeToString(mTransportType)); |
| mLowMediaQualityListeners.notifyResult(reason); |
| } |
| } |
| |
| class TransportQuality { |
| int mTransportType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID; |
| long mLowRtpQualityReportedTime = |
| MediaLowQualityHandler.LOW_QUALITY_REPORTED_TIME_INITIAL_VALUE; |
| List<CallQualityBlock> mCallQualityBlockList; |
| |
| TransportQuality(int transportType) { |
| mTransportType = transportType; |
| mCallQualityBlockList = new ArrayList<>(); |
| } |
| |
| boolean isLowRtpQualityReported() { |
| return mLowRtpQualityReportedTime |
| != MediaLowQualityHandler.LOW_QUALITY_REPORTED_TIME_INITIAL_VALUE; |
| } |
| |
| CallQualityBlock getLastCallQualityBlock() { |
| int length = mCallQualityBlockList.size(); |
| if (length > 0) { |
| return mCallQualityBlockList.get(length - 1); |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| ActiveCallTracker(int slotIndex, Looper looper) { |
| mLogTag = ActiveCallTracker.class.getSimpleName() + "_" + slotIndex; |
| if (looper == null) { |
| mHandlerThread = new HandlerThread(ActiveCallTracker.class.getSimpleName()); |
| mHandlerThread.start(); |
| mActiveCallHandler = new ActiveCallTrackerHandler(mHandlerThread.getLooper()); |
| mLowQualityHandler = new MediaLowQualityHandler(mHandlerThread.getLooper()); |
| } else { |
| mActiveCallHandler = new ActiveCallTrackerHandler(looper); |
| mLowQualityHandler = new MediaLowQualityHandler(looper); |
| } |
| mTelephonyListener.addMediaQualityStatusCallback(mMediaQualityStatusConsumer); |
| mTransportQualityArray.put( |
| AccessNetworkConstants.TRANSPORT_TYPE_WLAN, new ArrayList<>()); |
| mTransportQualityArray.put( |
| AccessNetworkConstants.TRANSPORT_TYPE_WWAN, new ArrayList<>()); |
| } |
| |
| void close() { |
| mTelephonyListener.removeMediaQualityStatusCallback(mMediaQualityStatusConsumer); |
| mTelephonyListener.unregisterPreciseDataConnectionStateChanged( |
| mNetCapability, mActiveCallHandler); |
| if (mHandlerThread != null) { |
| mHandlerThread.quitSafely(); |
| } |
| } |
| |
| @VisibleForTesting |
| void onDataConnectionStatusChanged(PreciseDataConnectionState state) { |
| if (state == null) { |
| Log.d(mLogTag, "onDataConnectionStatusChanged with null info"); |
| return; |
| } |
| if (state.getState() == TelephonyManager.DATA_CONNECTED) { |
| int transportType = state.getTransportType(); |
| if (transportType == AccessNetworkConstants.TRANSPORT_TYPE_INVALID) { |
| Log.w(mLogTag, "Unexpected transport type on connected DataNetwork."); |
| return; |
| } |
| if (mTransportType == AccessNetworkConstants.TRANSPORT_TYPE_INVALID) { |
| Log.d(mLogTag, "Call started with " |
| + QnsConstants.transportTypeToString(transportType)); |
| mTransportType = transportType; |
| startTrackingTransportQuality(transportType); |
| } else if (mTransportType != transportType) { |
| Log.d(mLogTag, "Call Handed over to " |
| + QnsConstants.transportTypeToString(transportType)); |
| mTransportType = transportType; |
| onHandoverCompleted(transportType); |
| } |
| } |
| } |
| |
| private void onHandoverCompleted( |
| @AccessNetworkConstants.TransportType int dstTransportType) { |
| long now = QnsUtils.getSystemElapsedRealTime(); |
| // complete to update TransportQuality for prev transport type |
| CallQualityBlock last = null; |
| int prevTransportType = QnsUtils.getOtherTransportType(dstTransportType); |
| TransportQuality prev = getLastTransportQuality(prevTransportType); |
| if (prev != null) { |
| last = prev.getLastCallQualityBlock(); |
| } |
| // add a new TransportQuality for new transport type |
| mTransportQualityArray.get(dstTransportType) |
| .add(new TransportQuality(dstTransportType)); |
| TransportQuality current = getLastTransportQuality(dstTransportType); |
| if (last != null) { |
| last.mDurationMillis = now - last.mCreatedElapsedTime; |
| current.mCallQualityBlockList |
| .add(new CallQualityBlock(last.mUpLinkLevel, last.mDownLinkLevel, now)); |
| } |
| mLowQualityHandler.updateForHandover(dstTransportType); |
| } |
| |
| private void startTrackingTransportQuality(int transportType) { |
| mTransportQualityArray.get(AccessNetworkConstants.TRANSPORT_TYPE_WLAN).clear(); |
| mTransportQualityArray.get(AccessNetworkConstants.TRANSPORT_TYPE_WWAN).clear(); |
| mTransportQualityArray.get(transportType) |
| .add(new TransportQuality(transportType)); |
| } |
| |
| void callStarted(@QnsConstants.QnsCallType int callType, int netCapability) { |
| if (mCallType != QnsConstants.CALL_TYPE_IDLE) { |
| if (mCallType != callType) { |
| callTypeUpdated(callType); |
| } else { |
| Log.w(mLogTag, "call type:" + callType + " already started."); |
| } |
| } |
| Log.d(mLogTag, "callStarted callType: " + callType + " netCapa:" |
| + QnsUtils.getNameOfNetCapability(netCapability)); |
| mCallType = callType; |
| mNetCapability = netCapability; |
| //Transport type will be updated when EVENT_DATA_CONNECTION_STATUS_CHANGED occurs. |
| PreciseDataConnectionState dataState = |
| mTelephonyListener.getLastPreciseDataConnectionState(netCapability); |
| if (dataState != null && dataState.getTransportType() |
| != AccessNetworkConstants.TRANSPORT_TYPE_INVALID) { |
| mTransportType = dataState.getTransportType(); |
| startTrackingTransportQuality(mTransportType); |
| } |
| mTelephonyListener.registerPreciseDataConnectionStateChanged(mNetCapability, |
| mActiveCallHandler, EVENT_DATA_CONNECTION_STATUS_CHANGED, null, true); |
| } |
| |
| private void callTypeUpdated(@QnsConstants.QnsCallType int callType) { |
| Log.d(mLogTag, "callTypeUpdated from " + mCallType + " to " + callType); |
| mCallType = callType; |
| } |
| |
| void callEnded() { |
| mLowQualityHandler.exitLowQualityState(); |
| long now = QnsUtils.getSystemElapsedRealTime(); |
| // complete to update TransportQuality for prev transport type |
| CallQualityBlock last = null; |
| TransportQuality prev = getLastTransportQuality(mTransportType); |
| if (prev != null) { |
| last = prev.getLastCallQualityBlock(); |
| } |
| if (last != null) { |
| last.mDurationMillis = now - last.mCreatedElapsedTime; |
| } |
| long upLinkQualityOverWwan = mActiveCallTracker |
| .getUpLinkQualityLevelDuringCall(AccessNetworkConstants.TRANSPORT_TYPE_WWAN); |
| long upLinkQualityOverWlan = mActiveCallTracker |
| .getUpLinkQualityLevelDuringCall(AccessNetworkConstants.TRANSPORT_TYPE_WLAN); |
| long downLinkQualityOverWwan = mActiveCallTracker |
| .getDownLinkQualityLevelDuringCall(AccessNetworkConstants.TRANSPORT_TYPE_WWAN); |
| long downLinkQualityOverWlan = mActiveCallTracker |
| .getDownLinkQualityLevelDuringCall(AccessNetworkConstants.TRANSPORT_TYPE_WLAN); |
| StringBuilder sb = new StringBuilder(); |
| sb.append("CallQuality [WWAN:"); |
| if (upLinkQualityOverWwan == QnsConstants.INVALID_VALUE |
| || downLinkQualityOverWwan == QnsConstants.INVALID_VALUE) { |
| sb.append("Not available] "); |
| } else { |
| sb.append("upLinkQualityOverWwan = ").append(upLinkQualityOverWwan) |
| .append(", downLinkQualityOverWwan = ").append(downLinkQualityOverWwan) |
| .append("] "); |
| } |
| sb.append("[WLAN:"); |
| if (upLinkQualityOverWlan == QnsConstants.INVALID_VALUE |
| || downLinkQualityOverWlan == QnsConstants.INVALID_VALUE) { |
| sb.append("Not available] "); |
| } else { |
| sb.append("upLinkQualityOverWlan = ").append(upLinkQualityOverWwan) |
| .append(", downLinkQualityOverWlan = ").append(downLinkQualityOverWwan) |
| .append("] "); |
| } |
| Log.d(mLogTag, "callEnded callType: " + mCallType + " netCapa:" |
| + QnsUtils.getNameOfNetCapability(mNetCapability) + " " + sb.toString()); |
| mCallType = QnsConstants.CALL_TYPE_IDLE; |
| mNetCapability = 0; |
| mAccessNetwork = AccessNetworkConstants.AccessNetworkType.UNKNOWN; |
| mTransportType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID; |
| mTelephonyListener.unregisterPreciseDataConnectionStateChanged( |
| mNetCapability, mActiveCallHandler); |
| } |
| |
| void onMediaQualityStatusChanged(MediaQualityStatus status) { |
| if (status == null) { |
| Log.e(mLogTag, "null MediaQualityStatus received."); |
| return; |
| } |
| Message msg = mLowQualityHandler |
| .obtainMessage(MediaLowQualityHandler.EVENT_MEDIA_QUALITY_CHANGED, status); |
| mLowQualityHandler.sendMessage(msg); |
| } |
| |
| int getTransportType() { |
| return this.mTransportType; |
| } |
| |
| int getCallType() { |
| return this.mCallType; |
| } |
| |
| int getNetCapability() { |
| return this.mNetCapability; |
| } |
| |
| @VisibleForTesting |
| TransportQuality getLastTransportQuality(int transportType) { |
| if (transportType == AccessNetworkConstants.TRANSPORT_TYPE_INVALID) { |
| Log.w(mLogTag, "getLastTransportQuality with invalid transport type."); |
| return null; |
| } |
| int size = mTransportQualityArray.get(transportType).size(); |
| if (size > 0) { |
| return mTransportQualityArray.get(transportType).get(size - 1); |
| } else { |
| return null; |
| } |
| } |
| |
| @VisibleForTesting |
| List<TransportQuality> getTransportQualityList(int transportType) { |
| return mTransportQualityArray.get(transportType); |
| } |
| |
| long getUpLinkQualityLevelDuringCall(int transportType) { |
| List<TransportQuality> tqList = getTransportQualityList(transportType); |
| long sumUplinkQualityLevelVolume = 0; |
| long totalDuration = 0; |
| for (int i = 0; i < tqList.size(); i++) { |
| List<CallQualityBlock> callQualityBlockList = tqList.get(i).mCallQualityBlockList; |
| for (int j = 0; j < callQualityBlockList.size(); j++) { |
| CallQualityBlock cq = callQualityBlockList.get(j); |
| sumUplinkQualityLevelVolume += cq.getUpLinkQualityVolume(); |
| long durationMillis = cq.mDurationMillis; |
| if (i == tqList.size() - 1 && j == callQualityBlockList.size() - 1) { |
| if (durationMillis == 0) { |
| durationMillis = QnsUtils.getSystemElapsedRealTime() |
| - cq.mCreatedElapsedTime; |
| } |
| } |
| if (durationMillis > 0) { |
| totalDuration += durationMillis; |
| } else { |
| return -1; |
| } |
| } |
| } |
| if (totalDuration <= 0) { |
| return QnsConstants.INVALID_VALUE; |
| } |
| long qualityLevel = sumUplinkQualityLevelVolume / totalDuration; |
| Log.d(mLogTag, "getUplinkQualityLevel for [" + QnsConstants |
| .transportTypeToString(transportType) + "] totalQualityVolume: " |
| + sumUplinkQualityLevelVolume + ", totalDuration: " + totalDuration |
| + " level:" + qualityLevel); |
| return qualityLevel; |
| } |
| |
| long getDownLinkQualityLevelDuringCall(int transportType) { |
| List<TransportQuality> tqList = getTransportQualityList(transportType); |
| long sumDownLinkQualityLevelVolume = 0; |
| long totalDuration = 0; |
| for (int i = 0; i < tqList.size(); i++) { |
| List<CallQualityBlock> callQualityBlockList = tqList.get(i).mCallQualityBlockList; |
| for (int j = 0; j < callQualityBlockList.size(); j++) { |
| CallQualityBlock cq = callQualityBlockList.get(j); |
| sumDownLinkQualityLevelVolume += cq.getDownLinkQualityVolume(); |
| long durationMillis = cq.mDurationMillis; |
| if (i == tqList.size() - 1 && j == callQualityBlockList.size() - 1) { |
| if (durationMillis == 0) { |
| durationMillis = QnsUtils.getSystemElapsedRealTime() |
| - cq.mCreatedElapsedTime; |
| } |
| } |
| if (durationMillis > 0) { |
| totalDuration += durationMillis; |
| } else { |
| return QnsConstants.INVALID_VALUE; |
| } |
| } |
| } |
| if (totalDuration <= 0) { |
| return QnsConstants.INVALID_VALUE; |
| } |
| long qualityLevel = sumDownLinkQualityLevelVolume / totalDuration; |
| Log.d(mLogTag, "getDownLinkQualityLevel for [" + AccessNetworkConstants |
| .transportTypeToString(transportType) + "] totalQualityVolume: " |
| + sumDownLinkQualityLevelVolume + ", totalDuration: " + totalDuration |
| + " level:" + qualityLevel); |
| return qualityLevel; |
| } |
| |
| void updateCallQuality(CallState state) { |
| if (state == null) { |
| Log.w(mLogTag, "updateCallQuality Null CallState."); |
| return; |
| } |
| CallQuality cq = state.getCallQuality(); |
| if (cq == null || isDummyCallQuality(cq)) { |
| return; |
| } |
| mActiveCallHandler.post(() -> onUpdateCallQuality(cq)); |
| } |
| |
| private void onUpdateCallQuality(CallQuality cq) { |
| TransportQuality transportQuality = getLastTransportQuality(mTransportType); |
| if (transportQuality != null) { |
| long now = QnsUtils.getSystemElapsedRealTime(); |
| CallQualityBlock prev = transportQuality.getLastCallQualityBlock(); |
| if (prev != null) { |
| prev.mDurationMillis = now - prev.mCreatedElapsedTime; |
| } |
| transportQuality.mCallQualityBlockList.add( |
| new CallQualityBlock( |
| cq.getUplinkCallQualityLevel(), cq.getDownlinkCallQualityLevel(), |
| now)); |
| } |
| } |
| |
| private boolean isDummyCallQuality(CallQuality cq) { |
| return (cq.getNumRtpPacketsTransmitted() == 0 |
| && cq.getNumRtpPacketsReceived() == 0 |
| && cq.getUplinkCallQualityLevel() == 0 |
| && cq.getDownlinkCallQualityLevel() == 0); |
| } |
| /** |
| * Register an event for low media quality report. |
| * |
| * @param h the Handler to get event. |
| * @param what the event. |
| * @param userObj user object. |
| */ |
| void registerLowMediaQualityListener( |
| Handler h, int what, Object userObj) { |
| Log.d(mLogTag, "registerLowMediaQualityListener"); |
| if (h != null) { |
| QnsRegistrant r = new QnsRegistrant(h, what, userObj); |
| mLowMediaQualityListeners.add(r); |
| } |
| } |
| |
| /** |
| * Unregister an event for low media quality report. |
| * |
| * @param h the handler to get event. |
| */ |
| void unregisterLowMediaQualityListener(Handler h) { |
| if (h != null) { |
| mLowMediaQualityListeners.remove(h); |
| } |
| } |
| |
| int thresholdBreached(MediaQualityStatus status) { |
| int breachedReason = 0; |
| QnsCarrierConfigManager.RtpMetricsConfig rtpConfig = mConfigManager.getRTPMetricsData(); |
| if (status.getRtpPacketLossRate() > 0 |
| && status.getRtpPacketLossRate() > rtpConfig.mPktLossRate) { |
| breachedReason |= 1 << QnsConstants.RTP_LOW_QUALITY_REASON_PACKET_LOSS; |
| } |
| if (status.getRtpJitterMillis() > 0 |
| && status.getRtpJitterMillis() > rtpConfig.mJitter) { |
| breachedReason |= 1 << QnsConstants.RTP_LOW_QUALITY_REASON_JITTER; |
| } |
| if (status.getRtpInactivityMillis() > 0 |
| && status.getRtpInactivityMillis() > rtpConfig.mNoRtpInterval) { |
| breachedReason |= 1 << QnsConstants.RTP_LOW_QUALITY_REASON_NO_RTP; |
| } |
| return breachedReason; |
| } |
| |
| boolean worseThanBefore(MediaQualityStatus before, MediaQualityStatus now) { |
| return thresholdBreached(now) > thresholdBreached(before); |
| } |
| } |
| |
| QnsCallStatusTracker(QnsTelephonyListener telephonyListener, |
| QnsCarrierConfigManager configManager, int slotIndex) { |
| this(telephonyListener, configManager, slotIndex, null); |
| } |
| |
| /** Only for test */ |
| @VisibleForTesting |
| QnsCallStatusTracker(QnsTelephonyListener telephonyListener, |
| QnsCarrierConfigManager configManager, int slotIndex, Looper looper) { |
| mLogTag = QnsCallStatusTracker.class.getSimpleName() + "_" + slotIndex; |
| mTelephonyListener = telephonyListener; |
| mConfigManager = configManager; |
| mActiveCallTracker = new ActiveCallTracker(slotIndex, looper); |
| mTelephonyListener.addCallStatesChangedCallback(mCallStatesConsumer); |
| mTelephonyListener.addSrvccStateChangedCallback(mSrvccStateConsumer); |
| } |
| |
| void close() { |
| mTelephonyListener.removeCallStatesChangedCallback(mCallStatesConsumer); |
| mTelephonyListener.removeSrvccStateChangedCallback(mSrvccStateConsumer); |
| if (mActiveCallTracker != null) { |
| mActiveCallTracker.close(); |
| } |
| } |
| |
| void updateCallState(List<CallState> callStateList) { |
| List<CallState> imsCallStateList = new ArrayList<>(); |
| StringBuilder sb = new StringBuilder(""); |
| |
| if (callStateList.size() > 0) { |
| for (CallState cs : callStateList) { |
| if (cs.getImsCallServiceType() != ImsCallProfile.SERVICE_TYPE_NONE |
| || cs.getImsCallType() != ImsCallProfile.CALL_TYPE_NONE) { |
| if (cs.getCallState() != PreciseCallState.PRECISE_CALL_STATE_DISCONNECTED) { |
| imsCallStateList.add(cs); |
| sb.append("{" + cs + "}"); |
| } |
| } |
| } |
| } |
| int ongoingCallNum = imsCallStateList.size(); |
| mCallStates = imsCallStateList; |
| Log.d(mLogTag, "updateCallState callNum:(" + ongoingCallNum + "): [" + sb + "]"); |
| if (imsCallStateList.size() == 0) { |
| if (mLastNormalCallType != QnsConstants.CALL_TYPE_IDLE) { |
| mLastNormalCallType = QnsConstants.CALL_TYPE_IDLE; |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastNormalCallType); |
| } |
| if (mLastEmergencyCallType != QnsConstants.CALL_TYPE_IDLE) { |
| mLastEmergencyCallType = QnsConstants.CALL_TYPE_IDLE; |
| if (mEmergencyOverIms) { |
| mEmergencyOverIms = false; |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastEmergencyCallType); |
| } else { |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_EIMS, mLastEmergencyCallType); |
| } |
| } |
| } else { |
| //1. Notify Call Type IDLE, if the call was removed from the call list. |
| if (mLastNormalCallType != QnsConstants.CALL_TYPE_IDLE |
| && !hasVideoCall() && !hasVoiceCall()) { |
| mLastNormalCallType = QnsConstants.CALL_TYPE_IDLE; |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastNormalCallType); |
| |
| } |
| if (mLastEmergencyCallType != QnsConstants.CALL_TYPE_IDLE && !hasEmergencyCall()) { |
| mLastEmergencyCallType = QnsConstants.CALL_TYPE_IDLE; |
| if (mEmergencyOverIms) { |
| mEmergencyOverIms = false; |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastEmergencyCallType); |
| } else { |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_EIMS, mLastEmergencyCallType); |
| } |
| } |
| //2. Notify a new ongoing call type |
| if (hasEmergencyCall() && mLastEmergencyCallType != QnsConstants.CALL_TYPE_EMERGENCY) { |
| mLastEmergencyCallType = QnsConstants.CALL_TYPE_EMERGENCY; |
| if (!isDataNetworkConnected(NetworkCapabilities.NET_CAPABILITY_EIMS) |
| && isDataNetworkConnected(NetworkCapabilities.NET_CAPABILITY_IMS)) { |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastEmergencyCallType); |
| mEmergencyOverIms = true; |
| } else { |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_EIMS, mLastEmergencyCallType); |
| } |
| } else if (hasVideoCall()) { |
| if (mLastNormalCallType != QnsConstants.CALL_TYPE_VIDEO) { |
| mLastNormalCallType = QnsConstants.CALL_TYPE_VIDEO; |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastNormalCallType); |
| } |
| } else if (hasVoiceCall()) { |
| if (mLastNormalCallType != QnsConstants.CALL_TYPE_VOICE) { |
| mLastNormalCallType = QnsConstants.CALL_TYPE_VOICE; |
| notifyCallType(NetworkCapabilities.NET_CAPABILITY_IMS, mLastNormalCallType); |
| } |
| } |
| if (mActiveCallTracker.getCallType() != QnsConstants.CALL_TYPE_IDLE) { |
| mActiveCallTracker.updateCallQuality(getActiveCall()); |
| } |
| } |
| } |
| |
| private void notifyCallType(int netCapability, int callType) { |
| Log.d(mLogTag, "notifyCallType:" + netCapability + ", callType:" + callType); |
| if (netCapability == NetworkCapabilities.NET_CAPABILITY_IMS |
| && mCallTypeChangedEventListener != null) { |
| mCallTypeChangedEventListener.notifyResult(callType); |
| } else if (netCapability == NetworkCapabilities.NET_CAPABILITY_EIMS |
| && mEmergencyCallTypeChangedEventListener != null) { |
| mEmergencyCallTypeChangedEventListener.notifyResult(callType); |
| } |
| if (callType == QnsConstants.CALL_TYPE_IDLE) { |
| mActiveCallTracker.callEnded(); |
| } else { |
| mActiveCallTracker.callStarted(callType, netCapability); |
| } |
| } |
| |
| boolean isCallIdle() { |
| return mCallStates.size() == 0; |
| } |
| |
| boolean hasEmergencyCall() { |
| for (CallState cs : mCallStates) { |
| if (cs.getImsCallServiceType() == ImsCallProfile.SERVICE_TYPE_EMERGENCY) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| CallState getActiveCall() { |
| for (CallState cs : mCallStates) { |
| if (cs.getCallState() == PreciseCallState.PRECISE_CALL_STATE_ACTIVE) { |
| return cs; |
| } |
| } |
| return null; |
| } |
| |
| boolean hasVideoCall() { |
| for (CallState cs : mCallStates) { |
| if (cs.getImsCallServiceType() == ImsCallProfile.SERVICE_TYPE_NORMAL |
| && cs.getImsCallType() == ImsCallProfile.CALL_TYPE_VT |
| && (cs.getCallState() != PreciseCallState.PRECISE_CALL_STATE_ALERTING |
| && cs.getCallState() != PreciseCallState.PRECISE_CALL_STATE_DIALING |
| && cs.getCallState() != PreciseCallState.PRECISE_CALL_STATE_INCOMING)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| boolean hasVoiceCall() { |
| for (CallState cs : mCallStates) { |
| if (cs.getImsCallServiceType() == ImsCallProfile.SERVICE_TYPE_NORMAL |
| && cs.getImsCallType() == ImsCallProfile.CALL_TYPE_VOICE) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * register call type changed event. |
| * |
| * @param netCapability Network Capability of caller |
| * @param h Handler want to receive event. |
| * @param what event Id to receive |
| * @param userObj user object |
| */ |
| void registerCallTypeChangedListener( |
| int netCapability, @NonNull Handler h, int what, Object userObj) { |
| if (netCapability != NetworkCapabilities.NET_CAPABILITY_IMS |
| && netCapability != NetworkCapabilities.NET_CAPABILITY_EIMS) { |
| Log.d(mLogTag, "registerCallTypeChangedListener : wrong netCapability"); |
| return; |
| } |
| if (h != null) { |
| QnsRegistrant r = new QnsRegistrant(h, what, userObj); |
| if (netCapability == NetworkCapabilities.NET_CAPABILITY_IMS) { |
| mCallTypeChangedEventListener = r; |
| } else if (netCapability == NetworkCapabilities.NET_CAPABILITY_EIMS) { |
| mEmergencyCallTypeChangedEventListener = r; |
| } |
| } else { |
| Log.d(mLogTag, "registerCallTypeChangedListener : Handler is Null"); |
| } |
| } |
| |
| /** |
| * Unregister call type changed event. |
| * |
| * @param netCapability Network Capability of caller |
| * @param h Handler want to receive event. |
| */ |
| void unregisterCallTypeChangedListener(int netCapability, @NonNull Handler h) { |
| if (netCapability != NetworkCapabilities.NET_CAPABILITY_IMS |
| && netCapability != NetworkCapabilities.NET_CAPABILITY_EIMS) { |
| Log.d(mLogTag, "unregisterCallTypeChangedListener : wrong netCapability"); |
| return; |
| } |
| if (h != null) { |
| if (netCapability == NetworkCapabilities.NET_CAPABILITY_IMS) { |
| mCallTypeChangedEventListener = null; |
| } else if (netCapability == NetworkCapabilities.NET_CAPABILITY_EIMS) { |
| mEmergencyCallTypeChangedEventListener = null; |
| } |
| } else { |
| Log.d(mLogTag, "unregisterCallTypeChangedListener : Handler is Null"); |
| } |
| } |
| |
| ActiveCallTracker getActiveCallTracker() { |
| return mActiveCallTracker; |
| } |
| |
| @VisibleForTesting |
| void onSrvccStateChangedInternal(int srvccState) { |
| if (srvccState == TelephonyManager.SRVCC_STATE_HANDOVER_COMPLETED) { |
| mCallStates.clear(); |
| if (mLastNormalCallType != QnsConstants.CALL_TYPE_IDLE) { |
| mLastNormalCallType = QnsConstants.CALL_TYPE_IDLE; |
| if (mCallTypeChangedEventListener != null) { |
| mCallTypeChangedEventListener.notifyResult(mLastNormalCallType); |
| } |
| } |
| if (mLastEmergencyCallType != QnsConstants.CALL_TYPE_IDLE) { |
| mLastEmergencyCallType = QnsConstants.CALL_TYPE_IDLE; |
| if (mEmergencyOverIms) { |
| mEmergencyOverIms = false; |
| if (mCallTypeChangedEventListener != null) { |
| mCallTypeChangedEventListener.notifyResult(mLastEmergencyCallType); |
| } |
| } else { |
| if (mEmergencyCallTypeChangedEventListener != null) { |
| mEmergencyCallTypeChangedEventListener.notifyResult(mLastEmergencyCallType); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| private boolean isDataNetworkConnected(int netCapability) { |
| PreciseDataConnectionState preciseDataStatus = |
| mTelephonyListener.getLastPreciseDataConnectionState(netCapability); |
| |
| if (preciseDataStatus == null) return false; |
| int state = preciseDataStatus.getState(); |
| return (state == TelephonyManager.DATA_CONNECTED |
| || state == TelephonyManager.DATA_HANDOVER_IN_PROGRESS |
| || state == TelephonyManager.DATA_SUSPENDED); |
| } |
| } |