| /* |
| * 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(); |
| } |
| } |