blob: 7a906fd8728aff5308f820d28578aada5442acc2 [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.imsserviceentitlement;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static java.time.temporal.ChronoUnit.SECONDS;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration;
import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior;
import com.android.imsserviceentitlement.entitlement.EntitlementResult;
import com.android.imsserviceentitlement.fcm.FcmTokenStore;
import com.android.imsserviceentitlement.fcm.FcmUtils;
import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes;
import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode;
import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus;
import com.android.imsserviceentitlement.ts43.Ts43VolteStatus;
import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus;
import com.android.imsserviceentitlement.utils.TelephonyUtils;
import com.android.imsserviceentitlement.utils.XmlDoc;
import com.android.libraries.entitlement.CarrierConfig;
import com.android.libraries.entitlement.ServiceEntitlement;
import com.android.libraries.entitlement.ServiceEntitlementException;
import com.android.libraries.entitlement.ServiceEntitlementRequest;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HttpHeaders;
import java.time.Clock;
import java.time.Instant;
import java.time.format.DateTimeParseException;
/** Implementation of the entitlement API. */
public class ImsEntitlementApi {
private static final String TAG = "IMSSE-ImsEntitlementApi";
private static final int RESPONSE_RETRY_AFTER = 503;
private static final int RESPONSE_TOKEN_EXPIRED = 511;
private static final int AUTHENTICATION_RETRIES = 1;
private final Context mContext;
private final int mSubId;
private final ServiceEntitlement mServiceEntitlement;
private final EntitlementConfiguration mLastEntitlementConfiguration;
private int mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES;
private boolean mNeedsImsProvisioning;
@VisibleForTesting
static Clock sClock = Clock.systemUTC();
public ImsEntitlementApi(Context context, int subId) {
this.mContext = context;
this.mSubId = subId;
CarrierConfig carrierConfig = getCarrierConfig(context);
this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired(context, subId);
this.mServiceEntitlement = new ServiceEntitlement(context, carrierConfig, subId);
this.mLastEntitlementConfiguration = new EntitlementConfiguration(context, subId);
}
@VisibleForTesting
ImsEntitlementApi(
Context context,
int subId,
boolean needsImsProvisioning,
ServiceEntitlement serviceEntitlement,
EntitlementConfiguration lastEntitlementConfiguration) {
this.mContext = context;
this.mSubId = subId;
this.mNeedsImsProvisioning = needsImsProvisioning;
this.mServiceEntitlement = serviceEntitlement;
this.mLastEntitlementConfiguration = lastEntitlementConfiguration;
}
/**
* Returns WFC entitlement check result from carrier API (over network), or {@code null} on
* unrecoverable network issue or malformed server response. This is blocking call so should
* not be called on main thread.
*/
@Nullable
public EntitlementResult checkEntitlementStatus() {
Log.d(TAG, "checkEntitlementStatus subId=" + mSubId);
ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder();
mLastEntitlementConfiguration.getToken().ifPresent(
token -> requestBuilder.setAuthenticationToken(token));
FcmUtils.fetchFcmToken(mContext, mSubId);
requestBuilder.setNotificationToken(FcmTokenStore.getToken(mContext, mSubId));
// Set fake device info to avoid leaking
requestBuilder.setTerminalVendor("vendorX");
requestBuilder.setTerminalModel("modelY");
requestBuilder.setTerminalSoftwareVersion("versionZ");
requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML);
if (mNeedsImsProvisioning) {
mLastEntitlementConfiguration.getVersion().ifPresent(
version -> requestBuilder.setConfigurationVersion(Integer.parseInt(version)));
}
ServiceEntitlementRequest request = requestBuilder.build();
XmlDoc entitlementXmlDoc = null;
try {
String rawXml = mServiceEntitlement.queryEntitlementStatus(
mNeedsImsProvisioning
? ImmutableList.of(
ServiceEntitlement.APP_VOWIFI,
ServiceEntitlement.APP_VOLTE,
ServiceEntitlement.APP_SMSOIP)
: ImmutableList.of(ServiceEntitlement.APP_VOWIFI),
request);
entitlementXmlDoc = new XmlDoc(rawXml);
mLastEntitlementConfiguration.update(rawXml);
// Reset the retry count if no exception from queryEntitlementStatus()
mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES;
} catch (ServiceEntitlementException e) {
if (e.getErrorCode() == ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS) {
if (e.getHttpStatus() == RESPONSE_TOKEN_EXPIRED) {
if (mRetryFullAuthenticationCount <= 0) {
Log.d(TAG, "Ran out of the retry count, stop query status.");
return null;
}
Log.d(TAG, "Server asking for full authentication, retry the query.");
// Clean up the cached data and perform full authentication next query.
mLastEntitlementConfiguration.reset();
mRetryFullAuthenticationCount--;
return checkEntitlementStatus();
} else if (e.getHttpStatus() == RESPONSE_RETRY_AFTER && !TextUtils.isEmpty(
e.getRetryAfter())) {
// For handling the case of HTTP_UNAVAILABLE(503), client would perform the
// retry for the delay of Retry-After.
Log.d(TAG, "Server asking for retry. retryAfter = " + e.getRetryAfter());
return EntitlementResult
.builder()
.setRetryAfterSeconds(parseDelaySecondsByRetryAfter(e.getRetryAfter()))
.build();
}
}
Log.e(TAG, "queryEntitlementStatus failed", e);
}
return entitlementXmlDoc == null ? null : toEntitlementResult(entitlementXmlDoc);
}
/**
* Parses the value of {@link HttpHeaders#RETRY_AFTER}. The possible formats could be a numeric
* value in second, or a HTTP-date in RFC-1123 date-time format.
*/
private long parseDelaySecondsByRetryAfter(String retryAfter) {
try {
return Long.parseLong(retryAfter);
} catch (NumberFormatException numberFormatException) {
}
try {
return SECONDS.between(
Instant.now(sClock), RFC_1123_DATE_TIME.parse(retryAfter, Instant::from));
} catch (DateTimeParseException dateTimeParseException) {
}
Log.w(TAG, "Unable to parse retry-after: " + retryAfter + ", ignore it.");
return -1;
}
private EntitlementResult toEntitlementResult(XmlDoc doc) {
EntitlementResult.Builder builder = EntitlementResult.builder();
ClientBehavior clientBehavior = mLastEntitlementConfiguration.entitlementValidation();
if (mNeedsImsProvisioning && isResetToDefault(clientBehavior)) {
// keep the entitlement result in default value and reset the configs.
if (clientBehavior == ClientBehavior.NEEDS_TO_RESET
|| clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR) {
mLastEntitlementConfiguration.reset();
} else {
mLastEntitlementConfiguration.resetConfigsExceptVers();
}
} else {
builder.setVowifiStatus(Ts43VowifiStatus.builder(doc).build())
.setVolteStatus(Ts43VolteStatus.builder(doc).build())
.setSmsoveripStatus(Ts43SmsOverIpStatus.builder(doc).build());
doc.get(
ResponseXmlNode.APPLICATION,
ResponseXmlAttributes.SERVER_FLOW_URL,
ServiceEntitlement.APP_VOWIFI)
.ifPresent(url -> builder.setEmergencyAddressWebUrl(url));
doc.get(
ResponseXmlNode.APPLICATION,
ResponseXmlAttributes.SERVER_FLOW_USER_DATA,
ServiceEntitlement.APP_VOWIFI)
.ifPresent(userData -> builder.setEmergencyAddressWebData(userData));
}
return builder.build();
}
private boolean isResetToDefault(ClientBehavior clientBehavior) {
return clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR
|| clientBehavior == ClientBehavior.NEEDS_TO_RESET
|| clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS
|| clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON;
}
private CarrierConfig getCarrierConfig(Context context) {
String entitlementServiceUrl = TelephonyUtils.getEntitlementServerUrl(context, mSubId);
return CarrierConfig.builder().setServerUrl(entitlementServiceUrl).build();
}
}