1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.imsserviceentitlement; 18 19 import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; 20 import static java.time.temporal.ChronoUnit.SECONDS; 21 22 import android.content.Context; 23 import android.text.TextUtils; 24 import android.util.Log; 25 26 import androidx.annotation.Nullable; 27 import androidx.annotation.VisibleForTesting; 28 29 import com.android.imsserviceentitlement.debug.DebugUtils; 30 import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration; 31 import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior; 32 import com.android.imsserviceentitlement.entitlement.EntitlementResult; 33 import com.android.imsserviceentitlement.fcm.FcmTokenStore; 34 import com.android.imsserviceentitlement.fcm.FcmUtils; 35 import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; 36 import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus; 37 import com.android.imsserviceentitlement.ts43.Ts43VolteStatus; 38 import com.android.imsserviceentitlement.ts43.Ts43VonrStatus; 39 import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; 40 import com.android.imsserviceentitlement.utils.TelephonyUtils; 41 import com.android.imsserviceentitlement.utils.XmlDoc; 42 import com.android.libraries.entitlement.CarrierConfig; 43 import com.android.libraries.entitlement.ServiceEntitlement; 44 import com.android.libraries.entitlement.ServiceEntitlementException; 45 import com.android.libraries.entitlement.ServiceEntitlementRequest; 46 47 import com.google.common.collect.ImmutableList; 48 import com.google.common.net.HttpHeaders; 49 50 import java.time.Clock; 51 import java.time.Instant; 52 import java.time.format.DateTimeParseException; 53 54 /** Implementation of the entitlement API. */ 55 public class ImsEntitlementApi { 56 private static final String TAG = "IMSSE-ImsEntitlementApi"; 57 58 private static final int RESPONSE_RETRY_AFTER = 503; 59 private static final int RESPONSE_TOKEN_EXPIRED = 511; 60 61 private static final int AUTHENTICATION_RETRIES = 1; 62 63 private final Context mContext; 64 private final int mSubId; 65 private final ServiceEntitlement mServiceEntitlement; 66 private final EntitlementConfiguration mLastEntitlementConfiguration; 67 68 private int mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; 69 private boolean mNeedsImsProvisioning; 70 71 @VisibleForTesting 72 static Clock sClock = Clock.systemUTC(); 73 ImsEntitlementApi(Context context, int subId)74 public ImsEntitlementApi(Context context, int subId) { 75 this.mContext = context; 76 this.mSubId = subId; 77 CarrierConfig carrierConfig = getCarrierConfig(context); 78 this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired(context, subId); 79 this.mServiceEntitlement = 80 new ServiceEntitlement( 81 context, 82 carrierConfig, 83 subId, 84 /* saveHttpHistory = */ false, 85 DebugUtils.getBypassEapAkaResponse()); 86 this.mLastEntitlementConfiguration = new EntitlementConfiguration(context, subId); 87 } 88 89 @VisibleForTesting ImsEntitlementApi( Context context, int subId, boolean needsImsProvisioning, ServiceEntitlement serviceEntitlement, EntitlementConfiguration lastEntitlementConfiguration)90 ImsEntitlementApi( 91 Context context, 92 int subId, 93 boolean needsImsProvisioning, 94 ServiceEntitlement serviceEntitlement, 95 EntitlementConfiguration lastEntitlementConfiguration) { 96 this.mContext = context; 97 this.mSubId = subId; 98 this.mNeedsImsProvisioning = needsImsProvisioning; 99 this.mServiceEntitlement = serviceEntitlement; 100 this.mLastEntitlementConfiguration = lastEntitlementConfiguration; 101 } 102 103 /** 104 * Returns WFC entitlement check result from carrier API (over network), or {@code null} on 105 * unrecoverable network issue or malformed server response. This is blocking call so should 106 * not be called on main thread. 107 */ 108 @Nullable checkEntitlementStatus()109 public EntitlementResult checkEntitlementStatus() { 110 Log.d(TAG, "checkEntitlementStatus subId=" + mSubId); 111 ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder(); 112 mLastEntitlementConfiguration.getToken().ifPresent( 113 token -> requestBuilder.setAuthenticationToken(token)); 114 FcmUtils.fetchFcmToken(mContext, mSubId); 115 requestBuilder.setNotificationToken(FcmTokenStore.getToken(mContext, mSubId)); 116 int entitlementVersion = TelephonyUtils.getEntitlementVersion(mContext, mSubId); 117 requestBuilder.setEntitlementVersion(entitlementVersion + ".0"); 118 requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML); 119 if (mNeedsImsProvisioning) { 120 requestBuilder.setConfigurationVersion( 121 Integer.parseInt(mLastEntitlementConfiguration.getVersion())); 122 } 123 ServiceEntitlementRequest request = requestBuilder.build(); 124 125 XmlDoc entitlementXmlDoc = null; 126 127 try { 128 String rawXml = mServiceEntitlement.queryEntitlementStatus( 129 mNeedsImsProvisioning 130 ? ImmutableList.of( 131 ServiceEntitlement.APP_VOWIFI, 132 ServiceEntitlement.APP_VOLTE, 133 ServiceEntitlement.APP_SMSOIP) 134 : ImmutableList.of(ServiceEntitlement.APP_VOWIFI), 135 request); 136 entitlementXmlDoc = new XmlDoc(rawXml); 137 mLastEntitlementConfiguration.update(entitlementVersion, rawXml); 138 // Reset the retry count if no exception from queryEntitlementStatus() 139 mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; 140 } catch (ServiceEntitlementException e) { 141 if (e.getErrorCode() == ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS) { 142 if (e.getHttpStatus() == RESPONSE_TOKEN_EXPIRED) { 143 if (mRetryFullAuthenticationCount <= 0) { 144 Log.d(TAG, "Ran out of the retry count, stop query status."); 145 return null; 146 } 147 Log.d(TAG, "Server asking for full authentication, retry the query."); 148 // Clean up the cached data and perform full authentication next query. 149 mLastEntitlementConfiguration.reset(); 150 mRetryFullAuthenticationCount--; 151 return checkEntitlementStatus(); 152 } else if (e.getHttpStatus() == RESPONSE_RETRY_AFTER && !TextUtils.isEmpty( 153 e.getRetryAfter())) { 154 // For handling the case of HTTP_UNAVAILABLE(503), client would perform the 155 // retry for the delay of Retry-After. 156 Log.d(TAG, "Server asking for retry. retryAfter = " + e.getRetryAfter()); 157 boolean isDefaultActive = TelephonyUtils.getDefaultStatus(mContext, mSubId); 158 return EntitlementResult.builder(isDefaultActive) 159 .setRetryAfterSeconds(parseDelaySecondsByRetryAfter(e.getRetryAfter())) 160 .build(); 161 } 162 } 163 Log.e(TAG, "queryEntitlementStatus failed", e); 164 } 165 return entitlementXmlDoc == null ? null : toEntitlementResult(entitlementXmlDoc); 166 } 167 168 /** 169 * Parses the value of {@link HttpHeaders#RETRY_AFTER}. The possible formats could be a numeric 170 * value in second, or a HTTP-date in RFC-1123 date-time format. 171 */ parseDelaySecondsByRetryAfter(String retryAfter)172 private long parseDelaySecondsByRetryAfter(String retryAfter) { 173 try { 174 return Long.parseLong(retryAfter); 175 } catch (NumberFormatException numberFormatException) { 176 } 177 178 try { 179 return SECONDS.between( 180 Instant.now(sClock), RFC_1123_DATE_TIME.parse(retryAfter, Instant::from)); 181 } catch (DateTimeParseException dateTimeParseException) { 182 } 183 184 Log.w(TAG, "Unable to parse retry-after: " + retryAfter + ", ignore it."); 185 return -1; 186 } 187 toEntitlementResult(XmlDoc doc)188 private EntitlementResult toEntitlementResult(XmlDoc doc) { 189 boolean isDefaultActive = TelephonyUtils.getDefaultStatus(mContext, mSubId); 190 EntitlementResult.Builder builder = EntitlementResult.builder(isDefaultActive); 191 ClientBehavior clientBehavior = mLastEntitlementConfiguration.entitlementValidation(); 192 193 if (mNeedsImsProvisioning && isResetToDefault(clientBehavior)) { 194 // keep the entitlement result in default value and reset the configs. 195 mLastEntitlementConfiguration.reset(clientBehavior); 196 } else { 197 builder.setVowifiStatus(Ts43VowifiStatus.builder(doc).build()) 198 .setVolteStatus(Ts43VolteStatus.builder(doc).build()) 199 .setVonrStatus(Ts43VonrStatus.builder(doc).build()) 200 .setSmsoveripStatus(Ts43SmsOverIpStatus.builder(doc).build()); 201 doc.getFromVowifi(ResponseXmlAttributes.SERVER_FLOW_URL) 202 .ifPresent(url -> builder.setEmergencyAddressWebUrl(url)); 203 doc.getFromVowifi(ResponseXmlAttributes.SERVER_FLOW_USER_DATA) 204 .ifPresent(userData -> builder.setEmergencyAddressWebData(userData)); 205 } 206 return builder.build(); 207 } 208 isResetToDefault(ClientBehavior clientBehavior)209 private boolean isResetToDefault(ClientBehavior clientBehavior) { 210 return clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR 211 || clientBehavior == ClientBehavior.NEEDS_TO_RESET 212 || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS 213 || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON; 214 } 215 getCarrierConfig(Context context)216 private CarrierConfig getCarrierConfig(Context context) { 217 String entitlementServiceUrl = TelephonyUtils.getEntitlementServerUrl(context, mSubId); 218 return CarrierConfig.builder() 219 .setClientTs43(CarrierConfig.CLIENT_TS_43_IMS_ENTITLEMENT) 220 .setServerUrl(entitlementServiceUrl) 221 .build(); 222 } 223 } 224