blob: 833cf86986f76f6fdc7e7ef51a421e8e99258ca7 [file] [log] [blame]
/*
* Copyright (C) 2021 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.dataconnection;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.SharedPreferences;
import android.hardware.display.DisplayManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Message;
import android.os.OutcomeReceiver;
import android.os.Registrant;
import android.os.RegistrantList;
import android.preference.PreferenceManager;
import android.telephony.AccessNetworkConstants;
import android.telephony.CellIdentity;
import android.telephony.CellIdentityGsm;
import android.telephony.CellIdentityLte;
import android.telephony.CellIdentityNr;
import android.telephony.CellIdentityTdscdma;
import android.telephony.CellIdentityWcdma;
import android.telephony.ModemActivityInfo;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.LocalLog;
import android.util.Pair;
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.DctConstants;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.TelephonyFacade;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.nano.TelephonyProto.NrMode;
import com.android.internal.util.IndentingPrintWriter;
import com.android.telephony.Rlog;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Objects;
/**
* Link Bandwidth Estimator based on the byte counts in TrafficStats and the time reported in modem
* activity.
*/
public class LinkBandwidthEstimator extends Handler {
private static final String TAG = LinkBandwidthEstimator.class.getSimpleName();
private static final boolean DBG = false;
@VisibleForTesting
static final int MSG_SCREEN_STATE_CHANGED = 1;
@VisibleForTesting
static final int MSG_TRAFFIC_STATS_POLL = 2;
@VisibleForTesting
static final int MSG_MODEM_ACTIVITY_RETURNED = 3;
@VisibleForTesting
static final int MSG_DEFAULT_NETWORK_CHANGED = 4;
@VisibleForTesting
static final int MSG_SIGNAL_STRENGTH_CHANGED = 5;
@VisibleForTesting
static final int MSG_NR_FREQUENCY_CHANGED = 6;
@VisibleForTesting
static final int MSG_NR_STATE_CHANGED = 7;
@VisibleForTesting
static final int MSG_ACTIVE_PHONE_CHANGED = 8;
@VisibleForTesting
static final int MSG_DATA_REG_STATE_OR_RAT_CHANGED = 9;
// TODO: move the following parameters to xml file
private static final int TRAFFIC_STATS_POLL_INTERVAL_MS = 1_000;
private static final int MODEM_POLL_MIN_INTERVAL_MS = 5_000;
private static final int TRAFFIC_MODEM_POLL_BYTE_RATIO = 8;
private static final int TRAFFIC_POLL_BYTE_THRESHOLD_MAX = 20_000;
private static final int BYTE_DELTA_ACC_THRESHOLD_MAX_KB = 8_000;
private static final int MODEM_POLL_TIME_DELTA_MAX_MS = 10_000;
private static final int FILTER_UPDATE_MAX_INTERVAL_MS = 5_100;
// BW samples with Tx or Rx time below the following value is ignored.
private static final int TX_RX_TIME_MIN_MS = 200;
// The large time constant used in BW filter
private static final int TIME_CONSTANT_LARGE_SEC = 6;
// The small time constant used in BW filter
private static final int TIME_CONSTANT_SMALL_SEC = 6;
// If RSSI changes by more than the below value, update BW filter with small time constant
private static final int RSSI_DELTA_THRESHOLD_DB = 6;
// The up-scaling factor of filter coefficient.
private static final int FILTER_SCALE = 128;
// Force weight to 0 if the elapsed time is above LARGE_TIME_DECAY_RATIO * time constant
private static final int LARGE_TIME_DECAY_RATIO = 4;
// Modem Tx time may contain Rx time as defined in HAL. To work around the issue, if Tx time
// over Rx time ratio is above the following value, use Tx time + Rx time as Rx time.
private static final int TX_OVER_RX_TIME_RATIO_THRESHOLD_NUM = 3;
private static final int TX_OVER_RX_TIME_RATIO_THRESHOLD_DEN = 2;
// Default Link bandwidth value if the RAT entry is not found in static BW table.
private static final int DEFAULT_LINK_BAND_WIDTH_KBPS = 14;
// If Tx or Rx link bandwidth change is above the following value, send the BW update
private static final int BW_UPDATE_THRESHOLD_PERCENT = 15;
// To be used in link bandwidth estimation, each TrafficStats poll sample needs to be above
// a predefine threshold.
// For RAT with static BW above HIGH_BANDWIDTH_THRESHOLD_KBPS, it uses the following table.
// For others RATs, the thresholds are derived from the static BW values.
// The following table is defined per signal level, int [NUM_SIGNAL_LEVEL].
private static final int HIGH_BANDWIDTH_THRESHOLD_KBPS = 5000;
//Array dimension : int [NUM_LINK_DIRECTION][NUM_SIGNAL_LEVEL]
private static final int[][] BYTE_DELTA_THRESHOLD_KB =
{{200, 300, 400, 600, 1000}, {400, 600, 800, 1000, 1000}};
// Used to derive byte count threshold from avg BW
private static final int LOW_BW_TO_AVG_BW_RATIO_NUM = 3;
private static final int LOW_BW_TO_AVG_BW_RATIO_DEN = 8;
private static final int BYTE_DELTA_THRESHOLD_MIN_KB = 10;
private static final int MAX_ERROR_PERCENT = 100 * 100;
private static final String[] AVG_BW_PER_RAT = {
"GPRS:24,24", "EDGE:70,18", "UMTS:115,115", "CDMA:14,14",
"CDMA - 1xRTT:30,30", "CDMA - EvDo rev. 0:750,48", "CDMA - EvDo rev. A:950,550",
"HSDPA:4300,620", "HSUPA:4300,1800", "HSPA:4300,1800", "CDMA - EvDo rev. B:1500,550",
"CDMA - eHRPD:750,48", "HSPA+:13000,3400", "TD_SCDMA:115,115",
"LTE:30000,15000", "NR_NSA:47000,18000",
"NR_NSA_MMWAVE:145000,60000", "NR:145000,60000", "NR_MMWAVE:145000,60000"};
private static final Map<String, Pair<Integer, Integer>> AVG_BW_PER_RAT_MAP = new ArrayMap<>();
private static final String UNKNOWN_PLMN = "";
// To be used in the long term avg, each count needs to be above the following value
public static final int BW_STATS_COUNT_THRESHOLD = 5;
public static final int NUM_SIGNAL_LEVEL = 5;
public static final int LINK_TX = 0;
public static final int LINK_RX = 1;
public static final int NUM_LINK_DIRECTION = 2;
// One common timestamp for all sim to avoid frequent modem polling
private final Phone mPhone;
private final TelephonyFacade mTelephonyFacade;
private final TelephonyManager mTelephonyManager;
private final ConnectivityManager mConnectivityManager;
private final LocalLog mLocalLog = new LocalLog(512);
private boolean mScreenOn = false;
private boolean mIsOnDefaultRoute = false;
private boolean mIsOnActiveData = false;
private long mLastModemPollTimeMs;
private boolean mLastTrafficValid = true;
private long mLastMobileTxBytes;
private long mLastMobileRxBytes;
private long mTxBytesDeltaAcc;
private long mRxBytesDeltaAcc;
private ModemActivityInfo mLastModemActivityInfo = null;
private final TelephonyCallback mTelephonyCallback = new TelephonyCallbackImpl();
private int mSignalStrengthDbm;
private int mSignalLevel;
private int mDataRat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
private int mTac;
private String mPlmn = UNKNOWN_PLMN;
private NetworkCapabilities mNetworkCapabilities;
private NetworkBandwidth mPlaceholderNetwork;
private long mFilterUpdateTimeMs;
private int mBandwidthUpdateSignalDbm = -1;
private int mBandwidthUpdateSignalLevel = -1;
private int mBandwidthUpdateDataRat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
private String mBandwidthUpdatePlmn = UNKNOWN_PLMN;
private BandwidthState mTxState = new BandwidthState(LINK_TX);
private BandwidthState mRxState = new BandwidthState(LINK_RX);
private RegistrantList mBandwidthChangedRegistrants = new RegistrantList();
private long mLastPlmnOrRatChangeTimeMs;
private long mLastDrsOrRatChangeTimeMs;
private static void initAvgBwPerRatTable() {
for (String config : AVG_BW_PER_RAT) {
int rxKbps = 14;
int txKbps = 14;
String[] kv = config.split(":");
if (kv.length == 2) {
String[] split = kv[1].split(",");
if (split.length == 2) {
try {
rxKbps = Integer.parseInt(split[0]);
txKbps = Integer.parseInt(split[1]);
} catch (NumberFormatException ignored) {
}
}
AVG_BW_PER_RAT_MAP.put(kv[0], new Pair<>(rxKbps, txKbps));
}
}
}
private final DisplayManager.DisplayListener mDisplayListener =
new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
obtainMessage(MSG_SCREEN_STATE_CHANGED, isScreenOn()).sendToTarget();
}
};
private final OutcomeReceiver<ModemActivityInfo, TelephonyManager.ModemActivityInfoException>
mOutcomeReceiver =
new OutcomeReceiver<ModemActivityInfo, TelephonyManager.ModemActivityInfoException>() {
@Override
public void onResult(ModemActivityInfo result) {
obtainMessage(MSG_MODEM_ACTIVITY_RETURNED, result).sendToTarget();
}
@Override
public void onError(TelephonyManager.ModemActivityInfoException e) {
Rlog.e(TAG, "error reading modem stats:" + e);
obtainMessage(MSG_MODEM_ACTIVITY_RETURNED, null).sendToTarget();
}
};
private final ConnectivityManager.NetworkCallback mDefaultNetworkCallback =
new ConnectivityManager.NetworkCallback() {
@Override
public void onCapabilitiesChanged(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities) {
obtainMessage(MSG_DEFAULT_NETWORK_CHANGED, networkCapabilities).sendToTarget();
}
public void onLost(@NonNull Network network) {
obtainMessage(MSG_DEFAULT_NETWORK_CHANGED, null).sendToTarget();
}
};
public LinkBandwidthEstimator(Phone phone, TelephonyFacade telephonyFacade) {
mPhone = phone;
mTelephonyFacade = telephonyFacade;
mTelephonyManager = phone.getContext()
.getSystemService(TelephonyManager.class)
.createForSubscriptionId(phone.getSubId());
mConnectivityManager = phone.getContext().getSystemService(ConnectivityManager.class);
DisplayManager dm = (DisplayManager) phone.getContext().getSystemService(
Context.DISPLAY_SERVICE);
dm.registerDisplayListener(mDisplayListener, null);
handleScreenStateChanged(isScreenOn());
mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, this);
mTelephonyManager.registerTelephonyCallback(new HandlerExecutor(this),
mTelephonyCallback);
mPlaceholderNetwork = new NetworkBandwidth(UNKNOWN_PLMN);
initAvgBwPerRatTable();
registerNrStateFrequencyChange();
mPhone.getServiceStateTracker().registerForDataRegStateOrRatChanged(AccessNetworkConstants
.TRANSPORT_TYPE_WWAN, this, MSG_DATA_REG_STATE_OR_RAT_CHANGED, null);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SCREEN_STATE_CHANGED:
handleScreenStateChanged((boolean) msg.obj);
break;
case MSG_TRAFFIC_STATS_POLL:
handleTrafficStatsPoll();
break;
case MSG_MODEM_ACTIVITY_RETURNED:
handleModemActivityReturned((ModemActivityInfo) msg.obj);
break;
case MSG_DEFAULT_NETWORK_CHANGED:
handleDefaultNetworkChanged((NetworkCapabilities) msg.obj);
break;
case MSG_SIGNAL_STRENGTH_CHANGED:
handleSignalStrengthChanged((SignalStrength) msg.obj);
break;
case MSG_NR_FREQUENCY_CHANGED:
// fall through
case MSG_NR_STATE_CHANGED:
updateStaticBwValueResetFilter();
break;
case MSG_ACTIVE_PHONE_CHANGED:
handleActivePhoneChanged((int) msg.obj);
break;
case MSG_DATA_REG_STATE_OR_RAT_CHANGED:
handleDrsOrRatChanged((AsyncResult) msg.obj);
break;
default:
Rlog.e(TAG, "invalid message " + msg.what);
break;
}
}
/**
* Registers for bandwidth estimation change. The bandwidth will be returned
* * {@link AsyncResult#result} as a {@link Pair} Object.
* * The {@link AsyncResult} will be in the notification {@link Message#obj}.
* @param h handler to notify
* @param what what code of message when delivered
* @param obj placed in Message.obj
*/
public void registerForBandwidthChanged(Handler h, int what, Object obj) {
Registrant r = new Registrant(h, what, obj);
mBandwidthChangedRegistrants.add(r);
}
/**
* Unregisters for bandwidth estimation change.
* @param h handler to notify
*/
public void unregisterForBandwidthChanged(Handler h) {
mBandwidthChangedRegistrants.remove(h);
}
/**
* @return True if one the device's screen (e.g. main screen, wifi display, HDMI display etc...)
* is on.
*/
private boolean isScreenOn() {
// Note that we don't listen to Intent.SCREEN_ON and Intent.SCREEN_OFF because they are no
// longer adequate for monitoring the screen state since they are not sent in cases where
// the screen is turned off transiently such as due to the proximity sensor.
final DisplayManager dm = (DisplayManager) mPhone.getContext().getSystemService(
Context.DISPLAY_SERVICE);
Display[] displays = dm.getDisplays();
if (displays != null) {
for (Display display : displays) {
// Anything other than STATE_ON is treated as screen off, such as STATE_DOZE,
// STATE_DOZE_SUSPEND, etc...
if (display.getState() == Display.STATE_ON) {
return true;
}
}
return false;
}
return false;
}
private void handleScreenStateChanged(boolean screenOn) {
if (mScreenOn == screenOn) {
return;
}
mScreenOn = screenOn;
handleTrafficStatsPollConditionChanged();
}
private void handleDefaultNetworkChanged(NetworkCapabilities networkCapabilities) {
mNetworkCapabilities = networkCapabilities;
boolean isOnDefaultRoute;
if (networkCapabilities == null) {
isOnDefaultRoute = false;
} else {
isOnDefaultRoute = networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
}
if (mIsOnDefaultRoute == isOnDefaultRoute) {
return;
}
mIsOnDefaultRoute = isOnDefaultRoute;
handleTrafficStatsPollConditionChanged();
}
private void handleActivePhoneChanged(int activeDataSubId) {
boolean isOnActiveData = activeDataSubId == mPhone.getSubId();
if (mIsOnActiveData == isOnActiveData) {
return;
}
mIsOnActiveData = isOnActiveData;
logd("mIsOnActiveData " + mIsOnActiveData + " activeDataSubId " + activeDataSubId);
handleTrafficStatsPollConditionChanged();
}
private void handleDrsOrRatChanged(AsyncResult ar) {
Pair<Integer, Integer> drsRatPair = (Pair<Integer, Integer>) ar.result;
logd("DrsOrRatChanged dataRegState " + drsRatPair.first + " rilRat " + drsRatPair.second);
mLastDrsOrRatChangeTimeMs = mTelephonyFacade.getElapsedSinceBootMillis();
}
private void handleTrafficStatsPollConditionChanged() {
removeMessages(MSG_TRAFFIC_STATS_POLL);
if (mScreenOn && mIsOnDefaultRoute && mIsOnActiveData) {
updateDataRatCellIdentityBandwidth();
handleTrafficStatsPoll();
}
}
private void handleTrafficStatsPoll() {
invalidateTxRxSamples();
long mobileTxBytes = mTelephonyFacade.getMobileTxBytes();
long mobileRxBytes = mTelephonyFacade.getMobileRxBytes();
long txBytesDelta = mobileTxBytes - mLastMobileTxBytes;
long rxBytesDelta = mobileRxBytes - mLastMobileRxBytes;
// Schedule the next traffic stats poll
sendEmptyMessageDelayed(MSG_TRAFFIC_STATS_POLL, TRAFFIC_STATS_POLL_INTERVAL_MS);
mLastMobileTxBytes = mobileTxBytes;
mLastMobileRxBytes = mobileRxBytes;
// Sometimes TrafficStats byte counts return invalid values
// Ignore two polls if it happens
boolean trafficValid = txBytesDelta >= 0 && rxBytesDelta >= 0;
if (!mLastTrafficValid || !trafficValid) {
mLastTrafficValid = trafficValid;
Rlog.e(TAG, " run into invalid traffic count");
return;
}
mTxBytesDeltaAcc += txBytesDelta;
mRxBytesDeltaAcc += rxBytesDelta;
boolean doModemPoll = true;
// Check if it meets the requirement to request modem activity
long txByteDeltaThr = Math.min(mTxState.mByteDeltaAccThr / TRAFFIC_MODEM_POLL_BYTE_RATIO,
TRAFFIC_POLL_BYTE_THRESHOLD_MAX);
long rxByteDeltaThr = Math.min(mRxState.mByteDeltaAccThr / TRAFFIC_MODEM_POLL_BYTE_RATIO,
TRAFFIC_POLL_BYTE_THRESHOLD_MAX);
if (txBytesDelta < txByteDeltaThr && rxBytesDelta < rxByteDeltaThr
&& mTxBytesDeltaAcc < mTxState.mByteDeltaAccThr
&& mRxBytesDeltaAcc < mRxState.mByteDeltaAccThr) {
doModemPoll = false;
}
long currTimeMs = mTelephonyFacade.getElapsedSinceBootMillis();
long timeSinceLastModemPollMs = currTimeMs - mLastModemPollTimeMs;
if (timeSinceLastModemPollMs < MODEM_POLL_MIN_INTERVAL_MS) {
doModemPoll = false;
}
if (doModemPoll) {
StringBuilder sb = new StringBuilder();
logd(sb.append("txByteDelta ").append(txBytesDelta)
.append(" rxByteDelta ").append(rxBytesDelta)
.append(" txByteDeltaAcc ").append(mTxBytesDeltaAcc)
.append(" rxByteDeltaAcc ").append(mRxBytesDeltaAcc)
.append(" trigger modem activity request").toString());
updateDataRatCellIdentityBandwidth();
// Filter update will happen after the request
makeRequestModemActivity();
return;
}
long timeSinceLastFilterUpdateMs = currTimeMs - mFilterUpdateTimeMs;
// Update filter
if (timeSinceLastFilterUpdateMs >= FILTER_UPDATE_MAX_INTERVAL_MS) {
if (!updateDataRatCellIdentityBandwidth()) {
updateTxRxBandwidthFilterSendToDataConnection();
}
}
}
private void makeRequestModemActivity() {
mLastModemPollTimeMs = mTelephonyFacade.getElapsedSinceBootMillis();
// TODO: add CountDown in case that onResult/OnError() never happen
mTelephonyManager.requestModemActivityInfo(Runnable::run, mOutcomeReceiver);
}
private void handleModemActivityReturned(ModemActivityInfo result) {
updateBandwidthTxRxSamples(result);
updateTxRxBandwidthFilterSendToDataConnection();
mLastModemActivityInfo = result;
// Update for next poll
resetByteDeltaAcc();
}
private void resetByteDeltaAcc() {
mTxBytesDeltaAcc = 0;
mRxBytesDeltaAcc = 0;
}
private void invalidateTxRxSamples() {
mTxState.mBwSampleValid = false;
mRxState.mBwSampleValid = false;
}
private void updateBandwidthTxRxSamples(ModemActivityInfo modemActivityInfo) {
if (mLastModemActivityInfo == null || modemActivityInfo == null
|| mNetworkCapabilities == null || hasRecentDataRegStatePlmnOrRatChange()) {
return;
}
long lastTimeMs = mLastModemActivityInfo.getTimestampMillis();
long currTimeMs = modemActivityInfo.getTimestampMillis();
long timeDeltaMs = currTimeMs - lastTimeMs;
if (timeDeltaMs > MODEM_POLL_TIME_DELTA_MAX_MS || timeDeltaMs <= 0) {
return;
}
ModemActivityInfo deltaInfo = mLastModemActivityInfo.getDelta(modemActivityInfo);
long txTimeDeltaMs = getModemTxTimeMs(deltaInfo);
long rxTimeDeltaMs = deltaInfo.getReceiveTimeMillis();
// Check if txTimeDeltaMs / rxTimeDeltaMs > TX_OVER_RX_TIME_RATIO_THRESHOLD
boolean isTxTimeOverRxTimeRatioLarge = (txTimeDeltaMs * TX_OVER_RX_TIME_RATIO_THRESHOLD_DEN
> rxTimeDeltaMs * TX_OVER_RX_TIME_RATIO_THRESHOLD_NUM);
long rxTimeBwEstMs = isTxTimeOverRxTimeRatioLarge
? (txTimeDeltaMs + rxTimeDeltaMs) : rxTimeDeltaMs;
mTxState.updateBandwidthSample(mTxBytesDeltaAcc, txTimeDeltaMs);
mRxState.updateBandwidthSample(mRxBytesDeltaAcc, rxTimeBwEstMs);
int reportedTxTputKbps = mNetworkCapabilities.getLinkUpstreamBandwidthKbps();
int reportedRxTputKbps = mNetworkCapabilities.getLinkDownstreamBandwidthKbps();
StringBuilder sb = new StringBuilder();
logd(sb.append("UpdateBwSample")
.append(" dBm ").append(mSignalStrengthDbm)
.append(" level ").append(mSignalLevel)
.append(" rat ").append(getDataRatName(mDataRat))
.append(" plmn ").append(mPlmn)
.append(" tac ").append(mTac)
.append(" reportedTxKbps ").append(reportedTxTputKbps)
.append(" reportedRxKbps ").append(reportedRxTputKbps)
.append(" txMs ").append(txTimeDeltaMs)
.append(" rxMs ").append(rxTimeDeltaMs)
.append(" txKB ").append(mTxBytesDeltaAcc / 1024)
.append(" rxKB ").append(mRxBytesDeltaAcc / 1024)
.append(" txKBThr ").append(mTxState.mByteDeltaAccThr / 1024)
.append(" rxKBThr ").append(mRxState.mByteDeltaAccThr / 1024)
.toString());
}
private boolean hasRecentDataRegStatePlmnOrRatChange() {
if (mLastModemActivityInfo == null) {
return false;
}
return (mLastDrsOrRatChangeTimeMs > mLastModemActivityInfo.getTimestampMillis()
|| mLastPlmnOrRatChangeTimeMs > mLastModemActivityInfo.getTimestampMillis());
}
private long getModemTxTimeMs(ModemActivityInfo modemActivity) {
long txTimeMs = 0;
for (int lvl = 0; lvl < ModemActivityInfo.getNumTxPowerLevels(); lvl++) {
txTimeMs += modemActivity.getTransmitDurationMillisAtPowerLevel(lvl);
}
return txTimeMs;
}
private void updateTxRxBandwidthFilterSendToDataConnection() {
mFilterUpdateTimeMs = mTelephonyFacade.getElapsedSinceBootMillis();
mTxState.updateBandwidthFilter();
mRxState.updateBandwidthFilter();
boolean isNetworkChanged = mTxState.hasLargeBwChange()
|| mRxState.hasLargeBwChange()
|| mBandwidthUpdateDataRat != mDataRat
|| mBandwidthUpdateSignalLevel != mSignalLevel
|| !mBandwidthUpdatePlmn.equals(mPlmn);
if (isValidNetwork() && isNetworkChanged) {
mTxState.mLastReportedBwKbps = mTxState.mAvgUsedKbps < 0 ? -1 : mTxState.mFilterKbps;
mRxState.mLastReportedBwKbps = mRxState.mAvgUsedKbps < 0 ? -1 : mRxState.mFilterKbps;
sendLinkBandwidthToDataConnection(
mTxState.mLastReportedBwKbps,
mRxState.mLastReportedBwKbps);
}
mBandwidthUpdateSignalDbm = mSignalStrengthDbm;
mBandwidthUpdateSignalLevel = mSignalLevel;
mBandwidthUpdateDataRat = mDataRat;
mBandwidthUpdatePlmn = mPlmn;
mTxState.calculateError();
mRxState.calculateError();
}
private boolean isValidNetwork() {
return !mPlmn.equals(UNKNOWN_PLMN) && mDataRat != TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
private class BandwidthState {
private final int mLink;
int mFilterKbps;
int mByteDeltaAccThr = BYTE_DELTA_THRESHOLD_KB[0][0];
int mAvgUsedKbps;
int mBwSampleKbps;
boolean mBwSampleValid;
long mBwSampleValidTimeMs;
int mStaticBwKbps;
int mLastReportedBwKbps;
BandwidthState(int link) {
mLink = link;
}
private void updateBandwidthSample(long bytesDelta, long timeDeltaMs) {
updateByteCountThr();
if (bytesDelta < mByteDeltaAccThr) {
return;
}
if (timeDeltaMs < TX_RX_TIME_MIN_MS) {
return;
}
long linkBandwidthLongKbps = bytesDelta * 8 / timeDeltaMs * 1000 / 1024;
if (linkBandwidthLongKbps > Integer.MAX_VALUE || linkBandwidthLongKbps < 0) {
return;
}
int linkBandwidthKbps = (int) linkBandwidthLongKbps;
mBwSampleValid = true;
mBwSampleKbps = linkBandwidthKbps;
String dataRatName = getDataRatName(mDataRat);
NetworkBandwidth network = lookupNetwork(mPlmn, dataRatName);
// Update per RAT stats of all TAC
network.update(linkBandwidthKbps, mLink, mSignalLevel);
// Update per TAC stats
network = lookupNetwork(mPlmn, mTac, dataRatName);
network.update(linkBandwidthKbps, mLink, mSignalLevel);
}
private void updateBandwidthFilter() {
int avgKbps = getAvgLinkBandwidthKbps();
// Feed the filter with the long term avg if there is no valid BW sample so that filter
// will gradually converge the long term avg.
int filterInKbps = mBwSampleValid ? mBwSampleKbps : avgKbps;
long currTimeMs = mTelephonyFacade.getElapsedSinceBootMillis();
int timeDeltaSec = (int) (currTimeMs - mBwSampleValidTimeMs) / 1000;
// If the operation condition changes significantly since the last update
// or the sample has higher BW, use a faster filter. Otherwise, use a slow filter
int timeConstantSec;
if (Math.abs(mBandwidthUpdateSignalDbm - mSignalStrengthDbm) > RSSI_DELTA_THRESHOLD_DB
|| !mBandwidthUpdatePlmn.equals(mPlmn)
|| mBandwidthUpdateDataRat != mDataRat
|| (mBwSampleValid && mBwSampleKbps > avgKbps)) {
timeConstantSec = TIME_CONSTANT_SMALL_SEC;
} else {
timeConstantSec = TIME_CONSTANT_LARGE_SEC;
}
// Update timestamp for next iteration
if (mBwSampleValid) {
mBwSampleValidTimeMs = currTimeMs;
}
if (filterInKbps == mFilterKbps) {
return;
}
int alpha = timeDeltaSec > LARGE_TIME_DECAY_RATIO * timeConstantSec ? 0
: (int) (FILTER_SCALE * Math.exp(-1.0 * timeDeltaSec / timeConstantSec));
if (alpha == 0) {
mFilterKbps = filterInKbps;
return;
}
long filterOutKbps = (long) mFilterKbps * alpha
+ filterInKbps * FILTER_SCALE - filterInKbps * alpha;
filterOutKbps = filterOutKbps / FILTER_SCALE;
mFilterKbps = (int) Math.min(filterOutKbps, Integer.MAX_VALUE);
StringBuilder sb = new StringBuilder();
logv(sb.append(mLink)
.append(" lastSampleWeight=").append(alpha)
.append("/").append(FILTER_SCALE)
.append(" filterInKbps=").append(filterInKbps)
.append(" avgKbps=").append(avgKbps)
.append(" filterOutKbps=").append(mFilterKbps)
.toString());
}
private int getAvgUsedLinkBandwidthKbps() {
// Check if current TAC/RAT/level has enough stats
String dataRatName = getDataRatName(mDataRat);
NetworkBandwidth network = lookupNetwork(mPlmn, mTac, dataRatName);
int count = network.getCount(mLink, mSignalLevel);
if (count >= BW_STATS_COUNT_THRESHOLD) {
return (int) (network.getValue(mLink, mSignalLevel) / count);
}
// Check if current RAT/level has enough stats
network = lookupNetwork(mPlmn, dataRatName);
count = network.getCount(mLink, mSignalLevel);
if (count >= BW_STATS_COUNT_THRESHOLD) {
return (int) (network.getValue(mLink, mSignalLevel) / count);
}
return -1;
}
private int getCurrentCount() {
String dataRatName = getDataRatName(mDataRat);
NetworkBandwidth network = lookupNetwork(mPlmn, dataRatName);
return network.getCount(mLink, mSignalLevel);
}
/** get a long term avg value (PLMN/RAT/TAC/level dependent) or static value */
private int getAvgLinkBandwidthKbps() {
mAvgUsedKbps = getAvgUsedLinkBandwidthKbps();
if (mAvgUsedKbps > 0) {
return mAvgUsedKbps;
}
// Fall back to static value
return mStaticBwKbps;
}
private void resetBandwidthFilter() {
mBwSampleValid = false;
mFilterKbps = getAvgLinkBandwidthKbps();
}
private void updateByteCountThr() {
// For high BW RAT cases, use predefined value + threshold derived from avg usage BW
if (mStaticBwKbps > HIGH_BANDWIDTH_THRESHOLD_KBPS) {
int lowBytes = calculateByteCountThreshold(getAvgUsedLinkBandwidthKbps(),
MODEM_POLL_MIN_INTERVAL_MS);
// Start with a predefined value
mByteDeltaAccThr = BYTE_DELTA_THRESHOLD_KB[mLink][mSignalLevel] * 1024;
if (lowBytes > 0) {
// Raise the threshold if the avg usage BW is high
mByteDeltaAccThr = Math.max(lowBytes, mByteDeltaAccThr);
mByteDeltaAccThr = Math.min(mByteDeltaAccThr,
BYTE_DELTA_ACC_THRESHOLD_MAX_KB * 1024);
}
return;
}
// For low BW RAT cases, derive the threshold from avg BW values
mByteDeltaAccThr = calculateByteCountThreshold(mStaticBwKbps,
MODEM_POLL_MIN_INTERVAL_MS);
mByteDeltaAccThr = Math.max(mByteDeltaAccThr, BYTE_DELTA_THRESHOLD_MIN_KB * 1024);
// Low BW RAT threshold value should be no more than high BW one.
mByteDeltaAccThr = Math.min(mByteDeltaAccThr, BYTE_DELTA_THRESHOLD_KB[mLink][0] * 1024);
}
// Calculate a byte count threshold for the given avg BW and observation window size
private int calculateByteCountThreshold(int avgBwKbps, int durationMs) {
long avgBytes = (long) avgBwKbps / 8 * durationMs;
long result = avgBytes * LOW_BW_TO_AVG_BW_RATIO_NUM / LOW_BW_TO_AVG_BW_RATIO_DEN;
return (int) Math.min(result, Integer.MAX_VALUE);
}
public boolean hasLargeBwChange() {
int deltaKbps = Math.abs(mLastReportedBwKbps - mFilterKbps);
return mAvgUsedKbps > 0
&& deltaKbps * 100 > BW_UPDATE_THRESHOLD_PERCENT * mLastReportedBwKbps;
}
public void calculateError() {
if (!mBwSampleValid || getCurrentCount() <= BW_STATS_COUNT_THRESHOLD + 1
|| mAvgUsedKbps <= 0) {
return;
}
int bwEstExtErrPercent = calculateErrorPercent(mLastReportedBwKbps, mBwSampleKbps);
int bwEstAvgErrPercent = calculateErrorPercent(mAvgUsedKbps, mBwSampleKbps);
int bwEstIntErrPercent = calculateErrorPercent(mFilterKbps, mBwSampleKbps);
int coldStartErrPercent = calculateErrorPercent(mStaticBwKbps, mBwSampleKbps);
TelephonyMetrics.getInstance().writeBandwidthStats(mLink, mDataRat, getNrMode(mDataRat),
mSignalLevel, bwEstExtErrPercent, coldStartErrPercent, mBwSampleKbps);
StringBuilder sb = new StringBuilder();
logd(sb.append(mLink)
.append(" sampKbps ").append(mBwSampleKbps)
.append(" filtKbps ").append(mFilterKbps)
.append(" reportKbps ").append(mLastReportedBwKbps)
.append(" avgUsedKbps ").append(mAvgUsedKbps)
.append(" csKbps ").append(mStaticBwKbps)
.append(" intErrPercent ").append(bwEstIntErrPercent)
.append(" avgErrPercent ").append(bwEstAvgErrPercent)
.append(" extErrPercent ").append(bwEstExtErrPercent)
.append(" csErrPercent ").append(coldStartErrPercent)
.toString());
}
private int calculateErrorPercent(int inKbps, int bwSampleKbps) {
long errorPercent = 100L * (inKbps - bwSampleKbps) / bwSampleKbps;
return (int) Math.max(-MAX_ERROR_PERCENT, Math.min(errorPercent, MAX_ERROR_PERCENT));
}
}
/**
* Update the byte count threshold.
* It should be called whenever the RAT or signal level is changed.
* For the RAT with high BW (4G and beyond), use BYTE_DELTA_THRESHOLD_KB table.
* For other RATs, derive the threshold based on the static BW values.
*/
private void updateByteCountThr() {
mTxState.updateByteCountThr();
mRxState.updateByteCountThr();
}
// Reset BW filter to a long term avg value (PLMN/RAT/TAC dependent) or static BW value.
// It should be called whenever PLMN/RAT or static BW value is changed;
private void resetBandwidthFilter() {
mTxState.resetBandwidthFilter();
mRxState.resetBandwidthFilter();
}
private void sendLinkBandwidthToDataConnection(int linkBandwidthTxKps, int linkBandwidthRxKps) {
logv("send to DC tx " + linkBandwidthTxKps + " rx " + linkBandwidthRxKps);
Pair<Integer, Integer> bandwidthInfo =
new Pair<Integer, Integer>(linkBandwidthTxKps, linkBandwidthRxKps);
mBandwidthChangedRegistrants.notifyRegistrants(new AsyncResult(null, bandwidthInfo, null));
}
private void handleSignalStrengthChanged(SignalStrength signalStrength) {
if (signalStrength == null) {
return;
}
mSignalStrengthDbm = signalStrength.getDbm();
mSignalLevel = signalStrength.getLevel();
updateByteCountThr();
if (updateDataRatCellIdentityBandwidth()) {
return;
}
if (Math.abs(mBandwidthUpdateSignalDbm - mSignalStrengthDbm) > RSSI_DELTA_THRESHOLD_DB) {
updateTxRxBandwidthFilterSendToDataConnection();
}
}
private void registerNrStateFrequencyChange() {
mPhone.getServiceStateTracker().registerForNrStateChanged(this,
MSG_NR_STATE_CHANGED, null);
mPhone.getServiceStateTracker().registerForNrFrequencyChanged(this,
MSG_NR_FREQUENCY_CHANGED, null);
}
/**
* Get a string based on current RAT
*/
public String getDataRatName(int rat) {
return getDataRatName(rat, getNrMode(rat));
}
private int getNrMode(int rat) {
if (rat == TelephonyManager.NETWORK_TYPE_LTE && isNrNsaConnected()) {
return mPhone.getServiceState().getNrFrequencyRange()
== ServiceState.FREQUENCY_RANGE_MMWAVE
? NrMode.NR_NSA_MMWAVE : NrMode.NR_NSA;
} else if (rat == TelephonyManager.NETWORK_TYPE_NR) {
return mPhone.getServiceState().getNrFrequencyRange()
== ServiceState.FREQUENCY_RANGE_MMWAVE
? NrMode.NR_SA_MMWAVE : NrMode.NR_SA;
}
return NrMode.NR_NONE;
}
/**
* Get a string based on current RAT and NR operation mode.
*/
public static String getDataRatName(int rat, int nrMode) {
if (rat == TelephonyManager.NETWORK_TYPE_LTE
&& (nrMode == NrMode.NR_NSA || nrMode == NrMode.NR_NSA_MMWAVE)) {
return nrMode == NrMode.NR_NSA
? DctConstants.RAT_NAME_NR_NSA : DctConstants.RAT_NAME_NR_NSA_MMWAVE;
} else if (rat == TelephonyManager.NETWORK_TYPE_NR) {
return nrMode == NrMode.NR_SA
? TelephonyManager.getNetworkTypeName(rat) : DctConstants.RAT_NAME_NR_SA_MMWAVE;
}
return TelephonyManager.getNetworkTypeName(rat);
}
/**
* Check if the device is connected to NR 5G Non-Standalone network
*/
private boolean isNrNsaConnected() {
return mPhone.getServiceState().getNrState()
== NetworkRegistrationInfo.NR_STATE_CONNECTED;
}
// Update avg BW values.
// It should be called whenever the RAT could be changed.
// return true if avg value is changed;
private boolean updateStaticBwValue(int dataRat) {
Pair<Integer, Integer> values = getStaticAvgBw(dataRat);
if (values == null) {
mTxState.mStaticBwKbps = DEFAULT_LINK_BAND_WIDTH_KBPS;
mRxState.mStaticBwKbps = DEFAULT_LINK_BAND_WIDTH_KBPS;
return true;
}
if (mTxState.mStaticBwKbps != values.second
|| mRxState.mStaticBwKbps != values.first) {
mTxState.mStaticBwKbps = values.second;
mRxState.mStaticBwKbps = values.first;
return true;
}
return false;
}
/** get per-RAT static bandwidth value */
public Pair<Integer, Integer> getStaticAvgBw(int dataRat) {
String dataRatName = getDataRatName(dataRat);
Pair<Integer, Integer> values = AVG_BW_PER_RAT_MAP.get(dataRatName);
if (values == null) {
Rlog.e(TAG, dataRatName + " is not found in Avg BW table");
}
return values;
}
private void updateStaticBwValueResetFilter() {
if (updateStaticBwValue(mDataRat)) {
updateByteCountThr();
resetBandwidthFilter();
updateTxRxBandwidthFilterSendToDataConnection();
}
}
private NetworkRegistrationInfo getDataNri() {
return mPhone.getServiceState().getNetworkRegistrationInfo(
NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
}
private boolean updateDataRatCellIdentityBandwidth() {
boolean updatedPlmn = false;
CellIdentity cellIdentity = mPhone.getCurrentCellIdentity();
mTac = getTac(cellIdentity);
String plmn;
if (mPhone.getServiceState().getOperatorNumeric() != null) {
plmn = mPhone.getServiceState().getOperatorNumeric();
} else {
if (cellIdentity.getPlmn() != null) {
plmn = cellIdentity.getPlmn();
} else {
plmn = UNKNOWN_PLMN;
}
}
if (mPlmn == null || !plmn.equals(mPlmn)) {
updatedPlmn = true;
mPlmn = plmn;
}
boolean updatedRat = false;
NetworkRegistrationInfo nri = getDataNri();
if (nri != null) {
int dataRat = nri.getAccessNetworkTechnology();
if (dataRat != mDataRat) {
updatedRat = true;
mDataRat = dataRat;
updateStaticBwValue(mDataRat);
updateByteCountThr();
}
}
boolean updatedPlmnOrRat = updatedPlmn || updatedRat;
if (updatedPlmnOrRat) {
resetBandwidthFilter();
updateTxRxBandwidthFilterSendToDataConnection();
mLastPlmnOrRatChangeTimeMs = mTelephonyFacade.getElapsedSinceBootMillis();
}
return updatedPlmnOrRat;
}
private int getTac(@NonNull CellIdentity cellIdentity) {
if (cellIdentity instanceof CellIdentityLte) {
return ((CellIdentityLte) cellIdentity).getTac();
}
if (cellIdentity instanceof CellIdentityNr) {
return ((CellIdentityNr) cellIdentity).getTac();
}
if (cellIdentity instanceof CellIdentityWcdma) {
return ((CellIdentityWcdma) cellIdentity).getLac();
}
if (cellIdentity instanceof CellIdentityTdscdma) {
return ((CellIdentityTdscdma) cellIdentity).getLac();
}
if (cellIdentity instanceof CellIdentityGsm) {
return ((CellIdentityGsm) cellIdentity).getLac();
}
return 0;
}
private class TelephonyCallbackImpl extends TelephonyCallback implements
TelephonyCallback.SignalStrengthsListener,
TelephonyCallback.ActiveDataSubscriptionIdListener {
@Override
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
obtainMessage(MSG_SIGNAL_STRENGTH_CHANGED, signalStrength).sendToTarget();
}
@Override
public void onActiveDataSubscriptionIdChanged(int subId) {
obtainMessage(MSG_ACTIVE_PHONE_CHANGED, subId).sendToTarget();
}
}
void logv(String msg) {
if (DBG) Rlog.v(TAG, msg);
}
void logd(String msg) {
if (DBG) Rlog.d(TAG, msg);
mLocalLog.log(msg);
}
@VisibleForTesting
static final int UNKNOWN_TAC = -1;
// Map with NetworkKey as the key and NetworkBandwidth as the value.
// NetworkKey is specified by the PLMN, data RAT and TAC of network.
// NetworkBandwidth represents the bandwidth related stats of each network.
private final Map<NetworkKey, NetworkBandwidth> mNetworkMap = new ArrayMap<>();
private static class NetworkKey {
private final String mPlmn;
private final String mDataRat;
private final int mTac;
NetworkKey(String plmn, int tac, String dataRat) {
mPlmn = plmn;
mTac = tac;
mDataRat = dataRat;
}
@Override
public boolean equals(@Nullable Object o) {
if (o == null || !(o instanceof NetworkKey) || hashCode() != o.hashCode()) {
return false;
}
if (this == o) {
return true;
}
NetworkKey that = (NetworkKey) o;
return mPlmn.equals(that.mPlmn)
&& mTac == that.mTac
&& mDataRat.equals(that.mDataRat);
}
@Override
public int hashCode() {
return Objects.hash(mPlmn, mDataRat, mTac);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Plmn").append(mPlmn)
.append("Rat").append(mDataRat)
.append("Tac").append(mTac)
.toString();
return sb.toString();
}
}
@NonNull
private NetworkBandwidth lookupNetwork(String plmn, String dataRat) {
return lookupNetwork(plmn, UNKNOWN_TAC, dataRat);
}
/** Look up NetworkBandwidth and create a new one if it doesn't exist */
@VisibleForTesting
@NonNull
public NetworkBandwidth lookupNetwork(String plmn, int tac, String dataRat) {
if (plmn == null || dataRat.equals(
TelephonyManager.getNetworkTypeName(TelephonyManager.NETWORK_TYPE_UNKNOWN))) {
return mPlaceholderNetwork;
}
NetworkKey key = new NetworkKey(plmn, tac, dataRat);
NetworkBandwidth ans = mNetworkMap.get(key);
if (ans == null) {
ans = new NetworkBandwidth(key.toString());
mNetworkMap.put(key, ans);
}
return ans;
}
/** A class holding link bandwidth related stats */
@VisibleForTesting
public class NetworkBandwidth {
private final String mKey;
NetworkBandwidth(String key) {
mKey = key;
}
/** Update link bandwidth stats */
public void update(long value, int link, int level) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(
mPhone.getContext());
String valueKey = getValueKey(link, level);
String countKey = getCountKey(link, level);
SharedPreferences.Editor editor = sp.edit();
long currValue = sp.getLong(valueKey, 0);
int currCount = sp.getInt(countKey, 0);
editor.putLong(valueKey, currValue + value);
editor.putInt(countKey, currCount + 1);
editor.apply();
}
private String getValueKey(int link, int level) {
return getDataKey(link, level) + "Data";
}
private String getCountKey(int link, int level) {
return getDataKey(link, level) + "Count";
}
private String getDataKey(int link, int level) {
StringBuilder sb = new StringBuilder();
return sb.append(mKey)
.append("Link").append(link)
.append("Level").append(level)
.toString();
}
/** Get the accumulated bandwidth value */
public long getValue(int link, int level) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(
mPhone.getContext());
String valueKey = getValueKey(link, level);
return sp.getLong(valueKey, 0);
}
/** Get the accumulated bandwidth count */
public int getCount(int link, int level) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(
mPhone.getContext());
String countKey = getCountKey(link, level);
return sp.getInt(countKey, 0);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(mKey);
sb.append("\n");
for (int link = 0; link < NUM_LINK_DIRECTION; link++) {
sb.append((link == 0 ? "tx" : "rx"));
sb.append("\n avgKbps");
for (int level = 0; level < NUM_SIGNAL_LEVEL; level++) {
int count = getCount(link, level);
int avgKbps = count == 0 ? 0 : (int) (getValue(link, level) / count);
sb.append(" ").append(avgKbps);
}
sb.append("\n count");
for (int level = 0; level < NUM_SIGNAL_LEVEL; level++) {
int count = getCount(link, level);
sb.append(" ").append(count);
}
sb.append("\n");
}
return sb.toString();
}
}
/**
* Dump the internal state and local logs
*/
public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
pw.increaseIndent();
pw.println("current PLMN " + mPlmn + " TAC " + mTac + " RAT " + getDataRatName(mDataRat));
pw.println("all networks visited since device boot");
for (NetworkBandwidth network : mNetworkMap.values()) {
pw.println(network.toString());
}
try {
mLocalLog.dump(fd, pw, args);
} catch (Exception e) {
e.printStackTrace();
}
pw.decreaseIndent();
pw.println();
pw.flush();
}
}