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.entitlement.EntitlementConfiguration; 30 import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior; 31 import com.android.imsserviceentitlement.entitlement.EntitlementResult; 32 import com.android.imsserviceentitlement.fcm.FcmTokenStore; 33 import com.android.imsserviceentitlement.fcm.FcmUtils; 34 import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; 35 import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode; 36 import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus; 37 import com.android.imsserviceentitlement.ts43.Ts43VolteStatus; 38 import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; 39 import com.android.imsserviceentitlement.utils.TelephonyUtils; 40 import com.android.imsserviceentitlement.utils.XmlDoc; 41 import com.android.libraries.entitlement.CarrierConfig; 42 import com.android.libraries.entitlement.ServiceEntitlement; 43 import com.android.libraries.entitlement.ServiceEntitlementException; 44 import com.android.libraries.entitlement.ServiceEntitlementRequest; 45 46 import com.google.common.collect.ImmutableList; 47 import com.google.common.net.HttpHeaders; 48 49 import java.time.Clock; 50 import java.time.Instant; 51 import java.time.format.DateTimeParseException; 52 53 /** Implementation of the entitlement API. */ 54 public class ImsEntitlementApi { 55 private static final String TAG = "IMSSE-ImsEntitlementApi"; 56 57 private static final int RESPONSE_RETRY_AFTER = 503; 58 private static final int RESPONSE_TOKEN_EXPIRED = 511; 59 60 private static final int AUTHENTICATION_RETRIES = 1; 61 62 private final Context mContext; 63 private final int mSubId; 64 private final ServiceEntitlement mServiceEntitlement; 65 private final EntitlementConfiguration mLastEntitlementConfiguration; 66 67 private int mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; 68 private boolean mNeedsImsProvisioning; 69 70 @VisibleForTesting 71 static Clock sClock = Clock.systemUTC(); 72 ImsEntitlementApi(Context context, int subId)73 public ImsEntitlementApi(Context context, int subId) { 74 this.mContext = context; 75 this.mSubId = subId; 76 CarrierConfig carrierConfig = getCarrierConfig(context); 77 this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired(context, subId); 78 this.mServiceEntitlement = new ServiceEntitlement(context, carrierConfig, subId); 79 this.mLastEntitlementConfiguration = new EntitlementConfiguration(context, subId); 80 } 81 82 @VisibleForTesting ImsEntitlementApi( Context context, int subId, boolean needsImsProvisioning, ServiceEntitlement serviceEntitlement, EntitlementConfiguration lastEntitlementConfiguration)83 ImsEntitlementApi( 84 Context context, 85 int subId, 86 boolean needsImsProvisioning, 87 ServiceEntitlement serviceEntitlement, 88 EntitlementConfiguration lastEntitlementConfiguration) { 89 this.mContext = context; 90 this.mSubId = subId; 91 this.mNeedsImsProvisioning = needsImsProvisioning; 92 this.mServiceEntitlement = serviceEntitlement; 93 this.mLastEntitlementConfiguration = lastEntitlementConfiguration; 94 } 95 96 /** 97 * Returns WFC entitlement check result from carrier API (over network), or {@code null} on 98 * unrecoverable network issue or malformed server response. This is blocking call so should 99 * not be called on main thread. 100 */ 101 @Nullable checkEntitlementStatus()102 public EntitlementResult checkEntitlementStatus() { 103 Log.d(TAG, "checkEntitlementStatus subId=" + mSubId); 104 ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder(); 105 mLastEntitlementConfiguration.getToken().ifPresent( 106 token -> requestBuilder.setAuthenticationToken(token)); 107 FcmUtils.fetchFcmToken(mContext, mSubId); 108 requestBuilder.setNotificationToken(FcmTokenStore.getToken(mContext, mSubId)); 109 // Set fake device info to avoid leaking 110 requestBuilder.setTerminalVendor("vendorX"); 111 requestBuilder.setTerminalModel("modelY"); 112 requestBuilder.setTerminalSoftwareVersion("versionZ"); 113 requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML); 114 if (mNeedsImsProvisioning) { 115 mLastEntitlementConfiguration.getVersion().ifPresent( 116 version -> requestBuilder.setConfigurationVersion(Integer.parseInt(version))); 117 } 118 ServiceEntitlementRequest request = requestBuilder.build(); 119 120 XmlDoc entitlementXmlDoc = null; 121 122 try { 123 String rawXml = mServiceEntitlement.queryEntitlementStatus( 124 mNeedsImsProvisioning 125 ? ImmutableList.of( 126 ServiceEntitlement.APP_VOWIFI, 127 ServiceEntitlement.APP_VOLTE, 128 ServiceEntitlement.APP_SMSOIP) 129 : ImmutableList.of(ServiceEntitlement.APP_VOWIFI), 130 request); 131 entitlementXmlDoc = new XmlDoc(rawXml); 132 mLastEntitlementConfiguration.update(rawXml); 133 // Reset the retry count if no exception from queryEntitlementStatus() 134 mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; 135 } catch (ServiceEntitlementException e) { 136 if (e.getErrorCode() == ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS) { 137 if (e.getHttpStatus() == RESPONSE_TOKEN_EXPIRED) { 138 if (mRetryFullAuthenticationCount <= 0) { 139 Log.d(TAG, "Ran out of the retry count, stop query status."); 140 return null; 141 } 142 Log.d(TAG, "Server asking for full authentication, retry the query."); 143 // Clean up the cached data and perform full authentication next query. 144 mLastEntitlementConfiguration.reset(); 145 mRetryFullAuthenticationCount--; 146 return checkEntitlementStatus(); 147 } else if (e.getHttpStatus() == RESPONSE_RETRY_AFTER && !TextUtils.isEmpty( 148 e.getRetryAfter())) { 149 // For handling the case of HTTP_UNAVAILABLE(503), client would perform the 150 // retry for the delay of Retry-After. 151 Log.d(TAG, "Server asking for retry. retryAfter = " + e.getRetryAfter()); 152 return EntitlementResult 153 .builder() 154 .setRetryAfterSeconds(parseDelaySecondsByRetryAfter(e.getRetryAfter())) 155 .build(); 156 } 157 } 158 Log.e(TAG, "queryEntitlementStatus failed", e); 159 } 160 return entitlementXmlDoc == null ? null : toEntitlementResult(entitlementXmlDoc); 161 } 162 163 /** 164 * Parses the value of {@link HttpHeaders#RETRY_AFTER}. The possible formats could be a numeric 165 * value in second, or a HTTP-date in RFC-1123 date-time format. 166 */ parseDelaySecondsByRetryAfter(String retryAfter)167 private long parseDelaySecondsByRetryAfter(String retryAfter) { 168 try { 169 return Long.parseLong(retryAfter); 170 } catch (NumberFormatException numberFormatException) { 171 } 172 173 try { 174 return SECONDS.between( 175 Instant.now(sClock), RFC_1123_DATE_TIME.parse(retryAfter, Instant::from)); 176 } catch (DateTimeParseException dateTimeParseException) { 177 } 178 179 Log.w(TAG, "Unable to parse retry-after: " + retryAfter + ", ignore it."); 180 return -1; 181 } 182 toEntitlementResult(XmlDoc doc)183 private EntitlementResult toEntitlementResult(XmlDoc doc) { 184 EntitlementResult.Builder builder = EntitlementResult.builder(); 185 ClientBehavior clientBehavior = mLastEntitlementConfiguration.entitlementValidation(); 186 187 if (mNeedsImsProvisioning && isResetToDefault(clientBehavior)) { 188 // keep the entitlement result in default value and reset the configs. 189 if (clientBehavior == ClientBehavior.NEEDS_TO_RESET 190 || clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR) { 191 mLastEntitlementConfiguration.reset(); 192 } else { 193 mLastEntitlementConfiguration.resetConfigsExceptVers(); 194 } 195 } else { 196 builder.setVowifiStatus(Ts43VowifiStatus.builder(doc).build()) 197 .setVolteStatus(Ts43VolteStatus.builder(doc).build()) 198 .setSmsoveripStatus(Ts43SmsOverIpStatus.builder(doc).build()); 199 doc.get( 200 ResponseXmlNode.APPLICATION, 201 ResponseXmlAttributes.SERVER_FLOW_URL, 202 ServiceEntitlement.APP_VOWIFI) 203 .ifPresent(url -> builder.setEmergencyAddressWebUrl(url)); 204 doc.get( 205 ResponseXmlNode.APPLICATION, 206 ResponseXmlAttributes.SERVER_FLOW_USER_DATA, 207 ServiceEntitlement.APP_VOWIFI) 208 .ifPresent(userData -> builder.setEmergencyAddressWebData(userData)); 209 } 210 return builder.build(); 211 } 212 isResetToDefault(ClientBehavior clientBehavior)213 private boolean isResetToDefault(ClientBehavior clientBehavior) { 214 return clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR 215 || clientBehavior == ClientBehavior.NEEDS_TO_RESET 216 || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS 217 || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON; 218 } 219 getCarrierConfig(Context context)220 private CarrierConfig getCarrierConfig(Context context) { 221 String entitlementServiceUrl = TelephonyUtils.getEntitlementServerUrl(context, mSubId); 222 return CarrierConfig.builder().setServerUrl(entitlementServiceUrl).build(); 223 } 224 } 225