blob: fe00a4accbbdd6098cf230ddf3f05d9b9faa3d44 [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 android.telephony.ims.RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED;
import static android.telephony.ims.RegistrationManager.REGISTRATION_STATE_REGISTERED;
import static android.telephony.ims.RegistrationManager.REGISTRATION_STATE_REGISTERING;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_NR;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static android.util.Patterns.EMAIL_ADDRESS;
import android.annotation.Nullable;
import android.os.SystemClock;
import android.telephony.AccessNetworkConstants;
import android.telephony.AccessNetworkConstants.TransportType;
import android.telephony.Annotation.NetworkType;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ProvisioningManager;
import android.telephony.ims.RegistrationManager.ImsRegistrationState;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability;
import android.telephony.ims.stub.ImsRegistrationImplBase.ImsRegistrationTech;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats;
import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
import com.android.telephony.Rlog;
import java.util.regex.Pattern;
/** Tracks IMS registration metrics for each phone. */
public class ImsStats {
private static final String TAG = ImsStats.class.getSimpleName();
/**
* Minimal duration of the registration state.
*
* <p>Registration state (including changes in capable/available features) with duration shorter
* than this will be ignored as they are considered transient states.
*/
private static final long MIN_REGISTRATION_DURATION_MILLIS = 1L * SECOND_IN_MILLIS;
/**
* Maximum length of the extra message in the termination reason.
*
* <p>If the extra message is longer than this length, it will be truncated.
*/
private static final int MAX_EXTRA_MESSAGE_LENGTH = 128;
/** Pattern used to match UUIDs in IMS extra messages for filtering. */
private static final Pattern PATTERN_UUID =
Pattern.compile(
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
/** Replacement for UUIDs. */
private static final String REPLACEMENT_UUID = "<UUID_REDACTED>";
/**
* Pattern used to match URI (e.g. sip, tel) in IMS extra messages for filtering.
*
* <p>NOTE: this simple pattern aims to catch the most common URI schemes. It is not meant to be
* RFC-complaint.
*/
private static final Pattern PATTERN_URI =
Pattern.compile("([a-zA-Z]{2,}:)" + EMAIL_ADDRESS.pattern());
/** Replacement for URI. */
private static final String REPLACEMENT_URI = "$1<REDACTED>";
/**
* Pattern used to match IPv4 addresses in IMS extra messages for filtering.
*
* <p>This is a copy of {@code android.util.Patterns.IP_ADDRESS}, which is deprecated and might
* be removed in the future.
*/
private static final Pattern PATTERN_IPV4 =
Pattern.compile(
"((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ "|[1-9][0-9]|[0-9]))");
/** Replacement for IPv4 addresses. */
private static final String REPLACEMENT_IPV4 = "<IPV4_REDACTED>";
/**
* Pattern used to match IPv6 addresses in IMS extra messages for filtering.
*
* <p>NOTE: this pattern aims to catch the most common IPv6 addresses. It is not meant to be
* RFC-complaint or free of false positives.
*/
private static final Pattern PATTERN_IPV6 =
Pattern.compile(
// Full address
"([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}"
// Abbreviated address, e.g. 2001:4860:4860::8888
+ "|([0-9a-fA-F]{1,4}:){1,6}(:[0-9a-fA-F]{1,4}){1,6}"
// Abbreviated network address, e.g. 2607:F8B0::
+ "|([0-9a-fA-F]{1,4}:){1,7}:"
// Abbreviated address, e.g. ::1
+ "|:(:[0-9a-fA-F]{1,4}){1,7}");
/** Replacement for IPv6 addresses. */
private static final String REPLACEMENT_IPV6 = "<IPV6_REDACTED>";
/**
* Pattern used to match potential IMEI values in IMS extra messages for filtering.
*
* <p>This includes segmented IMEI or IMEI/SV, as well as unsegmented IMEI/SV.
*/
private static final Pattern PATTERN_IMEI =
Pattern.compile(
"(^|[^0-9])(?:"
// IMEI, AABBBBBB-CCCCCC-D format; IMEI/SV, AABBBBBB-CCCCCC-EE format
+ "[0-9]{8}-[0-9]{6}-[0-9][0-9]?"
// IMEI, AA-BBBBBB-CCCCCC-D format; IMEI/SV, AA-BBBBBB-CCCCCC-EE format
+ "|[0-9]{2}-[0-9]{6}-[0-9]{6}-[0-9][0-9]?"
// IMEI/SV, unsegmented
+ "|[0-9]{16}"
+ ")($|[^0-9])");
/** Replacement for IMEI. */
private static final String REPLACEMENT_IMEI = "$1<IMEI_REDACTED>$2";
/**
* Pattern used to match potential unsegmented IMEI/IMSI values in IMS extra messages for
* filtering.
*/
private static final Pattern PATTERN_UNSEGMENTED_IMEI_IMSI =
Pattern.compile("(^|[^0-9])[0-9]{15}($|[^0-9])");
/** Replacement for unsegmented IMEI/IMSI. */
private static final String REPLACEMENT_UNSEGMENTED_IMEI_IMSI = "$1<IMEI_IMSI_REDACTED>$2";
/**
* Pattern used to match hostnames in IMS extra messages for filtering.
*
* <p>This pattern differs from {@link android.util.Patterns.DOMAIN_NAME} in a few ways: it
* requires the name to have at least 3 segments (shorter names are nearly always public or
* typos, i.e. missing space after period), does not check the validity of TLDs, and does not
* support punycodes in TLDs.
*/
private static final Pattern PATTERN_HOSTNAME =
Pattern.compile("([0-9a-zA-Z][0-9a-zA-Z_\\-]{0,61}[0-9a-zA-Z]\\.){2,}[a-zA-Z]{2,}");
/** Replacement for hostnames. */
private static final String REPLACEMENT_HOSTNAME = "<HOSTNAME_REDACTED>";
/**
* Pattern used to match potential IDs in IMS extra messages for filtering.
*
* <p>This pattern target numbers that are potential IDs in unknown formats. It should be
* replaced after all other replacements are done to ensure complete and correct filtering.
*
* <p>Specifically, this pattern looks for any number (including hex) that is separated by dots
* or dashes has at least 6 significant digits, and any unsegmented numbers that has at least 5
* significant digits.
*/
private static final Pattern PATTERN_UNKNOWN_ID =
Pattern.compile(
"(^|[^0-9a-fA-F])(([-\\.]?0)*[1-9a-fA-F]([-\\.]?[0-9a-fA-F]){5,}"
+ "|0*[1-9a-fA-F]([0-9a-fA-F]){4,})");
/** Replacement for potential IDs. */
private static final String REPLACEMENT_UNKNOWN_ID = "$1<ID_REDACTED>";
private final ImsPhone mPhone;
private final PersistAtomsStorage mStorage;
@ImsRegistrationState private int mLastRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
private long mLastTimestamp;
@Nullable private ImsRegistrationStats mLastRegistrationStats;
// Available features are those reported by ImsService to be available for use.
private MmTelCapabilities mLastAvailableFeatures = new MmTelCapabilities();
// Capable features (enabled by device/carrier). Theses are available before IMS is registered
// and not necessarily updated when RAT changes.
private final MmTelCapabilities mLastWwanCapableFeatures = new MmTelCapabilities();
private final MmTelCapabilities mLastWlanCapableFeatures = new MmTelCapabilities();
public ImsStats(ImsPhone phone) {
mPhone = phone;
mStorage = PhoneFactory.getMetricsCollector().getAtomsStorage();
}
/**
* Finalizes the durations of the current IMS registration stats segment.
*
* <p>This method is also invoked whenever the registration state, feature capability, or
* feature availability changes.
*/
public synchronized void conclude() {
long now = getTimeMillis();
// Currently not tracking time spent on registering.
if (mLastRegistrationState == REGISTRATION_STATE_REGISTERED) {
ImsRegistrationStats stats = copyOf(mLastRegistrationStats);
long duration = now - mLastTimestamp;
if (duration < MIN_REGISTRATION_DURATION_MILLIS) {
logw("conclude: discarding transient stats, duration=%d", duration);
} else {
stats.registeredMillis = duration;
stats.voiceAvailableMillis =
mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_VOICE) ? duration : 0;
stats.videoAvailableMillis =
mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_VIDEO) ? duration : 0;
stats.utAvailableMillis =
mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_UT) ? duration : 0;
stats.smsAvailableMillis =
mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_SMS) ? duration : 0;
MmTelCapabilities lastCapableFeatures =
stats.rat == TelephonyManager.NETWORK_TYPE_IWLAN
? mLastWlanCapableFeatures
: mLastWwanCapableFeatures;
stats.voiceCapableMillis =
lastCapableFeatures.isCapable(CAPABILITY_TYPE_VOICE) ? duration : 0;
stats.videoCapableMillis =
lastCapableFeatures.isCapable(CAPABILITY_TYPE_VIDEO) ? duration : 0;
stats.utCapableMillis =
lastCapableFeatures.isCapable(CAPABILITY_TYPE_UT) ? duration : 0;
stats.smsCapableMillis =
lastCapableFeatures.isCapable(CAPABILITY_TYPE_SMS) ? duration : 0;
mStorage.addImsRegistrationStats(stats);
}
}
mLastTimestamp = now;
}
/** Updates the stats when registered features changed. */
public synchronized void onImsCapabilitiesChanged(
@ImsRegistrationTech int radioTech, MmTelCapabilities capabilities) {
conclude();
if (mLastRegistrationStats != null) {
mLastRegistrationStats.rat = convertRegistrationTechToNetworkType(radioTech);
}
mLastAvailableFeatures = capabilities;
}
/** Updates the stats when capable features changed. */
public synchronized void onSetFeatureResponse(
@MmTelCapability int feature, @ImsRegistrationTech int network, int value) {
MmTelCapabilities lastCapableFeatures = getLastCapableFeaturesForTech(network);
if (lastCapableFeatures != null) {
conclude();
if (value == ProvisioningManager.PROVISIONING_VALUE_ENABLED) {
lastCapableFeatures.addCapabilities(feature);
} else {
lastCapableFeatures.removeCapabilities(feature);
}
}
}
/** Updates the stats when IMS registration is progressing. */
public synchronized void onImsRegistering(@TransportType int imsRadioTech) {
conclude();
mLastRegistrationStats = getDefaultImsRegistrationStats();
mLastRegistrationStats.rat = convertTransportTypeToNetworkType(imsRadioTech);
mLastRegistrationState = REGISTRATION_STATE_REGISTERING;
}
/** Updates the stats when IMS registration succeeds. */
public synchronized void onImsRegistered(@TransportType int imsRadioTech) {
conclude();
// NOTE: mLastRegistrationStats can be null (no registering phase).
if (mLastRegistrationStats == null) {
mLastRegistrationStats = getDefaultImsRegistrationStats();
}
mLastRegistrationStats.rat = convertTransportTypeToNetworkType(imsRadioTech);
mLastRegistrationState = REGISTRATION_STATE_REGISTERED;
}
/** Updates the stats and generates a termination atom when IMS registration fails/ends. */
public synchronized void onImsUnregistered(ImsReasonInfo reasonInfo) {
conclude();
// Generate end reason atom.
// NOTE: mLastRegistrationStats can be null (no registering phase).
ImsRegistrationTermination termination = new ImsRegistrationTermination();
if (mLastRegistrationStats != null) {
termination.carrierId = mLastRegistrationStats.carrierId;
termination.ratAtEnd = getRatAtEnd(mLastRegistrationStats.rat);
} else {
termination.carrierId = mPhone.getDefaultPhone().getCarrierId();
// We cannot tell whether the registration was intended for WWAN or WLAN
termination.ratAtEnd = TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
termination.isMultiSim = SimSlotState.isMultiSim();
termination.setupFailed = (mLastRegistrationState != REGISTRATION_STATE_REGISTERED);
termination.reasonCode = reasonInfo.getCode();
termination.extraCode = reasonInfo.getExtraCode();
termination.extraMessage = filterExtraMessage(reasonInfo.getExtraMessage());
termination.count = 1;
mStorage.addImsRegistrationTermination(termination);
// Reset state to unregistered.
mLastRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
mLastRegistrationStats = null;
mLastAvailableFeatures = new MmTelCapabilities();
}
@NetworkType
private int getRatAtEnd(@NetworkType int lastStateRat) {
return lastStateRat == TelephonyManager.NETWORK_TYPE_IWLAN ? lastStateRat : getWwanPsRat();
}
@NetworkType
private int convertTransportTypeToNetworkType(@TransportType int transportType) {
switch (transportType) {
case AccessNetworkConstants.TRANSPORT_TYPE_WWAN:
return getWwanPsRat();
case AccessNetworkConstants.TRANSPORT_TYPE_WLAN:
return TelephonyManager.NETWORK_TYPE_IWLAN;
default:
return TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
}
@NetworkType
private int getWwanPsRat() {
ServiceState state = mPhone.getServiceStateTracker().getServiceState();
final NetworkRegistrationInfo wwanRegInfo =
state.getNetworkRegistrationInfo(
NetworkRegistrationInfo.DOMAIN_PS,
AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
return wwanRegInfo != null
? wwanRegInfo.getAccessNetworkTechnology()
: TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
private ImsRegistrationStats getDefaultImsRegistrationStats() {
Phone phone = mPhone.getDefaultPhone();
ImsRegistrationStats stats = new ImsRegistrationStats();
stats.carrierId = phone.getCarrierId();
stats.simSlotIndex = phone.getPhoneId();
return stats;
}
@Nullable
private MmTelCapabilities getLastCapableFeaturesForTech(@ImsRegistrationTech int radioTech) {
switch (radioTech) {
case REGISTRATION_TECH_NONE:
return null;
case REGISTRATION_TECH_IWLAN:
return mLastWlanCapableFeatures;
default:
return mLastWwanCapableFeatures;
}
}
@NetworkType
private int convertRegistrationTechToNetworkType(@ImsRegistrationTech int radioTech) {
switch (radioTech) {
case REGISTRATION_TECH_NONE:
return TelephonyManager.NETWORK_TYPE_UNKNOWN;
case REGISTRATION_TECH_LTE:
return TelephonyManager.NETWORK_TYPE_LTE;
case REGISTRATION_TECH_IWLAN:
return TelephonyManager.NETWORK_TYPE_IWLAN;
case REGISTRATION_TECH_NR:
return TelephonyManager.NETWORK_TYPE_NR;
default:
loge("convertRegistrationTechToNetworkType: unknown radio tech %d", radioTech);
return getWwanPsRat();
}
}
private static ImsRegistrationStats copyOf(ImsRegistrationStats source) {
ImsRegistrationStats dest = new ImsRegistrationStats();
dest.carrierId = source.carrierId;
dest.simSlotIndex = source.simSlotIndex;
dest.rat = source.rat;
dest.registeredMillis = source.registeredMillis;
dest.voiceCapableMillis = source.voiceCapableMillis;
dest.voiceAvailableMillis = source.voiceAvailableMillis;
dest.smsCapableMillis = source.smsCapableMillis;
dest.smsAvailableMillis = source.smsAvailableMillis;
dest.videoCapableMillis = source.videoCapableMillis;
dest.videoAvailableMillis = source.videoAvailableMillis;
dest.utCapableMillis = source.utCapableMillis;
dest.utAvailableMillis = source.utAvailableMillis;
return dest;
}
@VisibleForTesting
protected long getTimeMillis() {
return SystemClock.elapsedRealtime();
}
/** Filters IMS extra messages to ensure length limit and remove IDs. */
public static String filterExtraMessage(@Nullable String str) {
if (str == null) {
return "";
}
str = PATTERN_UUID.matcher(str).replaceAll(REPLACEMENT_UUID);
str = PATTERN_URI.matcher(str).replaceAll(REPLACEMENT_URI);
str = PATTERN_HOSTNAME.matcher(str).replaceAll(REPLACEMENT_HOSTNAME);
str = PATTERN_IPV4.matcher(str).replaceAll(REPLACEMENT_IPV4);
str = PATTERN_IPV6.matcher(str).replaceAll(REPLACEMENT_IPV6);
str = PATTERN_IMEI.matcher(str).replaceAll(REPLACEMENT_IMEI);
str = PATTERN_UNSEGMENTED_IMEI_IMSI.matcher(str)
.replaceAll(REPLACEMENT_UNSEGMENTED_IMEI_IMSI);
str = PATTERN_UNKNOWN_ID.matcher(str).replaceAll(REPLACEMENT_UNKNOWN_ID);
return str.length() > MAX_EXTRA_MESSAGE_LENGTH
? str.substring(0, MAX_EXTRA_MESSAGE_LENGTH)
: str;
}
private void logw(String format, Object... args) {
Rlog.w(TAG, "[" + mPhone.getPhoneId() + "] " + String.format(format, args));
}
private void loge(String format, Object... args) {
Rlog.e(TAG, "[" + mPhone.getPhoneId() + "] " + String.format(format, args));
}
}