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.Ts43Constants.ResponseXmlNode; 37 import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus; 38 import com.android.imsserviceentitlement.ts43.Ts43VolteStatus; 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 // Set fake device info to avoid leaking 117 requestBuilder.setTerminalVendor("vendorX"); 118 requestBuilder.setTerminalModel("modelY"); 119 requestBuilder.setTerminalSoftwareVersion("versionZ"); 120 requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML); 121 if (mNeedsImsProvisioning) { 122 requestBuilder.setConfigurationVersion( 123 Integer.parseInt(mLastEntitlementConfiguration.getVersion())); 124 } 125 ServiceEntitlementRequest request = requestBuilder.build(); 126 127 XmlDoc entitlementXmlDoc = null; 128 129 try { 130 String rawXml = mServiceEntitlement.queryEntitlementStatus( 131 mNeedsImsProvisioning 132 ? ImmutableList.of( 133 ServiceEntitlement.APP_VOWIFI, 134 ServiceEntitlement.APP_VOLTE, 135 ServiceEntitlement.APP_SMSOIP) 136 : ImmutableList.of(ServiceEntitlement.APP_VOWIFI), 137 request); 138 entitlementXmlDoc = new XmlDoc(rawXml); 139 mLastEntitlementConfiguration.update(rawXml); 140 // Reset the retry count if no exception from queryEntitlementStatus() 141 mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; 142 } catch (ServiceEntitlementException e) { 143 if (e.getErrorCode() == ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS) { 144 if (e.getHttpStatus() == RESPONSE_TOKEN_EXPIRED) { 145 if (mRetryFullAuthenticationCount <= 0) { 146 Log.d(TAG, "Ran out of the retry count, stop query status."); 147 return null; 148 } 149 Log.d(TAG, "Server asking for full authentication, retry the query."); 150 // Clean up the cached data and perform full authentication next query. 151 mLastEntitlementConfiguration.reset(); 152 mRetryFullAuthenticationCount--; 153 return checkEntitlementStatus(); 154 } else if (e.getHttpStatus() == RESPONSE_RETRY_AFTER && !TextUtils.isEmpty( 155 e.getRetryAfter())) { 156 // For handling the case of HTTP_UNAVAILABLE(503), client would perform the 157 // retry for the delay of Retry-After. 158 Log.d(TAG, "Server asking for retry. retryAfter = " + e.getRetryAfter()); 159 return EntitlementResult 160 .builder() 161 .setRetryAfterSeconds(parseDelaySecondsByRetryAfter(e.getRetryAfter())) 162 .build(); 163 } 164 } 165 Log.e(TAG, "queryEntitlementStatus failed", e); 166 } 167 return entitlementXmlDoc == null ? null : toEntitlementResult(entitlementXmlDoc); 168 } 169 170 /** 171 * Parses the value of {@link HttpHeaders#RETRY_AFTER}. The possible formats could be a numeric 172 * value in second, or a HTTP-date in RFC-1123 date-time format. 173 */ parseDelaySecondsByRetryAfter(String retryAfter)174 private long parseDelaySecondsByRetryAfter(String retryAfter) { 175 try { 176 return Long.parseLong(retryAfter); 177 } catch (NumberFormatException numberFormatException) { 178 } 179 180 try { 181 return SECONDS.between( 182 Instant.now(sClock), RFC_1123_DATE_TIME.parse(retryAfter, Instant::from)); 183 } catch (DateTimeParseException dateTimeParseException) { 184 } 185 186 Log.w(TAG, "Unable to parse retry-after: " + retryAfter + ", ignore it."); 187 return -1; 188 } 189 toEntitlementResult(XmlDoc doc)190 private EntitlementResult toEntitlementResult(XmlDoc doc) { 191 EntitlementResult.Builder builder = EntitlementResult.builder(); 192 ClientBehavior clientBehavior = mLastEntitlementConfiguration.entitlementValidation(); 193 194 if (mNeedsImsProvisioning && isResetToDefault(clientBehavior)) { 195 // keep the entitlement result in default value and reset the configs. 196 if (clientBehavior == ClientBehavior.NEEDS_TO_RESET 197 || clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR) { 198 mLastEntitlementConfiguration.reset(); 199 } else { 200 mLastEntitlementConfiguration.resetConfigsExceptVers(); 201 } 202 } else { 203 builder.setVowifiStatus(Ts43VowifiStatus.builder(doc).build()) 204 .setVolteStatus(Ts43VolteStatus.builder(doc).build()) 205 .setSmsoveripStatus(Ts43SmsOverIpStatus.builder(doc).build()); 206 doc.get( 207 ResponseXmlNode.APPLICATION, 208 ResponseXmlAttributes.SERVER_FLOW_URL, 209 ServiceEntitlement.APP_VOWIFI) 210 .ifPresent(url -> builder.setEmergencyAddressWebUrl(url)); 211 doc.get( 212 ResponseXmlNode.APPLICATION, 213 ResponseXmlAttributes.SERVER_FLOW_USER_DATA, 214 ServiceEntitlement.APP_VOWIFI) 215 .ifPresent(userData -> builder.setEmergencyAddressWebData(userData)); 216 } 217 return builder.build(); 218 } 219 isResetToDefault(ClientBehavior clientBehavior)220 private boolean isResetToDefault(ClientBehavior clientBehavior) { 221 return clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR 222 || clientBehavior == ClientBehavior.NEEDS_TO_RESET 223 || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS 224 || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON; 225 } 226 getCarrierConfig(Context context)227 private CarrierConfig getCarrierConfig(Context context) { 228 String entitlementServiceUrl = TelephonyUtils.getEntitlementServerUrl(context, mSubId); 229 return CarrierConfig.builder().setServerUrl(entitlementServiceUrl).build(); 230 } 231 } 232