1 /*
2  * Copyright (C) 2023 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.libraries.entitlement;
18 
19 import android.content.Context;
20 import android.telephony.SubscriptionManager;
21 import android.telephony.TelephonyManager;
22 import android.text.TextUtils;
23 import android.util.Log;
24 
25 import androidx.annotation.IntDef;
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 
29 import com.android.libraries.entitlement.EsimOdsaOperation.OdsaServiceStatus;
30 import com.android.libraries.entitlement.http.HttpConstants;
31 import com.android.libraries.entitlement.odsa.AcquireConfigurationOperation.AcquireConfigurationRequest;
32 import com.android.libraries.entitlement.odsa.AcquireConfigurationOperation.AcquireConfigurationResponse;
33 import com.android.libraries.entitlement.odsa.AcquireTemporaryTokenOperation.AcquireTemporaryTokenRequest;
34 import com.android.libraries.entitlement.odsa.AcquireTemporaryTokenOperation.AcquireTemporaryTokenResponse;
35 import com.android.libraries.entitlement.odsa.CheckEligibilityOperation;
36 import com.android.libraries.entitlement.odsa.CheckEligibilityOperation.CheckEligibilityRequest;
37 import com.android.libraries.entitlement.odsa.CheckEligibilityOperation.CheckEligibilityResponse;
38 import com.android.libraries.entitlement.odsa.DownloadInfo;
39 import com.android.libraries.entitlement.odsa.GetPhoneNumberOperation.GetPhoneNumberRequest;
40 import com.android.libraries.entitlement.odsa.GetPhoneNumberOperation.GetPhoneNumberResponse;
41 import com.android.libraries.entitlement.odsa.ManageServiceOperation.ManageServiceRequest;
42 import com.android.libraries.entitlement.odsa.ManageServiceOperation.ManageServiceResponse;
43 import com.android.libraries.entitlement.odsa.ManageSubscriptionOperation.ManageSubscriptionRequest;
44 import com.android.libraries.entitlement.odsa.ManageSubscriptionOperation.ManageSubscriptionResponse;
45 import com.android.libraries.entitlement.odsa.MessageInfo;
46 import com.android.libraries.entitlement.odsa.OdsaResponse;
47 import com.android.libraries.entitlement.odsa.PlanOffer;
48 import com.android.libraries.entitlement.utils.Ts43Constants;
49 import com.android.libraries.entitlement.utils.Ts43XmlDoc;
50 
51 import com.google.common.base.Strings;
52 import com.google.common.collect.ImmutableList;
53 
54 import java.lang.annotation.Retention;
55 import java.lang.annotation.RetentionPolicy;
56 import java.net.MalformedURLException;
57 import java.net.URL;
58 import java.time.Instant;
59 import java.time.OffsetDateTime;
60 import java.time.format.DateTimeParseException;
61 import java.util.Arrays;
62 import java.util.Base64;
63 import java.util.Collections;
64 import java.util.List;
65 import java.util.Objects;
66 
67 /** TS43 operations described in GSMA Service Entitlement Configuration spec. */
68 public class Ts43Operation {
69     private static final String TAG = "Ts43";
70 
71     /**
72      * The normal token retrieved via {@link Ts43Authentication#getAuthToken(int, String, String,
73      * String)} or {@link Ts43Authentication#getAuthToken(URL)}.
74      */
75     public static final int TOKEN_TYPE_NORMAL = 1;
76 
77     /**
78      * The temporary token retrieved via {@link
79      * Ts43Operation#acquireTemporaryToken(AcquireTemporaryTokenRequest)}.
80      */
81     public static final int TOKEN_TYPE_TEMPORARY = 2;
82 
83     @Retention(RetentionPolicy.SOURCE)
84     @IntDef({TOKEN_TYPE_NORMAL, TOKEN_TYPE_TEMPORARY})
85     public @interface TokenType {
86     }
87 
88     /** The application context. */
89     @NonNull
90     private final Context mContext;
91 
92     /**
93      * The TS.43 entitlement version to use. For example, {@code "9.0"}. If {@code null}, version
94      * {@code "2.0"} will be used by default.
95      */
96     @NonNull
97     private final String mEntitlementVersion;
98 
99     /** The entitlement server address. */
100     @NonNull
101     private final URL mEntitlementServerAddress;
102 
103     /**
104      * The authentication token used for TS.43 operation. This token could be automatically updated
105      * after each TS.43 operation if the server provides the new token in the operation's HTTP
106      * response.
107      */
108     @Nullable
109     private String mAuthToken;
110 
111     /**
112      * The temporary token retrieved from {@link
113      * #acquireTemporaryToken(AcquireTemporaryTokenRequest)}.
114      */
115     @Nullable
116     private String mTemporaryToken;
117 
118     /**
119      * Token type. When token type is {@link #TOKEN_TYPE_NORMAL}, {@link #mAuthToken} is used. When
120      * toke type is {@link #TOKEN_TYPE_TEMPORARY}, {@link #mTemporaryToken} is used.
121      */
122     @TokenType
123     private int mTokenType;
124 
125     private final ServiceEntitlement mServiceEntitlement;
126 
127     /** IMEI of the device. */
128     private final String mImei;
129 
130     /**
131      * Constructor of Ts43Operation.
132      *
133      * @param slotIndex The logical SIM slot index involved in ODSA operation.
134      * @param entitlementServerAddress The entitlement server address.
135      * @param entitlementVersion The TS.43 entitlement version to use. For example,
136      *                           {@code "9.0"}. If {@code null}, version {@code "2.0"} will be used
137      *                           by default.
138      * @param authToken The authentication token.
139      * @param tokenType The token type. Can be {@link #TOKEN_TYPE_NORMAL} or
140      *                  {@link #TOKEN_TYPE_TEMPORARY}.
141      */
Ts43Operation( @onNull Context context, int slotIndex, @NonNull URL entitlementServerAddress, @Nullable String entitlementVersion, @NonNull String authToken, @TokenType int tokenType)142     public Ts43Operation(
143             @NonNull Context context,
144             int slotIndex,
145             @NonNull URL entitlementServerAddress,
146             @Nullable String entitlementVersion,
147             @NonNull String authToken,
148             @TokenType int tokenType) {
149         mContext = context;
150         mEntitlementServerAddress = entitlementServerAddress;
151         if (entitlementVersion != null) {
152             mEntitlementVersion = entitlementVersion;
153         } else {
154             mEntitlementVersion = Ts43Constants.DEFAULT_ENTITLEMENT_VERSION;
155         }
156 
157         if (tokenType == TOKEN_TYPE_NORMAL) {
158             mAuthToken = authToken;
159         } else if (tokenType == TOKEN_TYPE_TEMPORARY) {
160             mTemporaryToken = authToken;
161         } else {
162             throw new IllegalArgumentException("Invalid token type " + tokenType);
163         }
164         mTokenType = tokenType;
165 
166         CarrierConfig carrierConfig =
167                 CarrierConfig.builder().setServerUrl(mEntitlementServerAddress.toString()).build();
168 
169         mServiceEntitlement =
170                 new ServiceEntitlement(
171                         mContext, carrierConfig, SubscriptionManager.getSubscriptionId(slotIndex));
172 
173         String imei = null;
174         TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
175         if (telephonyManager != null) {
176             if (slotIndex < 0 || slotIndex >= telephonyManager.getActiveModemCount()) {
177                 throw new IllegalArgumentException("getAuthToken: invalid slot index " + slotIndex);
178             }
179             imei = telephonyManager.getImei(slotIndex);
180         }
181         mImei = Strings.nullToEmpty(imei);
182     }
183 
184     /**
185      * To verify if end-user is allowed to invoke the ODSA application as described in GSMA Service
186      * Entitlement Configuration section 6.2 and 6.5.2.
187      *
188      * @return {@code true} if the end-user is allowed to perform ODSA operation.
189      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
190      *                                     error from the server, the error code can be retrieved by
191      *                                     {@link ServiceEntitlementException#getHttpStatus()}
192      */
193     @NonNull
checkEligibility( @onNull CheckEligibilityRequest checkEligibilityRequest)194     public CheckEligibilityResponse checkEligibility(
195             @NonNull CheckEligibilityRequest checkEligibilityRequest)
196             throws ServiceEntitlementException {
197         Objects.requireNonNull(checkEligibilityRequest);
198 
199         ServiceEntitlementRequest.Builder builder =
200                 ServiceEntitlementRequest.builder()
201                         .setEntitlementVersion(mEntitlementVersion)
202                         .setTerminalId(mImei);
203 
204         if (mTokenType == TOKEN_TYPE_NORMAL) {
205             builder.setAuthenticationToken(mAuthToken);
206         } else if (mTokenType == TOKEN_TYPE_TEMPORARY) {
207             builder.setTemporaryToken(mTemporaryToken);
208         }
209 
210         String notificationToken = checkEligibilityRequest.notificationToken();
211         if (!TextUtils.isEmpty(notificationToken)) {
212             builder.setNotificationToken(notificationToken);
213         }
214         int notificationAction = checkEligibilityRequest.notificationAction();
215         if (Ts43Constants.isValidNotificationAction(notificationAction)) {
216             builder.setNotificationAction(notificationAction);
217         }
218 
219         ServiceEntitlementRequest request = builder.build();
220 
221         EsimOdsaOperation operation =
222                 EsimOdsaOperation.builder()
223                         .setOperation(EsimOdsaOperation.OPERATION_CHECK_ELIGIBILITY)
224                         .setCompanionTerminalId(checkEligibilityRequest.companionTerminalId())
225                         .setCompanionTerminalVendor(
226                                 checkEligibilityRequest.companionTerminalVendor())
227                         .setCompanionTerminalModel(checkEligibilityRequest.companionTerminalModel())
228                         .setCompanionTerminalSoftwareVersion(
229                                 checkEligibilityRequest.companionTerminalSoftwareVersion())
230                         .setCompanionTerminalFriendlyName(
231                                 checkEligibilityRequest.companionTerminalFriendlyName())
232                         .build();
233 
234         String rawXml;
235         try {
236             rawXml =
237                     mServiceEntitlement.performEsimOdsa(checkEligibilityRequest.appId(), request,
238                             operation);
239         } catch (ServiceEntitlementException e) {
240             Log.w(TAG, "manageSubscription: Failed to perform ODSA operation. e=" + e);
241             throw e;
242         }
243 
244         // Build the response of check eligibility operation. Refer to GSMA Service Entitlement
245         // Configuration section 6.5.2.
246         CheckEligibilityResponse.Builder responseBuilder = CheckEligibilityResponse.builder();
247 
248         Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml);
249 
250         try {
251             processGeneralResult(ts43XmlDoc, responseBuilder);
252         } catch (MalformedURLException e) {
253             throw new ServiceEntitlementException(
254                     ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE,
255                     "checkEligibility: Malformed URL " + rawXml);
256         }
257 
258         // Parse the eligibility
259         String eligibilityString =
260                 ts43XmlDoc.get(
261                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
262                         Ts43XmlDoc.Parm.PRIMARY_APP_ELIGIBILITY);
263         if (TextUtils.isEmpty(eligibilityString)) {
264             eligibilityString =
265                     ts43XmlDoc.get(
266                             ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
267                             Ts43XmlDoc.Parm.COMPANION_APP_ELIGIBILITY);
268         }
269 
270         int eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_UNKNOWN;
271         if (!TextUtils.isEmpty(eligibilityString)) {
272             switch (eligibilityString) {
273                 case Ts43XmlDoc.ParmValues.DISABLED:
274                     eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_DISABLED;
275                     break;
276                 case Ts43XmlDoc.ParmValues.ENABLED:
277                     eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_ENABLED;
278                     break;
279                 case Ts43XmlDoc.ParmValues.INCOMPATIBLE:
280                     eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_INCOMPATIBLE;
281                     break;
282             }
283         }
284         responseBuilder.setAppEligibility(eligibility);
285 
286         // Parse companion device services
287         String companionDeviceServices =
288                 ts43XmlDoc.get(
289                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
290                         Ts43XmlDoc.Parm.COMPANION_DEVICE_SERVICES);
291 
292         if (!TextUtils.isEmpty(companionDeviceServices)) {
293             List<String> companionDeviceServicesList =
294                     Arrays.asList(companionDeviceServices.split("\\s*,\\s*"));
295             responseBuilder.setCompanionDeviceServices(
296                     ImmutableList.copyOf(companionDeviceServicesList));
297         }
298 
299         // Parse notEnabledURL
300         URL notEnabledURL = null;
301         String notEnabledURLString =
302                 ts43XmlDoc.get(
303                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
304                         Ts43XmlDoc.Parm.NOT_ENABLED_URL);
305 
306         try {
307             notEnabledURL = new URL(notEnabledURLString);
308             responseBuilder.setNotEnabledUrl(notEnabledURL);
309         } catch (MalformedURLException e) {
310             Log.w(TAG, "checkEligibility: malformed URL " + notEnabledURLString);
311         }
312 
313         // Parse notEnabledUserData
314         String notEnabledUserData =
315                 ts43XmlDoc.get(
316                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
317                         Ts43XmlDoc.Parm.NOT_ENABLED_USER_DATA);
318 
319         if (!TextUtils.isEmpty(notEnabledUserData)) {
320             responseBuilder.setNotEnabledUserData(notEnabledUserData);
321         }
322 
323         // Parse notEnabledContentsType
324         String notEnabledContentsTypeString =
325                 ts43XmlDoc.get(
326                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
327                         Ts43XmlDoc.Parm.NOT_ENABLED_CONTENTS_TYPE);
328 
329         int notEnabledContentsType = HttpConstants.ContentType.UNKNOWN;
330         if (!TextUtils.isEmpty(notEnabledContentsTypeString)) {
331             switch (notEnabledContentsTypeString) {
332                 case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_XML:
333                     notEnabledContentsType = HttpConstants.ContentType.XML;
334                     break;
335                 case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_JSON:
336                     notEnabledContentsType = HttpConstants.ContentType.JSON;
337                     break;
338             }
339         }
340         responseBuilder.setNotEnabledContentsType(notEnabledContentsType);
341 
342         return responseBuilder.build();
343     }
344 
345     /**
346      * To request for subscription-related action on a primary or companion device as described in
347      * GSMA Service Entitlement Configuration section 6.2 and 6.5.3.
348      *
349      * @param manageSubscriptionRequest The manage subscription request.
350      * @return The response of manage subscription request.
351      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
352      *                                     error from the server, the error code can be retrieved by
353      *                                     {@link ServiceEntitlementException#getHttpStatus()}
354      */
355     @NonNull
manageSubscription( @onNull ManageSubscriptionRequest manageSubscriptionRequest)356     public ManageSubscriptionResponse manageSubscription(
357             @NonNull ManageSubscriptionRequest manageSubscriptionRequest)
358             throws ServiceEntitlementException {
359         Objects.requireNonNull(manageSubscriptionRequest);
360 
361         ServiceEntitlementRequest.Builder builder =
362                 ServiceEntitlementRequest.builder()
363                         .setEntitlementVersion(mEntitlementVersion)
364                         .setTerminalId(mImei)
365                         .setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML);
366 
367         if (mTokenType == TOKEN_TYPE_NORMAL) {
368             builder.setAuthenticationToken(mAuthToken);
369         } else if (mTokenType == TOKEN_TYPE_TEMPORARY) {
370             builder.setTemporaryToken(mTemporaryToken);
371         }
372 
373         String notificationToken = manageSubscriptionRequest.notificationToken();
374         if (!TextUtils.isEmpty(notificationToken)) {
375             builder.setNotificationToken(notificationToken);
376         }
377         int notificationAction = manageSubscriptionRequest.notificationAction();
378         if (Ts43Constants.isValidNotificationAction(notificationAction)) {
379             builder.setNotificationAction(notificationAction);
380         }
381 
382         ServiceEntitlementRequest request = builder.build();
383 
384         EsimOdsaOperation operation =
385                 EsimOdsaOperation.builder()
386                         .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION)
387                         .setOperationType(manageSubscriptionRequest.operationType())
388                         .setCompanionTerminalId(manageSubscriptionRequest.companionTerminalId())
389                         .setCompanionTerminalVendor(
390                                 manageSubscriptionRequest.companionTerminalVendor())
391                         .setCompanionTerminalModel(
392                                 manageSubscriptionRequest.companionTerminalModel())
393                         .setCompanionTerminalSoftwareVersion(
394                                 manageSubscriptionRequest.companionTerminalSoftwareVersion())
395                         .setCompanionTerminalFriendlyName(
396                                 manageSubscriptionRequest.companionTerminalFriendlyName())
397                         .setCompanionTerminalService(
398                                 manageSubscriptionRequest.companionTerminalService())
399                         .setCompanionTerminalIccid(
400                                 manageSubscriptionRequest.companionTerminalIccid())
401                         .setCompanionTerminalEid(manageSubscriptionRequest.companionTerminalEid())
402                         .setTerminalIccid(manageSubscriptionRequest.terminalIccid())
403                         .setTerminalEid(manageSubscriptionRequest.terminalEid())
404                         .setTargetTerminalId(manageSubscriptionRequest.targetTerminalId())
405                         // non TS.43 standard support
406                         .setTargetTerminalIds(manageSubscriptionRequest.targetTerminalIds())
407                         .setTargetTerminalIccid(manageSubscriptionRequest.targetTerminalIccid())
408                         .setTargetTerminalEid(manageSubscriptionRequest.targetTerminalEid())
409                         // non TS.43 standard support
410                         .setTargetTerminalSerialNumber(
411                                 manageSubscriptionRequest.targetTerminalSerialNumber())
412                         // non TS.43 standard support
413                         .setTargetTerminalModel(manageSubscriptionRequest.targetTerminalModel())
414                         .setOldTerminalId(manageSubscriptionRequest.oldTerminalId())
415                         .setOldTerminalIccid(manageSubscriptionRequest.oldTerminalIccid())
416                         .setMessageResponse(manageSubscriptionRequest.messageResponse())
417                         .setMessageButton(manageSubscriptionRequest.messageButton())
418                         .build();
419 
420         String rawXml;
421         try {
422             rawXml =
423                     mServiceEntitlement.performEsimOdsa(
424                             manageSubscriptionRequest.appId(), request, operation);
425         } catch (ServiceEntitlementException e) {
426             Log.w(TAG, "manageSubscription: Failed to perform ODSA operation. e=" + e);
427             throw e;
428         }
429 
430         // Build the response of manage subscription operation. Refer to GSMA Service Entitlement
431         // Configuration section 6.5.3.
432         ManageSubscriptionResponse.Builder responseBuilder = ManageSubscriptionResponse.builder();
433 
434         Ts43XmlDoc ts43XmlDoc;
435         try {
436             ts43XmlDoc = new Ts43XmlDoc(rawXml);
437             processGeneralResult(ts43XmlDoc, responseBuilder);
438         } catch (MalformedURLException e) {
439             throw new ServiceEntitlementException(
440                     ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE,
441                     "manageSubscription: Malformed URL " + rawXml);
442         }
443 
444         int subscriptionResult = ManageSubscriptionResponse.SUBSCRIPTION_RESULT_UNKNOWN;
445 
446         // Parse subscription result.
447         String subscriptionResultString =
448                 ts43XmlDoc.get(
449                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
450                         Ts43XmlDoc.Parm.SUBSCRIPTION_RESULT);
451 
452         if (!TextUtils.isEmpty(subscriptionResultString)) {
453             switch (subscriptionResultString) {
454                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET:
455                     subscriptionResult =
456                             ManageSubscriptionResponse.SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET;
457 
458                     String subscriptionServiceURLString =
459                             ts43XmlDoc.get(
460                                     ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
461                                     Ts43XmlDoc.Parm.SUBSCRIPTION_SERVICE_URL);
462 
463                     if (!TextUtils.isEmpty(subscriptionServiceURLString)) {
464                         try {
465                             responseBuilder.setSubscriptionServiceUrl(
466                                     new URL(subscriptionServiceURLString));
467 
468                             String subscriptionServiceUserDataString =
469                                     ts43XmlDoc.get(
470                                             ImmutableList.of(
471                                                     Ts43XmlDoc.CharacteristicType.APPLICATION),
472                                             Ts43XmlDoc.Parm.SUBSCRIPTION_SERVICE_USER_DATA);
473                             if (!TextUtils.isEmpty(subscriptionServiceUserDataString)) {
474                                 responseBuilder.setSubscriptionServiceUserData(
475                                         subscriptionServiceUserDataString);
476                             }
477 
478                             String subscriptionServiceContentsTypeString =
479                                     ts43XmlDoc.get(
480                                             ImmutableList.of(
481                                                     Ts43XmlDoc.CharacteristicType.APPLICATION),
482                                             Ts43XmlDoc.Parm.SUBSCRIPTION_SERVICE_CONTENTS_TYPE);
483                             if (!TextUtils.isEmpty(subscriptionServiceContentsTypeString)) {
484                                 int contentsType = HttpConstants.ContentType.UNKNOWN;
485                                 switch (subscriptionServiceContentsTypeString) {
486                                     case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_XML:
487                                         contentsType = HttpConstants.ContentType.XML;
488                                         break;
489                                     case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_JSON:
490                                         contentsType = HttpConstants.ContentType.JSON;
491                                         break;
492                                 }
493                                 responseBuilder.setSubscriptionServiceContentsType(contentsType);
494                             }
495                         } catch (MalformedURLException e) {
496                             Log.w(TAG, "Malformed URL received. " + subscriptionServiceURLString);
497                         }
498                     }
499                     break;
500                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE:
501                     subscriptionResult =
502                             ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE;
503                     DownloadInfo downloadInfo =
504                             parseDownloadInfo(
505                                     ImmutableList.of(
506                                             Ts43XmlDoc.CharacteristicType.APPLICATION,
507                                             Ts43XmlDoc.CharacteristicType.DOWNLOAD_INFO),
508                                     ts43XmlDoc);
509                     if (downloadInfo != null) {
510                         responseBuilder.setDownloadInfo(downloadInfo);
511                     }
512                     break;
513                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DONE:
514                     subscriptionResult = ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DONE;
515                     break;
516                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD:
517                     subscriptionResult =
518                             ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD;
519                     break;
520                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DISMISS:
521                     subscriptionResult = ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DISMISS;
522                     break;
523                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE:
524                     subscriptionResult =
525                             ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE;
526                     break;
527                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_REDOWNLOADABLE_PROFILE_IS_MANDATORY:
528                     subscriptionResult =
529                             ManageSubscriptionResponse
530                                     .SUBSCRIPTION_RESULT_REDOWNLOADABLE_PROFILE_IS_MANDATORY;
531                     break;
532                 case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_REQUIRES_USER_INPUT:
533                     subscriptionResult =
534                             ManageSubscriptionResponse.SUBSCRIPTION_RESULT_REQUIRES_USER_INPUT;
535                     break;
536             }
537         }
538 
539         responseBuilder.setSubscriptionResult(subscriptionResult);
540         return responseBuilder.build();
541     }
542 
543     /**
544      * To activate/deactivate the service on the primary or companion device as described in GSMA
545      * Service Entitlement Configuration section 6.2 and 6.5.4. This is an optional operation.
546      *
547      * @param manageServiceRequest The manage service request.
548      * @return The response of manage service request.
549      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
550      *                                     error from the server, the error code can be retrieved by
551      *                                     {@link ServiceEntitlementException#getHttpStatus()}
552      */
553     @NonNull
manageService(@onNull ManageServiceRequest manageServiceRequest)554     public ManageServiceResponse manageService(@NonNull ManageServiceRequest manageServiceRequest)
555             throws ServiceEntitlementException {
556         Objects.requireNonNull(manageServiceRequest);
557 
558         ServiceEntitlementRequest.Builder builder =
559                 ServiceEntitlementRequest.builder()
560                         .setEntitlementVersion(mEntitlementVersion)
561                         .setTerminalId(mImei);
562 
563         if (mTokenType == TOKEN_TYPE_NORMAL) {
564             builder.setAuthenticationToken(mAuthToken);
565         } else if (mTokenType == TOKEN_TYPE_TEMPORARY) {
566             builder.setTemporaryToken(mTemporaryToken);
567         }
568 
569         ServiceEntitlementRequest request = builder.build();
570 
571         EsimOdsaOperation operation =
572                 EsimOdsaOperation.builder()
573                         .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SERVICE)
574                         .setOperationType(manageServiceRequest.operationType())
575                         .setCompanionTerminalId(manageServiceRequest.companionTerminalId())
576                         .setCompanionTerminalVendor(manageServiceRequest.companionTerminalVendor())
577                         .setCompanionTerminalModel(manageServiceRequest.companionTerminalModel())
578                         .setCompanionTerminalSoftwareVersion(
579                                 manageServiceRequest.companionTerminalSoftwareVersion())
580                         .setCompanionTerminalFriendlyName(
581                                 manageServiceRequest.companionTerminalFriendlyName())
582                         .setCompanionTerminalService(
583                                 manageServiceRequest.companionTerminalService())
584                         .setCompanionTerminalIccid(manageServiceRequest.companionTerminalIccid())
585                         .build();
586 
587         String rawXml;
588         try {
589             rawXml =
590                     mServiceEntitlement.performEsimOdsa(manageServiceRequest.appId(), request,
591                             operation);
592         } catch (ServiceEntitlementException e) {
593             Log.w(TAG, "manageService: Failed to perform ODSA operation. e=" + e);
594             throw e;
595         }
596 
597         // Build the response of manage service operation. Refer to GSMA Service Entitlement
598         // Configuration section 6.5.4.
599         ManageServiceResponse.Builder responseBuilder = ManageServiceResponse.builder();
600 
601         Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml);
602 
603         try {
604             processGeneralResult(ts43XmlDoc, responseBuilder);
605         } catch (MalformedURLException e) {
606             throw new ServiceEntitlementException(
607                     ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE,
608                     "manageService: Malformed URL " + rawXml);
609         }
610 
611         // Parse service status.
612         String serviceStatusString =
613                 ts43XmlDoc.get(
614                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
615                         Ts43XmlDoc.Parm.SERVICE_STATUS);
616 
617         if (!TextUtils.isEmpty(serviceStatusString)) {
618             responseBuilder.setServiceStatus(getServiceStatusFromString(serviceStatusString));
619         }
620 
621         return responseBuilder.build();
622     }
623 
624     /**
625      * To provide service related data about a primary or companion device as described in GSMA
626      * Service Entitlement Configuration section 6.2 and 6.5.5.
627      *
628      * @param acquireConfigurationRequest The acquire configuration request.
629      * @return The response of acquire configuration request.
630      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
631      *                                     error from the server, the error code can be retrieved by
632      *                                     {@link ServiceEntitlementException#getHttpStatus()}
633      */
634     @NonNull
acquireConfiguration( @onNull AcquireConfigurationRequest acquireConfigurationRequest)635     public AcquireConfigurationResponse acquireConfiguration(
636             @NonNull AcquireConfigurationRequest acquireConfigurationRequest)
637             throws ServiceEntitlementException {
638         Objects.requireNonNull(acquireConfigurationRequest);
639 
640         ServiceEntitlementRequest.Builder builder = ServiceEntitlementRequest.builder()
641                 .setEntitlementVersion(mEntitlementVersion)
642                 .setTerminalId(mImei)
643                 .setAuthenticationToken(mAuthToken);
644 
645         String notificationToken = acquireConfigurationRequest.notificationToken();
646         if (!TextUtils.isEmpty(notificationToken)) {
647             builder.setNotificationToken(notificationToken);
648         }
649         int notificationAction = acquireConfigurationRequest.notificationAction();
650         if (Ts43Constants.isValidNotificationAction(notificationAction)) {
651             builder.setNotificationAction(notificationAction);
652         }
653 
654         ServiceEntitlementRequest request = builder.build();
655 
656         EsimOdsaOperation operation =
657                 EsimOdsaOperation.builder()
658                         .setOperation(EsimOdsaOperation.OPERATION_ACQUIRE_CONFIGURATION)
659                         .setCompanionTerminalId(acquireConfigurationRequest.companionTerminalId())
660                         .setCompanionTerminalIccid(
661                                 acquireConfigurationRequest.companionTerminalIccid())
662                         .setCompanionTerminalEid(acquireConfigurationRequest.companionTerminalEid())
663                         .setTerminalIccid(acquireConfigurationRequest.terminalIccid())
664                         .setTerminalEid(acquireConfigurationRequest.terminalEid())
665                         .setTargetTerminalId(acquireConfigurationRequest.targetTerminalId())
666                         .setTargetTerminalIccid(acquireConfigurationRequest.targetTerminalIccid())
667                         .setTargetTerminalEid(acquireConfigurationRequest.targetTerminalEid())
668                         .build();
669 
670         String rawXml;
671         try {
672             rawXml =
673                     mServiceEntitlement.performEsimOdsa(
674                             acquireConfigurationRequest.appId(), request, operation);
675         } catch (ServiceEntitlementException e) {
676             Log.w(TAG, "acquireConfiguration: Failed to perform ODSA operation. e=" + e);
677             throw e;
678         }
679 
680         AcquireConfigurationResponse.Builder responseBuilder =
681                 AcquireConfigurationResponse.builder();
682         AcquireConfigurationResponse.Configuration.Builder configBuilder =
683                 AcquireConfigurationResponse.Configuration.builder();
684 
685         Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml);
686 
687         try {
688             processGeneralResult(ts43XmlDoc, responseBuilder);
689         } catch (MalformedURLException e) {
690             throw new ServiceEntitlementException(
691                     ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE,
692                     "manageSubscription: Malformed URL " + rawXml);
693         }
694 
695         // Parse service status.
696         String serviceStatusString =
697                 ts43XmlDoc.get(
698                         ImmutableList.of(
699                                 Ts43XmlDoc.CharacteristicType.APPLICATION,
700                                 Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION),
701                         Ts43XmlDoc.Parm.SERVICE_STATUS);
702 
703         if (!TextUtils.isEmpty(serviceStatusString)) {
704             configBuilder.setServiceStatus(getServiceStatusFromString(serviceStatusString));
705         }
706 
707         // Parse ICCID
708         String iccIdString =
709                 ts43XmlDoc.get(
710                         ImmutableList.of(
711                                 Ts43XmlDoc.CharacteristicType.APPLICATION,
712                                 Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION),
713                         Ts43XmlDoc.Parm.ICCID);
714 
715         if (!TextUtils.isEmpty(iccIdString)) {
716             configBuilder.setIccid(iccIdString);
717         }
718 
719         // Parse polling interval
720         String pollingIntervalString =
721                 ts43XmlDoc.get(
722                         ImmutableList.of(
723                                 Ts43XmlDoc.CharacteristicType.APPLICATION,
724                                 Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION),
725                         Ts43XmlDoc.Parm.POLLING_INTERVAL);
726 
727         if (!TextUtils.isEmpty(pollingIntervalString)) {
728             try {
729                 configBuilder.setPollingInterval(Integer.parseInt(pollingIntervalString));
730             } catch (NumberFormatException e) {
731                 Log.w(
732                         TAG, "acquireConfiguration: Failed to parse polling interval "
733                                 + pollingIntervalString);
734             }
735         }
736 
737         // Parse download info
738         DownloadInfo downloadInfo =
739                 parseDownloadInfo(
740                         ImmutableList.of(
741                                 Ts43XmlDoc.CharacteristicType.APPLICATION,
742                                 Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION,
743                                 Ts43XmlDoc.CharacteristicType.DOWNLOAD_INFO),
744                         ts43XmlDoc);
745         if (downloadInfo != null) {
746             configBuilder.setDownloadInfo(downloadInfo);
747         }
748 
749         // Parse message info
750         MessageInfo messageInfo =
751                 parseMessageInfo(
752                         ImmutableList.of(
753                                 Ts43XmlDoc.CharacteristicType.APPLICATION,
754                                 Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION,
755                                 Ts43XmlDoc.CharacteristicType.MSG),
756                         ts43XmlDoc);
757         if (messageInfo != null) {
758             configBuilder.setMessageInfo(messageInfo);
759         }
760 
761         // TODO: Support different type of configuration.
762         configBuilder.setType(
763                 AcquireConfigurationResponse.Configuration.CONFIGURATION_TYPE_PRIMARY);
764 
765         // TODO: Support multiple configurations.
766         return responseBuilder.setConfigurations(ImmutableList.of(configBuilder.build())).build();
767     }
768 
769     /**
770      * Acquire available mobile plans to be offered by the MNO to a specific user or MDM as
771      * described in GSMA Service Entitlement Configuration section 6.2 and 6.5.6.
772      *
773      * @return List of mobile plans. Empty list if not available.
774      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
775      *                                     error from the server, the error code can be retrieved by
776      *                                     {@link ServiceEntitlementException#getHttpStatus()}
777      */
778     @NonNull
acquirePlans()779     public List<PlanOffer> acquirePlans() throws ServiceEntitlementException {
780         return Collections.emptyList();
781     }
782 
783     /**
784      * To request a temporary token used to establish trust between ECS and the client as described
785      * in GSMA Service Entitlement Configuration section 6.2 and 6.5.7.
786      *
787      * @param acquireTemporaryTokenRequest The acquire temporary token request.
788      * @return The temporary token response.
789      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
790      *                                     error from the server, the error code can be retrieved by
791      *                                     {@link ServiceEntitlementException#getHttpStatus()}
792      */
793     @NonNull
794     @SuppressWarnings("AndroidJdkLibsChecker") // java.time.Instant
acquireTemporaryToken( @onNull AcquireTemporaryTokenRequest acquireTemporaryTokenRequest)795     public AcquireTemporaryTokenResponse acquireTemporaryToken(
796             @NonNull AcquireTemporaryTokenRequest acquireTemporaryTokenRequest)
797             throws ServiceEntitlementException {
798         Objects.requireNonNull(acquireTemporaryTokenRequest);
799 
800         ServiceEntitlementRequest request =
801                 ServiceEntitlementRequest.builder()
802                         .setEntitlementVersion(mEntitlementVersion)
803                         .setTerminalId(mImei)
804                         .setAuthenticationToken(mAuthToken)
805                         .build();
806 
807         EsimOdsaOperation operation =
808                 EsimOdsaOperation.builder()
809                         .setOperation(EsimOdsaOperation.OPERATION_ACQUIRE_TEMPORARY_TOKEN)
810                         .setOperationTargets(acquireTemporaryTokenRequest.operationTargets())
811                         .setCompanionTerminalId(acquireTemporaryTokenRequest.companionTerminalId())
812                         .build();
813 
814         String rawXml;
815         try {
816             rawXml =
817                     mServiceEntitlement.performEsimOdsa(
818                             acquireTemporaryTokenRequest.appId(), request, operation);
819         } catch (ServiceEntitlementException e) {
820             Log.w(TAG, "acquireTemporaryToken: Failed to perform ODSA operation. e=" + e);
821             throw e;
822         }
823 
824         Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml);
825         AcquireTemporaryTokenResponse.Builder responseBuilder =
826                 AcquireTemporaryTokenResponse.builder();
827 
828         try {
829             processGeneralResult(ts43XmlDoc, responseBuilder);
830         } catch (MalformedURLException e) {
831             throw new ServiceEntitlementException(
832                     ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE,
833                     "AcquireTemporaryTokenResponse: Malformed URL " + rawXml);
834         }
835 
836         // Parse the operation targets.
837         String operationTargets =
838                 Strings.nullToEmpty(
839                         ts43XmlDoc.get(
840                                 ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
841                                 Ts43XmlDoc.Parm.OPERATION_TARGETS));
842 
843         if (operationTargets != null) {
844             List<String> operationTargetsList = Arrays.asList(operationTargets.split("\\s*,\\s*"));
845             responseBuilder.setOperationTargets(ImmutableList.copyOf(operationTargetsList));
846         }
847 
848         // Parse the temporary token
849         String temporaryToken =
850                 ts43XmlDoc.get(
851                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
852                         Ts43XmlDoc.Parm.TEMPORARY_TOKEN);
853 
854         if (temporaryToken == null) {
855             throw new ServiceEntitlementException(
856                     ServiceEntitlementException.ERROR_TOKEN_NOT_AVAILABLE,
857                     "temporary token is not available.");
858         }
859 
860         responseBuilder.setTemporaryToken(temporaryToken);
861 
862         String temporaryTokenExpiry =
863                 ts43XmlDoc.get(
864                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
865                         Ts43XmlDoc.Parm.TEMPORARY_TOKEN_EXPIRY);
866 
867         // Parse the token expiration time.
868         Instant expiry;
869         try {
870             expiry = OffsetDateTime.parse(temporaryTokenExpiry).toInstant();
871             responseBuilder.setTemporaryTokenExpiry(expiry);
872         } catch (DateTimeParseException e) {
873             Log.w(TAG, "Failed to parse temporaryTokenExpiry: " + temporaryTokenExpiry);
874         }
875 
876         return responseBuilder.build();
877     }
878 
879     /**
880      * Get the phone number as described in GSMA Service Entitlement Configuration section 6.2 and
881      * 6.5.8.
882      *
883      * @param getPhoneNumberRequest The get phone number request.
884      * @return The phone number response from the network.
885      * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response
886      *                                     error from the server, the error code can be retrieved by
887      *                                     {@link ServiceEntitlementException#getHttpStatus()}
888      */
889     @NonNull
getPhoneNumber( @onNull GetPhoneNumberRequest getPhoneNumberRequest)890     public GetPhoneNumberResponse getPhoneNumber(
891             @NonNull GetPhoneNumberRequest getPhoneNumberRequest)
892             throws ServiceEntitlementException {
893         ServiceEntitlementRequest.Builder builder =
894                 ServiceEntitlementRequest.builder()
895                         .setEntitlementVersion(mEntitlementVersion);
896 
897         if (!TextUtils.isEmpty(getPhoneNumberRequest.terminalId())) {
898             builder.setTerminalId(getPhoneNumberRequest.terminalId());
899         } else {
900             builder.setTerminalId(mImei);
901         }
902 
903         if (mTokenType == TOKEN_TYPE_NORMAL) {
904             builder.setAuthenticationToken(mAuthToken);
905         } else if (mTokenType == TOKEN_TYPE_TEMPORARY) {
906             builder.setTemporaryToken(mTemporaryToken);
907         }
908 
909         ServiceEntitlementRequest request = builder.build();
910 
911         EsimOdsaOperation operation =
912                 EsimOdsaOperation.builder()
913                         .setOperation(EsimOdsaOperation.OPERATION_GET_PHONE_NUMBER)
914                         .build();
915 
916         String rawXml;
917         try {
918             rawXml =
919                     mServiceEntitlement.performEsimOdsa(
920                         Ts43Constants.APP_PHONE_NUMBER_INFORMATION, request, operation);
921         } catch (ServiceEntitlementException e) {
922             Log.w(TAG, "getPhoneNumber: Failed to perform ODSA operation. e=" + e);
923             throw e;
924         }
925 
926         // Build the response of get phone number operation. Refer to GSMA Service Entitlement
927         // Configuration section 6.5.8.
928         GetPhoneNumberResponse.Builder responseBuilder = GetPhoneNumberResponse.builder();
929 
930         Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml);
931 
932         try {
933             processGeneralResult(ts43XmlDoc, responseBuilder);
934         } catch (MalformedURLException e) {
935             throw new ServiceEntitlementException(
936                     ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE,
937                     "getPhoneNumber: Malformed URL " + rawXml);
938         }
939 
940         // Parse msisdn.
941         String msisdn =
942                 ts43XmlDoc.get(
943                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
944                         Ts43XmlDoc.Parm.MSISDN);
945 
946         if (!TextUtils.isEmpty(msisdn)) {
947             responseBuilder.setMsisdn(msisdn);
948         }
949 
950         return responseBuilder.build();
951     }
952 
953     /**
954      * Parse the download info from {@link ManageSubscriptionResponse}.
955      *
956      * @param characteristics The XML nodes to search activation code.
957      * @param ts43XmlDoc The XML format http response.
958      * @return The download info.
959      */
960     @Nullable
961     @SuppressWarnings("AndroidJdkLibsChecker") // java.util.Base64
parseDownloadInfo( @onNull ImmutableList<String> characteristics, @NonNull Ts43XmlDoc ts43XmlDoc)962     private DownloadInfo parseDownloadInfo(
963             @NonNull ImmutableList<String> characteristics, @NonNull Ts43XmlDoc ts43XmlDoc) {
964         String activationCode =
965                 Strings.nullToEmpty(
966                         ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.PROFILE_ACTIVATION_CODE));
967         String smdpAddress =
968                 Strings.nullToEmpty(
969                         ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.PROFILE_SMDP_ADDRESS));
970         String iccid =
971                 Strings.nullToEmpty(ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.PROFILE_ICCID));
972 
973         // DownloadInfo should contain either activationCode or smdpAddress + iccid
974         if (!activationCode.isEmpty()) {
975             // decode the activation code, which is in base64 format
976             try {
977                 activationCode = new String(Base64.getDecoder().decode(activationCode));
978             } catch (IllegalArgumentException e) {
979                 Log.w(TAG, "Failed to decode the activation code " + activationCode);
980                 return null;
981             }
982             return DownloadInfo.builder()
983                     .setProfileActivationCode(activationCode)
984                     .setProfileIccid(iccid)
985                     .build();
986         } else if (!smdpAddress.isEmpty() && !iccid.isEmpty()) {
987             return DownloadInfo.builder()
988                     .setProfileIccid(iccid)
989                     .setProfileSmdpAddresses(
990                             ImmutableList.copyOf(Arrays.asList(smdpAddress.split("\\s*,\\s*"))))
991                     .build();
992         } else {
993             Log.w(
994                     TAG,
995                     "Failed to parse download info. activationCode="
996                             + activationCode
997                             + ", smdpAddress="
998                             + smdpAddress
999                             + ", iccid="
1000                             + iccid);
1001             return null;
1002         }
1003     }
1004 
1005     /**
1006      * Parse the MSG info from {@link AcquireConfigurationResponse}.
1007      *
1008      * @param characteristics The XML nodes to search.
1009      * @param ts43XmlDoc The XML format http response.
1010      * @return The MSG info.
1011      */
1012     @Nullable
parseMessageInfo( @onNull ImmutableList<String> characteristics, @NonNull Ts43XmlDoc ts43XmlDoc)1013     private MessageInfo parseMessageInfo(
1014             @NonNull ImmutableList<String> characteristics, @NonNull Ts43XmlDoc ts43XmlDoc) {
1015         String message =
1016                 Strings.nullToEmpty(ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.MESSAGE));
1017         String acceptButton =
1018                 Strings.nullToEmpty(ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.ACCEPT_BUTTON));
1019         String acceptButtonLabel =
1020                 Strings.nullToEmpty(
1021                         ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.ACCEPT_BUTTON_LABEL));
1022         String rejectButton =
1023                 Strings.nullToEmpty(ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.REJECT_BUTTON));
1024         String rejectButtonLabel =
1025                 Strings.nullToEmpty(
1026                         ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.REJECT_BUTTON_LABEL));
1027         String acceptFreetext =
1028                 Strings.nullToEmpty(
1029                         ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.ACCEPT_FREETEXT));
1030 
1031         // MessageInfo should contain message, accept button, reject button, and accept freetext
1032         if (!message.isEmpty() && !acceptButton.isEmpty() && !rejectButton.isEmpty()
1033                 && !acceptFreetext.isEmpty()) {
1034             return MessageInfo.builder()
1035                     .setMessage(message)
1036                     .setAcceptButton(acceptButton)
1037                     .setAcceptButtonLabel(acceptButtonLabel)
1038                     .setRejectButton(rejectButton)
1039                     .setRejectButtonLabel(rejectButtonLabel)
1040                     .setAcceptFreetext(acceptFreetext)
1041                     .build();
1042         } else {
1043             Log.w(
1044                     TAG,
1045                     "Failed to parse message info. message="
1046                             + message
1047                             + ", acceptButton="
1048                             + acceptButton
1049                             + ", acceptButtonLabel="
1050                             + acceptButtonLabel
1051                             + ", rejectButton="
1052                             + rejectButton
1053                             + ", rejectButtonLabel="
1054                             + rejectButtonLabel
1055                             + ", acceptFreetext="
1056                             + acceptFreetext);
1057             return null;
1058         }
1059     }
1060 
1061     /**
1062      * Process the common ODSA result from HTTP response.
1063      *
1064      * @param ts43XmlDoc The TS.43 ODSA operation response in XLM format.
1065      * @param builder The response builder.
1066      * @throws MalformedURLException when HTTP response is not well formatted.
1067      */
processGeneralResult( @onNull Ts43XmlDoc ts43XmlDoc, @NonNull OdsaResponse.Builder builder)1068     private void processGeneralResult(
1069             @NonNull Ts43XmlDoc ts43XmlDoc, @NonNull OdsaResponse.Builder builder)
1070             throws MalformedURLException {
1071         // Now start to parse the result from HTTP response.
1072         // Parse the operation result.
1073         String operationResult =
1074                 ts43XmlDoc.get(
1075                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
1076                         Ts43XmlDoc.Parm.OPERATION_RESULT);
1077 
1078         builder.setOperationResult(EsimOdsaOperation.OPERATION_RESULT_UNKNOWN);
1079         if (!TextUtils.isEmpty(operationResult)) {
1080             switch (operationResult) {
1081                 case Ts43XmlDoc.ParmValues.OPERATION_RESULT_SUCCESS:
1082                     builder.setOperationResult(EsimOdsaOperation.OPERATION_RESULT_SUCCESS);
1083                     break;
1084                 case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_GENERAL:
1085                     builder.setOperationResult(EsimOdsaOperation.OPERATION_RESULT_ERROR_GENERAL);
1086                     break;
1087                 case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_INVALID_OPERATION:
1088                     builder.setOperationResult(
1089                             EsimOdsaOperation.OPERATION_RESULT_ERROR_INVALID_OPERATION);
1090                     break;
1091                 case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_INVALID_PARAMETER:
1092                     builder.setOperationResult(
1093                             EsimOdsaOperation.OPERATION_RESULT_ERROR_INVALID_PARAMETER);
1094                     break;
1095                 case Ts43XmlDoc.ParmValues.OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION:
1096                     builder.setOperationResult(
1097                             EsimOdsaOperation.OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION);
1098                     break;
1099                 case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_INVALID_MSG_RESPONSE:
1100                     builder.setOperationResult(
1101                             EsimOdsaOperation.OPERATION_RESULT_ERROR_INVALID_MSG_RESPONSE);
1102                     break;
1103             }
1104         }
1105 
1106         // Parse the general error URL
1107         String generalErrorUrl =
1108                 ts43XmlDoc.get(
1109                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
1110                         Ts43XmlDoc.Parm.GENERAL_ERROR_URL);
1111         if (!TextUtils.isEmpty(generalErrorUrl)) {
1112             builder.setGeneralErrorUrl(new URL(generalErrorUrl));
1113         }
1114 
1115         // Parse the general error URL user data
1116         String generalErrorUserData =
1117                 ts43XmlDoc.get(
1118                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
1119                         Ts43XmlDoc.Parm.GENERAL_ERROR_USER_DATA);
1120         if (!TextUtils.isEmpty(generalErrorUserData)) {
1121             builder.setGeneralErrorUserData(generalErrorUserData);
1122         }
1123 
1124         // Parse the general error text
1125         String generalErrorText =
1126                 ts43XmlDoc.get(
1127                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION),
1128                         Ts43XmlDoc.Parm.GENERAL_ERROR_TEXT);
1129         if (!TextUtils.isEmpty(generalErrorText)) {
1130             builder.setGeneralErrorText(generalErrorText);
1131         }
1132 
1133         // Parse the token for next operation.
1134         String token =
1135                 ts43XmlDoc.get(
1136                         ImmutableList.of(Ts43XmlDoc.CharacteristicType.TOKEN),
1137                         Ts43XmlDoc.Parm.TOKEN);
1138         if (!TextUtils.isEmpty(token)) {
1139             // Some servers issue the new token in operation result for next operation to use.
1140             // We need to save it.
1141             mAuthToken = token;
1142             Log.d(TAG, "processGeneralResult: Token replaced.");
1143         }
1144     }
1145 
1146     /**
1147      * Get the service status from string as described in GSMA Service Entitlement Configuration
1148      * section 6.5.4.
1149      *
1150      * @param serviceStatusString Service status in string format defined in GSMA Service
1151      *                            Entitlement Configuration section 6.5.4.
1152      * @return The converted service status. {@link EsimOdsaOperation#SERVICE_STATUS_UNKNOWN} if not
1153      * able to convert.
1154      */
1155     @OdsaServiceStatus
getServiceStatusFromString(@onNull String serviceStatusString)1156     private int getServiceStatusFromString(@NonNull String serviceStatusString) {
1157         switch (serviceStatusString) {
1158             case Ts43XmlDoc.ParmValues.SERVICE_STATUS_ACTIVATED:
1159                 return EsimOdsaOperation.SERVICE_STATUS_ACTIVATED;
1160             case Ts43XmlDoc.ParmValues.SERVICE_STATUS_ACTIVATING:
1161                 return EsimOdsaOperation.SERVICE_STATUS_ACTIVATING;
1162             case Ts43XmlDoc.ParmValues.SERVICE_STATUS_DEACTIVATED:
1163                 return EsimOdsaOperation.SERVICE_STATUS_DEACTIVATED;
1164             case Ts43XmlDoc.ParmValues.SERVICE_STATUS_DEACTIVATED_NO_REUSE:
1165                 return EsimOdsaOperation.SERVICE_STATUS_DEACTIVATED_NO_REUSE;
1166         }
1167         return EsimOdsaOperation.SERVICE_STATUS_UNKNOWN;
1168     }
1169 }