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