diff options
author | Xin Li <delphij@google.com> | 2021-10-06 22:53:55 +0000 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2021-10-06 22:53:55 +0000 |
commit | 3b0e1ef3e5f6c1e0eb18cc4e0e3e39439cf36cc3 (patch) | |
tree | a02088ca955f2ab31aad58fdc2c40f57b1d80bde | |
parent | 8b96dede56e916bb8dd58c02372b3833403803a4 (diff) | |
parent | 104894e9086c8563b91f6488d97428b80c742781 (diff) | |
download | ImsServiceEntitlement-3b0e1ef3e5f6c1e0eb18cc4e0e3e39439cf36cc3.tar.gz |
Merge Android 12
Bug: 202323961
Merged-In: I8abe9ad7f1ddd26e960c2f1e1f08e71c278df027
Change-Id: I8c020f916580005ebd3e8a3db79e99287e15f564
56 files changed, 6656 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..60ba277 --- /dev/null +++ b/Android.bp @@ -0,0 +1,80 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +genrule { + name: "statslog-imsentitlement-java-gen", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --java $(out) --module imsentitlement --javaPackage com.android.imsserviceentitlement --javaClass ImsServiceEntitlementStatsLog", + out: ["com/android/imsserviceentitlement/ImsServiceEntitlementStatsLog.java"], +} + +// Library isn't proguard optimized, suitable for unit test +android_library { + name: "ImsServiceEntitlementLib", + static_libs: [ + "androidx.annotation_annotation", + "androidx.fragment_fragment", + "service-entitlement", + "setupdesign", + "guava", + "firebase-encoders-jar", + "firebase-common-aar", + "firebase-components-aar", + "firebase-iid-aar", + "firebase-iid-interop-aar", + "firebase-installations-aar", + "firebase-installations-interop-aar", + "firebase-messaging-aar", + "play-services-basement-aar", + "play-services-cloud-messaging-aar", + "play-services-tasks-aar", + "transport-api-aar", + "firebase-measurement-connector-aar", + "firebase-encoders-json-aar", + "firebase-datatransport-aar", + "play-services-stats-aar", + "transport-runtime-aar", + "transport-backend-cct-aar", + "jsr330", + "dagger2", + ], + libs: [ + "auto_value_annotations", + ], + plugins: ["auto_value_plugin"], + resource_dirs: ["res"], + srcs: [ + "src/**/*.java", + ":statslog-imsentitlement-java-gen", + ], + sdk_version: "system_current", +} + +android_app { + name: "ImsServiceEntitlement", + static_libs: [ + "ImsServiceEntitlementLib", + ], + optimize: { + proguard_flags_files: ["proguard.flags"], + }, + product_specific: true, + sdk_version: "system_current", + privileged: true, + required: ["privapp_whitelist_com.android.imsserviceentitlement"], +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..2311c9b --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.imsserviceentitlement"> + + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/> + <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> + <uses-permission android:name="android.permission.WAKE_LOCK"/> + <uses-permission android:name="com.google.android.setupwizard.SETUP_COMPAT_SERVICE"/> + + <application> + <activity + android:name=".WfcActivationActivity" + android:exported="true" + android:screenOrientation="nosensor" + android:theme="@style/SudThemeGlif.Light"> + </activity> + + <service + android:name=".ImsEntitlementPollingService" + android:exported="true" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + + <!-- START: FCM related components --> + <!-- The FcmReceiver is in GMS client lib; need to declare it here to receive FCM. --> + <receiver + android:name=".fcm.FcmRegistrationReceiver" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + </intent-filter> + </receiver> + + <service + android:name=".fcm.FcmService" + android:exported="true"> + <intent-filter> + <action android:name="com.google.firebase.MESSAGING_EVENT" /> + </intent-filter> + </service> + + <service + android:name=".fcm.FcmRegistrationService" + android:exported="true" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <!-- END: FCM related components --> + + <receiver + android:name=".ImsEntitlementReceiver" + android:exported="true"> + <intent-filter> + <action android:name="android.telephony.action.CARRIER_CONFIG_CHANGED" /> + </intent-filter> + </receiver> + </application> + +</manifest> @@ -0,0 +1,3 @@ +mewan@google.com +samalin@google.com +danielwbhuang@google.com diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg new file mode 100644 index 0000000..f3db20e --- /dev/null +++ b/PREUPLOAD.cfg @@ -0,0 +1,2 @@ +[Hook Scripts] +checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} diff --git a/TEST_MAPPING b/TEST_MAPPING new file mode 100644 index 0000000..981a136 --- /dev/null +++ b/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "ImsServiceEntitlementUnitTests" + } + ], + "postsubmit": [ + { + "name": "ImsServiceEntitlementUnitTests" + } + ] +} diff --git a/proguard.flags b/proguard.flags new file mode 100644 index 0000000..f5bd5b2 --- /dev/null +++ b/proguard.flags @@ -0,0 +1,4 @@ +# Preserve annotated Javascript interface methods. +-keepclassmembers class * { + @android.webkit.JavascriptInterface <methods>; +} diff --git a/res/drawable/ic_phone_in_talk.xml b/res/drawable/ic_phone_in_talk.xml new file mode 100644 index 0000000..de206fa --- /dev/null +++ b/res/drawable/ic_phone_in_talk.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/glif_icon_size" + android:height="@dimen/glif_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?android:attr/colorPrimary" + android:pathData="M20,15.5c-1.25,0 -2.45,-0.2 -3.57,-0.57 -0.35,-0.11 -0.74,-0.03 -1.02,0.24l-2.2,2.2c-2.83,-1.44 -5.15,-3.75 -6.59,-6.59l2.2,-2.21c0.28,-0.26 0.36,-0.65 0.25,-1C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1L4,3c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.5c0,-0.55 -0.45,-1 -1,-1zM19,12h2c0,-4.97 -4.03,-9 -9,-9v2c3.87,0 7,3.13 7,7zM15,12h2c0,-2.76 -2.24,-5 -5,-5v2c1.66,0 3,1.34 3,3z"/> +</vector>
\ No newline at end of file diff --git a/res/drawable/stroke.xml b/res/drawable/stroke.xml new file mode 100644 index 0000000..964acc7 --- /dev/null +++ b/res/drawable/stroke.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + + <solid + android:color="#1f000000"/> + + <size + android:width="720dp" + android:height="1dp"/> + +</shape>
\ No newline at end of file diff --git a/res/layout/activity_wfc_activation.xml b/res/layout/activity_wfc_activation.xml new file mode 100644 index 0000000..0fa15ee --- /dev/null +++ b/res/layout/activity_wfc_activation.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Layout of WfcActivationActivity --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/wfc_activation_container" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <!-- Empty --> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/fragment_suw_ui.xml b/res/layout/fragment_suw_ui.xml new file mode 100644 index 0000000..d49c992 --- /dev/null +++ b/res/layout/fragment_suw_ui.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Layout for the SuW UI screen --> +<com.google.android.setupdesign.GlifLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/setup_wizard_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:icon="@drawable/ic_phone_in_talk" + app:sucHeaderText="@string/emergency_address_app_label"> + + <LinearLayout + android:paddingStart="@dimen/suw_margin" + android:paddingEnd="@dimen/suw_margin" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/entry_text" + android:textAppearance="@style/SetupWizardText.Body1" + android:lineSpacingExtra="8sp" + android:paddingBottom="10dp" + android:paddingTop="8dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </LinearLayout> + +</com.google.android.setupdesign.GlifLayout>
\ No newline at end of file diff --git a/res/layout/fragment_webview.xml b/res/layout/fragment_webview.xml new file mode 100644 index 0000000..0cede7d --- /dev/null +++ b/res/layout/fragment_webview.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Layout for a full screen webview --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <WebView + android:id="@+id/webview" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <ProgressBar + android:id="@+id/loadingbar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:layout_centerInParent="true" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/res/values/config.xml b/res/values/config.xml new file mode 100644 index 0000000..9145e16 --- /dev/null +++ b/res/values/config.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <!-- Required for Firebase functionality to initialize FirebaseApp instance. + These parameters can be found on this page: + https://firebase.google.com/support/privacy/init-options + --> + <!-- Project ID --> + <string name="fcm_project_id" translatable="false">wfcactivation-e5dd9</string> + <!-- App ID --> + <string name="fcm_app_id" translatable="false">1:202982214007:android:57f812ebf3faca7cc33972</string> + <!-- API Key (client)--> + <string name="fcm_api_key" translatable="false">AIzaSyCj8FavhUY66av7wm-EcYKD8xW6LiEEqqo</string> +</resources>
\ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml new file mode 100644 index 0000000..fb1ff8a --- /dev/null +++ b/res/values/dimens.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <dimen name="glif_icon_size">32dp</dimen> + <dimen name="suw_margin">@dimen/sud_glif_margin_start</dimen> +</resources>
\ No newline at end of file diff --git a/res/values/integers.xml b/res/values/integers.xml new file mode 100644 index 0000000..99a5b7e --- /dev/null +++ b/res/values/integers.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="state_max_length">2</integer> + <integer name="zip_max_length">5</integer> +</resources>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..5d6d44d --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- The name of the package [CHAR LIMIT=NONE] --> + <string name="app_label" translatable="false">VoWiFi Activation</string> + + <!-- Error message showed when the app failed to activate WiFi calling and is to exit; + the user may want to retry. [CHAR LIMIT=NONE] --> + <string name="wfc_activation_error">Unable to activate Wi-Fi calling. Please try again later.</string> + <!-- Error message showed when the app failed to update e911 address and is to exit; + the user may want to retry. [CHAR LIMIT=NONE] --> + <string name="address_update_error">Unable to update the current emergency address at this time. Please try again later.</string> + <!-- Error message showed when the terms + and conditions page failed to load. [CHAR LIMIT=NONE] --> + <string name="show_terms_and_condition_error">Can\'t show carrier Terms and Conditions. Try again later.</string> + + <!-- Default title showed on the top of a fullscreen view, indicating that the app is for + managing the user's e911 address. [CHAR LIMIT=40] --> + <string name="emergency_address_app_label">Carrier Setup</string> + <!-- Text used in progress dialog which is showed + when app is loading web content. [CHAR LIMIT=30] --> + <string name="progress_text">This will take a few moments</string> + + <!-- Error message showed when nothing can be done on device to enable Wi-Fi calling; + the user has to contact the wireless carrier to enable. [CHAR LIMIT=NONE] --> + <string name="failure_contact_carrier">Please contact your carrier to enable Wi-Fi calling.</string> + + <!-- Strings for Carrier TOS Fragment --> + <!-- Title of 'Activate Wi-Fi Calling' screen. Generic for carriers --> + <string name="activate_title">Activate Wi-Fi Calling</string> + <!-- Label of a button which the user clicks to cancel current operation + and exit the app. [CHAR LIMIT=10]--> + <string name="cancel">Cancel</string> + <!-- Title of 'Terms and Conditions' screen. Generic for carriers --> + <string name="tos_title">Terms and Conditions</string> + <!-- Button to continue to the next Wi-Fi calling activation step [CHAR LIMIT=20] --> + <string name="tos_continue">Continue</string> + + <!-- Strings for Emergency Address Fragment --> + <!-- Title showed on the top of a fullscreen view + which is for the user to enter e911 location for Wi-Fi calling. [CHAR LIMIT=50] --> + <string name="e911_title">Emergency Location Information</string> + + <!-- Label of a button in error message dialog; + clicking it dismisses error dialog. [CHAR LIMIT=10] --> + <string name="ok">OK</string> +</resources>
\ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..70e998e --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + + <style name="SetupWizardContentFrame" parent="@style/SudContentFrame"> + <item name="android:paddingEnd">0dp</item> + </style> + + <style name="SetupWizardButton.Negative" + parent="@android:style/Widget.Material.Button.Borderless.Colored"> + <item name="android:minWidth">0dp</item> + <item name="android:textAllCaps">false</item> + <item name="android:theme">@style/AccentColorHighlightBorderlessButton</item> + </style> + + <style name="SetupWizardButton.Positive" + parent="@android:style/Widget.Material.Button.Colored"/> + + <style name="AccentColorHighlightBorderlessButton"> + <item name="android:colorControlHighlight">?android:attr/colorAccent</item> + </style> + + <style name="SetupWizardText.Body1" parent="@android:style/TextAppearance.Material.Subhead"> + </style> + + <style name="SetupWizardText.Link1" parent="@style/SetupWizardText.Body1"> + <item name="android:textColor">?android:attr/colorPrimary</item> + </style> + + <style name="SetupWizardText.Error1" parent="@style/SetupWizardText.Body1"> + <item name="android:textColor">#f00</item> + </style> + + <style name="SetupWizardText.Address1" parent="@android:style/TextAppearance.Material.Subhead"> + <item name="android:fontFamily">sans-serif-medium</item> + </style> + + <style name="SetupWizardText.AddressRadioButton2" + parent="@android:style/Widget.CompoundButton.RadioButton"> + <item name="android:textAppearance">@style/SetupWizardText.Address1</item> + <item name="android:drawableBottom">@drawable/stroke</item> + <item name="android:drawablePadding">15dp</item> + <item name="android:paddingStart">15dp</item> + <item name="android:paddingTop">15dp</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + </style> + + <style name="SetupWizardText.AddressRadioButton1" + parent="@style/SetupWizardText.AddressRadioButton2"> + <item name="android:drawableTop">@drawable/stroke</item> + <item name="android:paddingTop">0dp</item> + </style> + +</resources>
\ No newline at end of file diff --git a/src/com/android/imsserviceentitlement/ActivityConstants.java b/src/com/android/imsserviceentitlement/ActivityConstants.java new file mode 100644 index 0000000..d015713 --- /dev/null +++ b/src/com/android/imsserviceentitlement/ActivityConstants.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import android.content.Intent; +import android.telephony.SubscriptionManager; +import android.util.Log; + +/** + * Constants shared by framework to start WFC activation activity. + * + * <p>Must match with WifiCallingSettings. + */ +public final class ActivityConstants { + public static final String TAG = "IMSSE-ActivityConstants"; + + /** Constants shared by WifiCallingSettings */ + public static final String EXTRA_LAUNCH_CARRIER_APP = "EXTRA_LAUNCH_CARRIER_APP"; + + public static final int LAUNCH_APP_ACTIVATE = 0; + public static final int LAUNCH_APP_UPDATE = 1; + public static final int LAUNCH_APP_SHOW_TC = 2; + + /** + * Returns {@code true} if the app is launched for WFC activation; {@code false} for emergency + * address update or displaying terms & conditions. + */ + public static boolean isActivationFlow(Intent intent) { + int intention = getLaunchIntention(intent); + Log.d(TAG, "Start Activity intention : " + intention); + return intention == LAUNCH_APP_ACTIVATE; + } + + /** Returns the launch intention extra in the {@code intent}. */ + public static int getLaunchIntention(Intent intent) { + if (intent == null) { + return LAUNCH_APP_ACTIVATE; + } + + return intent.getIntExtra(EXTRA_LAUNCH_CARRIER_APP, LAUNCH_APP_ACTIVATE); + } + + /** Returns the subscription id of starting the WFC activation activity. */ + public static int getSubId(Intent intent) { + if (intent == null) { + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + int subId = + intent.getIntExtra( + SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + Log.d(TAG, "Start Activity with subId : " + subId); + return subId; + } + + private ActivityConstants() {} +} diff --git a/src/com/android/imsserviceentitlement/EntitlementUtils.java b/src/com/android/imsserviceentitlement/EntitlementUtils.java new file mode 100644 index 0000000..83dab5a --- /dev/null +++ b/src/com/android/imsserviceentitlement/EntitlementUtils.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.android.imsserviceentitlement.utils.Executors.getAsyncExecutor; +import static com.android.imsserviceentitlement.utils.Executors.getDirectExecutor; + +import android.util.Log; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import com.android.imsserviceentitlement.WfcActivationController.EntitlementResultCallback; +import com.android.imsserviceentitlement.entitlement.EntitlementResult; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +/** Handles entitlement check from main thread. */ +public final class EntitlementUtils { + + public static final String LOG_TAG = "IMSSE-EntitlementUtils"; + + private static ListenableFuture<EntitlementResult> sCheckEntitlementFuture; + + private EntitlementUtils() {} + + /** + * Performs the entitlement status check, and passes the result via {@link + * EntitlementResultCallback}. + */ + @MainThread + public static void entitlementCheck( + ImsEntitlementApi activationApi, EntitlementResultCallback callback) { + sCheckEntitlementFuture = + Futures.submit(() -> getEntitlementStatus(activationApi), getAsyncExecutor()); + Futures.addCallback( + sCheckEntitlementFuture, + new FutureCallback<EntitlementResult>() { + @Override + public void onSuccess(EntitlementResult result) { + callback.onEntitlementResult(result); + sCheckEntitlementFuture = null; + } + + @Override + public void onFailure(Throwable t) { + Log.w(LOG_TAG, "get entitlement status failed.", t); + sCheckEntitlementFuture = null; + } + }, + getDirectExecutor()); + } + + /** Cancels the running task of entitlement status check if exist. */ + public static void cancelEntitlementCheck() { + if (sCheckEntitlementFuture != null) { + Log.i(LOG_TAG, "cancel entitlement status check."); + sCheckEntitlementFuture.cancel(true); + } + } + + /** + * Gets entitlement status via carrier-specific entitlement API over network; returns null on + * network falure or other unexpected failure from entitlement API. + */ + @WorkerThread + @Nullable + private static EntitlementResult getEntitlementStatus(ImsEntitlementApi activationApi) { + try { + return activationApi.checkEntitlementStatus(); + } catch (RuntimeException e) { + Log.e("WfcActivationActivity", "getEntitlementStatus failed.", e); + return null; + } + } +} diff --git a/src/com/android/imsserviceentitlement/ImsEntitlementApi.java b/src/com/android/imsserviceentitlement/ImsEntitlementApi.java new file mode 100644 index 0000000..7a906fd --- /dev/null +++ b/src/com/android/imsserviceentitlement/ImsEntitlementApi.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; +import static java.time.temporal.ChronoUnit.SECONDS; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration; +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior; +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.fcm.FcmTokenStore; +import com.android.imsserviceentitlement.fcm.FcmUtils; +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode; +import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus; +import com.android.imsserviceentitlement.ts43.Ts43VolteStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; +import com.android.imsserviceentitlement.utils.TelephonyUtils; +import com.android.imsserviceentitlement.utils.XmlDoc; +import com.android.libraries.entitlement.CarrierConfig; +import com.android.libraries.entitlement.ServiceEntitlement; +import com.android.libraries.entitlement.ServiceEntitlementException; +import com.android.libraries.entitlement.ServiceEntitlementRequest; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.HttpHeaders; + +import java.time.Clock; +import java.time.Instant; +import java.time.format.DateTimeParseException; + +/** Implementation of the entitlement API. */ +public class ImsEntitlementApi { + private static final String TAG = "IMSSE-ImsEntitlementApi"; + + private static final int RESPONSE_RETRY_AFTER = 503; + private static final int RESPONSE_TOKEN_EXPIRED = 511; + + private static final int AUTHENTICATION_RETRIES = 1; + + private final Context mContext; + private final int mSubId; + private final ServiceEntitlement mServiceEntitlement; + private final EntitlementConfiguration mLastEntitlementConfiguration; + + private int mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; + private boolean mNeedsImsProvisioning; + + @VisibleForTesting + static Clock sClock = Clock.systemUTC(); + + public ImsEntitlementApi(Context context, int subId) { + this.mContext = context; + this.mSubId = subId; + CarrierConfig carrierConfig = getCarrierConfig(context); + this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired(context, subId); + this.mServiceEntitlement = new ServiceEntitlement(context, carrierConfig, subId); + this.mLastEntitlementConfiguration = new EntitlementConfiguration(context, subId); + } + + @VisibleForTesting + ImsEntitlementApi( + Context context, + int subId, + boolean needsImsProvisioning, + ServiceEntitlement serviceEntitlement, + EntitlementConfiguration lastEntitlementConfiguration) { + this.mContext = context; + this.mSubId = subId; + this.mNeedsImsProvisioning = needsImsProvisioning; + this.mServiceEntitlement = serviceEntitlement; + this.mLastEntitlementConfiguration = lastEntitlementConfiguration; + } + + /** + * Returns WFC entitlement check result from carrier API (over network), or {@code null} on + * unrecoverable network issue or malformed server response. This is blocking call so should + * not be called on main thread. + */ + @Nullable + public EntitlementResult checkEntitlementStatus() { + Log.d(TAG, "checkEntitlementStatus subId=" + mSubId); + ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder(); + mLastEntitlementConfiguration.getToken().ifPresent( + token -> requestBuilder.setAuthenticationToken(token)); + FcmUtils.fetchFcmToken(mContext, mSubId); + requestBuilder.setNotificationToken(FcmTokenStore.getToken(mContext, mSubId)); + // Set fake device info to avoid leaking + requestBuilder.setTerminalVendor("vendorX"); + requestBuilder.setTerminalModel("modelY"); + requestBuilder.setTerminalSoftwareVersion("versionZ"); + requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML); + if (mNeedsImsProvisioning) { + mLastEntitlementConfiguration.getVersion().ifPresent( + version -> requestBuilder.setConfigurationVersion(Integer.parseInt(version))); + } + ServiceEntitlementRequest request = requestBuilder.build(); + + XmlDoc entitlementXmlDoc = null; + + try { + String rawXml = mServiceEntitlement.queryEntitlementStatus( + mNeedsImsProvisioning + ? ImmutableList.of( + ServiceEntitlement.APP_VOWIFI, + ServiceEntitlement.APP_VOLTE, + ServiceEntitlement.APP_SMSOIP) + : ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + request); + entitlementXmlDoc = new XmlDoc(rawXml); + mLastEntitlementConfiguration.update(rawXml); + // Reset the retry count if no exception from queryEntitlementStatus() + mRetryFullAuthenticationCount = AUTHENTICATION_RETRIES; + } catch (ServiceEntitlementException e) { + if (e.getErrorCode() == ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS) { + if (e.getHttpStatus() == RESPONSE_TOKEN_EXPIRED) { + if (mRetryFullAuthenticationCount <= 0) { + Log.d(TAG, "Ran out of the retry count, stop query status."); + return null; + } + Log.d(TAG, "Server asking for full authentication, retry the query."); + // Clean up the cached data and perform full authentication next query. + mLastEntitlementConfiguration.reset(); + mRetryFullAuthenticationCount--; + return checkEntitlementStatus(); + } else if (e.getHttpStatus() == RESPONSE_RETRY_AFTER && !TextUtils.isEmpty( + e.getRetryAfter())) { + // For handling the case of HTTP_UNAVAILABLE(503), client would perform the + // retry for the delay of Retry-After. + Log.d(TAG, "Server asking for retry. retryAfter = " + e.getRetryAfter()); + return EntitlementResult + .builder() + .setRetryAfterSeconds(parseDelaySecondsByRetryAfter(e.getRetryAfter())) + .build(); + } + } + Log.e(TAG, "queryEntitlementStatus failed", e); + } + return entitlementXmlDoc == null ? null : toEntitlementResult(entitlementXmlDoc); + } + + /** + * Parses the value of {@link HttpHeaders#RETRY_AFTER}. The possible formats could be a numeric + * value in second, or a HTTP-date in RFC-1123 date-time format. + */ + private long parseDelaySecondsByRetryAfter(String retryAfter) { + try { + return Long.parseLong(retryAfter); + } catch (NumberFormatException numberFormatException) { + } + + try { + return SECONDS.between( + Instant.now(sClock), RFC_1123_DATE_TIME.parse(retryAfter, Instant::from)); + } catch (DateTimeParseException dateTimeParseException) { + } + + Log.w(TAG, "Unable to parse retry-after: " + retryAfter + ", ignore it."); + return -1; + } + + private EntitlementResult toEntitlementResult(XmlDoc doc) { + EntitlementResult.Builder builder = EntitlementResult.builder(); + ClientBehavior clientBehavior = mLastEntitlementConfiguration.entitlementValidation(); + + if (mNeedsImsProvisioning && isResetToDefault(clientBehavior)) { + // keep the entitlement result in default value and reset the configs. + if (clientBehavior == ClientBehavior.NEEDS_TO_RESET + || clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR) { + mLastEntitlementConfiguration.reset(); + } else { + mLastEntitlementConfiguration.resetConfigsExceptVers(); + } + } else { + builder.setVowifiStatus(Ts43VowifiStatus.builder(doc).build()) + .setVolteStatus(Ts43VolteStatus.builder(doc).build()) + .setSmsoveripStatus(Ts43SmsOverIpStatus.builder(doc).build()); + doc.get( + ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.SERVER_FLOW_URL, + ServiceEntitlement.APP_VOWIFI) + .ifPresent(url -> builder.setEmergencyAddressWebUrl(url)); + doc.get( + ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.SERVER_FLOW_USER_DATA, + ServiceEntitlement.APP_VOWIFI) + .ifPresent(userData -> builder.setEmergencyAddressWebData(userData)); + } + return builder.build(); + } + + private boolean isResetToDefault(ClientBehavior clientBehavior) { + return clientBehavior == ClientBehavior.UNKNOWN_BEHAVIOR + || clientBehavior == ClientBehavior.NEEDS_TO_RESET + || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS + || clientBehavior == ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON; + } + + private CarrierConfig getCarrierConfig(Context context) { + String entitlementServiceUrl = TelephonyUtils.getEntitlementServerUrl(context, mSubId); + return CarrierConfig.builder().setServerUrl(entitlementServiceUrl).build(); + } +} diff --git a/src/com/android/imsserviceentitlement/ImsEntitlementPollingService.java b/src/com/android/imsserviceentitlement/ImsEntitlementPollingService.java new file mode 100644 index 0000000..bd9ab76 --- /dev/null +++ b/src/com/android/imsserviceentitlement/ImsEntitlementPollingService.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__POLLING; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__SMSOIP; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOLTE; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.os.PersistableBundle; +import android.telephony.SubscriptionManager; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration; +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior; +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.job.JobManager; +import com.android.imsserviceentitlement.utils.ImsUtils; +import com.android.imsserviceentitlement.utils.TelephonyUtils; + +import java.time.Duration; + +/** + * The {@link JobService} for querying entitlement status in the background. The jobId is unique for + * different subId + job combination, so can run the same job for different subIds w/o cancelling + * each others. See {@link JobManager}. + */ +public class ImsEntitlementPollingService extends JobService { + private static final String TAG = "IMSSE-ImsEntitlementPollingService"; + + public static final ComponentName COMPONENT_NAME = + ComponentName.unflattenFromString( + "com.android.imsserviceentitlement/.ImsEntitlementPollingService"); + + private ImsEntitlementApi mImsEntitlementApi; + + /** + * Cache job id associated {@link EntitlementPollingTask} objects for canceling once job be + * canceled. + */ + private final SparseArray<EntitlementPollingTask> mTasks = new SparseArray<>(); + + @VisibleForTesting + EntitlementPollingTask mOngoingTask; + + @Override + @VisibleForTesting + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + } + + @VisibleForTesting + void injectImsEntitlementApi(ImsEntitlementApi imsEntitlementApi) { + this.mImsEntitlementApi = imsEntitlementApi; + } + + /** Enqueues a job to query entitlement status. */ + public static void enqueueJob(Context context, int subId, int retryCount) { + JobManager.getInstance( + context, + COMPONENT_NAME, + subId) + .queryEntitlementStatusOnceNetworkReady(retryCount); + } + + /** Enqueues a job to query entitlement status with delay. */ + private static void enqueueJobWithDelay(Context context, int subId, long delayInSeconds) { + JobManager.getInstance( + context, + COMPONENT_NAME, + subId) + .queryEntitlementStatusOnceNetworkReady(0, Duration.ofSeconds(delayInSeconds)); + } + + @Override + public boolean onStartJob(final JobParameters params) { + PersistableBundle bundle = params.getExtras(); + int subId = + bundle.getInt( + SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + + int jobId = params.getJobId(); + Log.d(TAG, "onStartJob: " + jobId); + + // Ignore the job if the SIM be removed or swapped + if (!JobManager.isValidJob(this, params)) { + Log.d(TAG, "Stop for invalid job! " + jobId); + return false; + } + + // if the same job ID is scheduled again, the current one will be cancelled by platform and + // #onStopJob will be called to removed the job. + mOngoingTask = new EntitlementPollingTask(params, subId); + mTasks.put(jobId, mOngoingTask); + mOngoingTask.execute(); + return true; + } + + @Override + public boolean onStopJob(final JobParameters params) { + int jobId = params.getJobId(); + Log.d(TAG, "onStopJob: " + jobId); + EntitlementPollingTask task = mTasks.get(jobId); + if (task != null) { + task.cancel(true); + mTasks.remove(jobId); + } + + return true; + } + + @VisibleForTesting + class EntitlementPollingTask extends AsyncTask<Void, Void, Void> { + private final JobParameters mParams; + private final ImsEntitlementApi mImsEntitlementApi; + private final ImsUtils mImsUtils; + private final TelephonyUtils mTelephonyUtils; + private final int mSubid; + private final boolean mNeedsImsProvisioning; + + // States for metrics + private long mStartTime; + private long mDurationMillis; + private int mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE; + private int mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT; + private int mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT; + private int mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT; + + EntitlementPollingTask(final JobParameters params, int subId) { + this.mParams = params; + this.mImsUtils = ImsUtils.getInstance(ImsEntitlementPollingService.this, subId); + this.mTelephonyUtils = new TelephonyUtils(ImsEntitlementPollingService.this, subId); + this.mSubid = subId; + this.mNeedsImsProvisioning = TelephonyUtils.isImsProvisioningRequired( + ImsEntitlementPollingService.this, mSubid); + this.mImsEntitlementApi = ImsEntitlementPollingService.this.mImsEntitlementApi != null + ? ImsEntitlementPollingService.this.mImsEntitlementApi + : new ImsEntitlementApi(ImsEntitlementPollingService.this, subId); + } + + @Override + protected Void doInBackground(Void... unused) { + mStartTime = mTelephonyUtils.getUptimeMillis(); + int jobId = JobManager.getPureJobId(mParams.getJobId()); + switch (jobId) { + case JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID: + mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__POLLING; + doEntitlementCheck(); + break; + default: + break; + } + return null; + } + + @Override + protected void onPostExecute(Void unused) { + Log.d(TAG, "JobId:" + mParams.getJobId() + "- Task done."); + sendStatsLogToMetrics(); + ImsEntitlementPollingService.this.jobFinished(mParams, false); + } + + @Override + protected void onCancelled(Void unused) { + sendStatsLogToMetrics(); + } + + private void doEntitlementCheck() { + if (mNeedsImsProvisioning) { + // TODO(b/190476343): Unify EntitlementResult and EntitlementConfiguration. + doImsEntitlementCheck(); + } else { + doWfcEntitlementCheck(); + } + } + + @WorkerThread + private void doImsEntitlementCheck() { + try { + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + Log.d(TAG, "Entitlement result: " + result); + + if (performRetryIfNeeded(result)) { + return; + } + + if (shouldTurnOffWfc(result)) { + mImsUtils.setVowifiProvisioned(false); + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED; + } else { + mImsUtils.setVowifiProvisioned(true); + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED; + } + + if (shouldTurnOffVolte(result)) { + mImsUtils.setVolteProvisioned(false); + mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED; + } else { + mImsUtils.setVolteProvisioned(true); + mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED; + } + + if (shouldTurnOffSMSoIP(result)) { + mImsUtils.setSmsoipProvisioned(false); + mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED; + } else { + mImsUtils.setSmsoipProvisioned(true); + mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED; + } + } catch (RuntimeException e) { + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; + mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; + mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; + Log.d(TAG, "checkEntitlementStatus failed.", e); + } + checkVersValidity(); + } + + @WorkerThread + private void doWfcEntitlementCheck() { + if (!mImsUtils.isWfcEnabledByUser()) { + Log.d(TAG, "WFC not turned on; checkEntitlementStatus not needed this time."); + return; + } + try { + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + Log.d(TAG, "Entitlement result: " + result); + + if (performRetryIfNeeded(result)) { + return; + } + + if (shouldTurnOffWfc(result)) { + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED; + mImsUtils.disableWfc(); + } else { + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__ENABLED; + } + } catch (RuntimeException e) { + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; + Log.d(TAG, "checkEntitlementStatus failed.", e); + } + } + + /** + * Performs retry if needed. Returns true if {@link ImsEntitlementPollingService} has + * scheduled. + */ + private boolean performRetryIfNeeded(@Nullable EntitlementResult result) { + if (result == null || result.getRetryAfterSeconds() < 0) { + return false; + } + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; + ImsEntitlementPollingService.enqueueJobWithDelay( + ImsEntitlementPollingService.this, + mSubid, + result.getRetryAfterSeconds()); + return true; + } + + /** + * Schedules entitlement status check after a VERS.validity time, if the last valid is + * during validity. + */ + private void checkVersValidity() { + EntitlementConfiguration lastEntitlementConfiguration = + new EntitlementConfiguration(ImsEntitlementPollingService.this, mSubid); + if (lastEntitlementConfiguration.entitlementValidation() + == ClientBehavior.VALID_DURING_VALIDITY) { + enqueueJobWithDelay( + ImsEntitlementPollingService.this, + mSubid, + lastEntitlementConfiguration.getVersValidity()); + } + } + + /** + * Returns {@code true} when {@code EntitlementResult} says WFC is not activated; Otherwise + * {@code false} if {@code EntitlementResult} is not of any known pattern. + */ + private boolean shouldTurnOffWfc(@Nullable EntitlementResult result) { + if (result == null) { + Log.d(TAG, "Entitlement API failed to return a result; don't turn off WFC."); + return false; + } + + // Only turn off WFC for known patterns indicating WFC not activated. + return result.getVowifiStatus().serverDataMissing() + || result.getVowifiStatus().inProgress() + || result.getVowifiStatus().incompatible(); + } + + private boolean shouldTurnOffVolte(@Nullable EntitlementResult result) { + if (result == null) { + Log.d(TAG, "Entitlement API failed to return a result; don't turn off VoLTE."); + return false; + } + + // Only turn off VoLTE for known patterns indicating VoLTE not activated. + return !result.getVolteStatus().isActive(); + } + + private boolean shouldTurnOffSMSoIP(@Nullable EntitlementResult result) { + if (result == null) { + Log.d(TAG, "Entitlement API failed to return a result; don't turn off SMSoIP."); + return false; + } + + // Only turn off SMSoIP for known patterns indicating SMSoIP not activated. + return !result.getSmsoveripStatus().isActive(); + } + + private void sendStatsLogToMetrics() { + mDurationMillis = mTelephonyUtils.getUptimeMillis() - mStartTime; + + // If no result set, it was cancelled for reasons. + if (mVowifiResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) { + mVowifiResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED; + } + writeStateLogByApps( + IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI, mVowifiResult); + + if (mNeedsImsProvisioning) { + if (mVolteResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) { + mVolteResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED; + } + if (mSmsoipResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) { + mSmsoipResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED; + } + writeStateLogByApps( + IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOLTE, mVolteResult); + writeStateLogByApps( + IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__SMSOIP, mSmsoipResult); + } + } + + private void writeStateLogByApps(int appId, int appResult) { + ImsServiceEntitlementStatsLog.write( + IMS_SERVICE_ENTITLEMENT_UPDATED, + mTelephonyUtils.getCarrierId(), + mTelephonyUtils.getSpecificCarrierId(), + mPurpose, + appId, + appResult, + mDurationMillis); + } + } +} diff --git a/src/com/android/imsserviceentitlement/ImsEntitlementReceiver.java b/src/com/android/imsserviceentitlement/ImsEntitlementReceiver.java new file mode 100644 index 0000000..dc78b0a --- /dev/null +++ b/src/com/android/imsserviceentitlement/ImsEntitlementReceiver.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior.NEEDS_TO_RESET; +import static com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior.VALID_DURING_VALIDITY; +import static com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior.VALID_WITHOUT_DURATION; +import static com.android.imsserviceentitlement.utils.Executors.getAsyncExecutor; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration; +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior; +import com.android.imsserviceentitlement.job.JobManager; +import com.android.imsserviceentitlement.utils.TelephonyUtils; + +/** Watches events and manages service entitlement polling. */ +public class ImsEntitlementReceiver extends BroadcastReceiver { + private static final String TAG = "IMSSE-ImsEntitlementReceiver"; + + /** + * Shared preference name for activation information, the key used in this file should append + * slot id if the value depended on carrier. + */ + private static final String PREFERENCE_ACTIVATION_INFO = "PREFERENCE_ACTIVATION_INFO"; + /** + * Shared preference key for last known subscription id of a SIM slot; default value {@link + * SubscriptionManager#INVALID_SUBSCRIPTION_ID}. + */ + private static final String KEY_LAST_SUB_ID = "last_sub_id_"; + /** Shared preference key for last boot count. */ + private static final String KEY_LAST_BOOT_COUNT = "last_boot_count_"; + + @Override + public void onReceive(Context context, Intent intent) { + int currentSubId = + intent.getIntExtra( + SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + int slotId = + intent.getIntExtra( + SubscriptionManager.EXTRA_SLOT_INDEX, + SubscriptionManager.INVALID_SIM_SLOT_INDEX); + Dependencies dependencies = createDependency(context, currentSubId); + if (!dependencies.userManager.isSystemUser() + || !TelephonyUtils.isImsProvisioningRequired(context, currentSubId)) { + return; + } + + String action = intent.getAction(); + if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)) { + final PendingResult result = goAsync(); + getAsyncExecutor().execute( + () -> handleCarrierConfigChanged( + context, currentSubId, slotId, dependencies.jobManager, result)); + } + } + + /** + * Handles the event of SIM change and device boot up while receiving {@link + * CarrierConfigManager#ACTION_CARRIER_CONFIG_CHANGED}. + */ + @WorkerThread + private void handleCarrierConfigChanged( + Context context, int currentSubId, int slotId, JobManager jobManager, + PendingResult result) { + if (!SubscriptionManager.isValidSubscriptionId(currentSubId)) { + return; + } + boolean shouldQuery = false; + + // Handle device boot up. + if (isBootUp(context, slotId)) { + ClientBehavior clientBehavior = + new EntitlementConfiguration(context, currentSubId).entitlementValidation(); + Log.d(TAG, "Device boot up, clientBehavior=" + clientBehavior); + if (clientBehavior == VALID_DURING_VALIDITY + || clientBehavior == VALID_WITHOUT_DURATION + || clientBehavior == NEEDS_TO_RESET) { + shouldQuery = true; + } + } + + // Handle SIM changed. + int lastSubId = getAndSetSubId(context, currentSubId, slotId); + if (currentSubId != lastSubId) { + Log.d(TAG, + "SubId for slot " + slotId + " changed: " + lastSubId + " -> " + currentSubId); + if (SubscriptionManager.isValidSubscriptionId(lastSubId)) { + new EntitlementConfiguration(context, lastSubId).reset(); + } + shouldQuery = true; + } + + if (shouldQuery) { + jobManager.queryEntitlementStatusOnceNetworkReady(); + } + + if (result != null) { + result.finish(); + } + } + + /** + * Returns {@code true} if current boot count greater than previous one. Saves the latest boot + * count into shared preference. + */ + @VisibleForTesting + boolean isBootUp(Context context, int slotId) { + SharedPreferences preferences = + context.getSharedPreferences(PREFERENCE_ACTIVATION_INFO, Context.MODE_PRIVATE); + int lastBootCount = preferences.getInt(KEY_LAST_BOOT_COUNT + slotId, 0); + int currentBootCount = + Settings.Global.getInt( + context.getContentResolver(), Settings.Global.BOOT_COUNT, /* def= */ -1); + preferences.edit().putInt(KEY_LAST_BOOT_COUNT + slotId, currentBootCount).apply(); + + return currentBootCount != lastBootCount; + } + + private int getAndSetSubId(Context context, int currentSubId, int slotId) { + SharedPreferences preferences = + context.getSharedPreferences(PREFERENCE_ACTIVATION_INFO, Context.MODE_PRIVATE); + int lastSubId = preferences.getInt( + KEY_LAST_SUB_ID + slotId, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + preferences.edit().putInt(KEY_LAST_SUB_ID + slotId, currentSubId).apply(); + return lastSubId; + } + + /** Returns initialized dependencies */ + @VisibleForTesting + Dependencies createDependency(Context context, int subId) { + // Wrap return value + Dependencies ret = new Dependencies(); + ret.telephonyUtils = new TelephonyUtils(context, subId); + ret.userManager = context.getSystemService(UserManager.class); + ret.jobManager = + JobManager.getInstance(context, ImsEntitlementPollingService.COMPONENT_NAME, subId); + return ret; + } + + /** A collection of dependency objects */ + protected static class Dependencies { + public TelephonyUtils telephonyUtils; + public UserManager userManager; + public JobManager jobManager; + } +} diff --git a/src/com/android/imsserviceentitlement/SuwUiFragment.java b/src/com/android/imsserviceentitlement/SuwUiFragment.java new file mode 100644 index 0000000..c0fa82e --- /dev/null +++ b/src/com/android/imsserviceentitlement/SuwUiFragment.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +/** A {@link Fragment} with SuW GlifLayout. */ +public class SuwUiFragment extends Fragment { + private static final String TITLE_RES_ID_KEY = "TITLE_RES_ID_KEY"; + private static final String TEXT_RES_ID_KEY = "TEXT_RES_ID_KEY"; + private static final String PROGRESS_BAR_SHOWN_KEY = "PROGRESS_BAR_SHOWN_KEY"; + private static final String PRIMARY_BUTTON_TEXT_ID_KEY = "PRIMARY_BUTTON_TEXT_ID_KEY"; + private static final String PRIMARY_BUTTON_RESULT_KEY = "PRIMARY_BUTTON_RESULT_KEY"; + private static final String SECONDARY_BUTTON_TEXT_ID_KEY = "SECONDARY_BUTTON_TEXT_ID_KEY"; + + /** Static constructor */ + public static SuwUiFragment newInstance( + @StringRes int title, + @StringRes int text, + boolean progressBarShown, + @StringRes int primaryButtonText, + int primaryResult, + @StringRes int secondaryButtonText) { + SuwUiFragment frag = new SuwUiFragment(); + Bundle args = new Bundle(); + args.putInt(TITLE_RES_ID_KEY, title); + args.putInt(TEXT_RES_ID_KEY, text); + args.putBoolean(PROGRESS_BAR_SHOWN_KEY, progressBarShown); + args.putInt(PRIMARY_BUTTON_TEXT_ID_KEY, primaryButtonText); + // Action for primaryButton is: finishActivity(primaryResult) + args.putInt(PRIMARY_BUTTON_RESULT_KEY, primaryResult); + args.putInt(SECONDARY_BUTTON_TEXT_ID_KEY, secondaryButtonText); + // Action for secondaryButton is: finishActivity(Activity.RESULT_CANCELED) + frag.setArguments(args); + return frag; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_suw_ui, container, false); + + Bundle arguments = getArguments(); + int titleResId = arguments.getInt(TITLE_RES_ID_KEY, 0); + int textResId = arguments.getInt(TEXT_RES_ID_KEY, 0); + boolean progressBarShown = arguments.getBoolean(PROGRESS_BAR_SHOWN_KEY, false); + int primaryButtonText = arguments.getInt(PRIMARY_BUTTON_TEXT_ID_KEY, 0); + int primaryResult = arguments.getInt(PRIMARY_BUTTON_RESULT_KEY, Activity.RESULT_CANCELED); + int secondaryButtonText = arguments.getInt(SECONDARY_BUTTON_TEXT_ID_KEY, 0); + + GlifLayout layout = view.findViewById(R.id.setup_wizard_layout); + if (titleResId != 0) { + layout.setHeaderText(titleResId); + } + + layout.setProgressBarShown(progressBarShown); + if (progressBarShown) { + // Keep screen on if something in progress. And remove the flag on destroy view. + getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + if (textResId != 0) { + TextView text = view.findViewById(R.id.entry_text); + text.setText(textResId); + } + + final FooterBarMixin buttonFooterMixin = layout.getMixin(FooterBarMixin.class); + + if (primaryButtonText != 0) { + buttonFooterMixin.setPrimaryButton( + new FooterButton.Builder(getContext()) + .setListener(v -> finishActivity(primaryResult)) + .setText(primaryButtonText) + .setTheme(R.style.SudGlifButton_Primary) + .build()); + } + + if (secondaryButtonText != 0) { + buttonFooterMixin.setSecondaryButton( + new FooterButton.Builder(getContext()) + .setListener(v -> finishActivity(Activity.RESULT_CANCELED)) + .setText(secondaryButtonText) + .setTheme(R.style.SudGlifButton_Primary) + .build()); + } + + return view; + } + + @Override + public void onDestroyView() { + boolean progressBarShown = getArguments().getBoolean(PROGRESS_BAR_SHOWN_KEY, false); + if (progressBarShown) { + getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + super.onDestroyView(); + } + + /** Finishes the associated activity with {@code result}; no-op if no activity associated. */ + private void finishActivity(int result) { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + ((WfcActivationUi) activity).setResultAndFinish(result); + } +} diff --git a/src/com/android/imsserviceentitlement/WfcActivationActivity.java b/src/com/android/imsserviceentitlement/WfcActivationActivity.java new file mode 100644 index 0000000..56db329 --- /dev/null +++ b/src/com/android/imsserviceentitlement/WfcActivationActivity.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import android.content.Intent; +import android.os.Bundle; +import android.os.SystemProperties; +import android.util.Log; +import android.view.KeyEvent; + +import androidx.annotation.StringRes; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.setupdesign.util.ThemeHelper; +import com.google.android.setupdesign.util.ThemeResolver; + +/** The UI for WFC activation. */ +public class WfcActivationActivity extends FragmentActivity implements WfcActivationUi { + private static final String TAG = "IMSSE-WfcActivationActivity"; + + // Dependencies + private WfcActivationController mWfcActivationController; + private WfcWebPortalFragment mWfcWebPortalFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + createDependeny(); + setSuwTheme(); + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_wfc_activation); + + int subId = ActivityConstants.getSubId(getIntent()); + mWfcActivationController.startFlow(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mWfcActivationController.finish(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mWfcWebPortalFragment != null && mWfcWebPortalFragment.onKeyDown(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean showActivationUi( + @StringRes int title, + @StringRes int text, + boolean isInProgress, + @StringRes int primaryButtonText, + int primaryButtonResult, + @StringRes int secondaryButtonText) { + runOnUiThreadIfAlive( + () -> { + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + SuwUiFragment frag = + SuwUiFragment.newInstance( + title, + text, + isInProgress, + primaryButtonText, + primaryButtonResult, + secondaryButtonText); + ft.replace(R.id.wfc_activation_container, frag); + // commit may be executed after activity's state is saved. + ft.commitAllowingStateLoss(); + }); + return true; + } + + @Override + public boolean showWebview(String url, String postData) { + runOnUiThreadIfAlive( + () -> { + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + mWfcWebPortalFragment = WfcWebPortalFragment.newInstance(url, postData); + ft.replace(R.id.wfc_activation_container, mWfcWebPortalFragment); + // commit may be executed after activity's state is saved. + ft.commitAllowingStateLoss(); + }); + return true; + } + + private void runOnUiThreadIfAlive(Runnable r) { + if (!isFinishing() && !isDestroyed()) { + runOnUiThread(r); + } + } + + @Override + public void setResultAndFinish(int resultCode) { + Log.d(TAG, "setResultAndFinish: result=" + resultCode); + if (!isFinishing() && !isDestroyed()) { + setResult(resultCode); + finish(); + } + } + + @Override + public WfcActivationController getController() { + return mWfcActivationController; + } + + private void setSuwTheme() { + int defaultTheme = + ThemeHelper.isSetupWizardDayNightEnabled(this) + ? R.style.SudThemeGlifV3_DayNight + : R.style.SudThemeGlifV3_Light; + ThemeResolver themeResolver = + new ThemeResolver.Builder(ThemeResolver.getDefault()) + .setDefaultTheme(defaultTheme) + .setUseDayNight(true) + .build(); + setTheme(themeResolver.resolve( + SystemProperties.get("setupwizard.theme", "SudThemeGlifV3_DayNight"), + /* suppressDayNight= */ !ThemeHelper.isSetupWizardDayNightEnabled(this))); + } + + private void createDependeny() { + Log.d(TAG, "Loading dependencies..."); + // TODO(b/177495634) Use DependencyInjector + if (mWfcActivationController == null) { + // Default initialization + Log.d(TAG, "Default WfcActivationController initialization"); + Intent startIntent = this.getIntent(); + int subId = ActivityConstants.getSubId(startIntent); + mWfcActivationController = + new WfcActivationController( + /* context = */ this, + /* wfcActivationUi = */ this, + new ImsEntitlementApi(this, subId), + this.getIntent()); + } + } +} diff --git a/src/com/android/imsserviceentitlement/WfcActivationController.java b/src/com/android/imsserviceentitlement/WfcActivationController.java new file mode 100644 index 0000000..ed63bf9 --- /dev/null +++ b/src/com/android/imsserviceentitlement/WfcActivationController.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__INCOMPATIBLE; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__TIMEOUT; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__ACTIVATION; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UPDATE; +import static com.android.imsserviceentitlement.ImsServiceEntitlementStatsLog.IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.CountDownTimer; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; +import com.android.imsserviceentitlement.utils.ImsUtils; +import com.android.imsserviceentitlement.utils.TelephonyUtils; + +import java.time.Duration; + +/** + * The driver for WFC activation workflow: go/vowifi-entitlement-status-analysis. + * + * <p>One {@link WfcActivationActivity} owns one and only one controller instance. + */ +public class WfcActivationController { + private static final String TAG = "IMSSE-WfcActivationController"; + + // Entitlement status update retry + private static final int ENTITLEMENT_STATUS_UPDATE_RETRY_MAX = 6; + private static final long ENTITLEMENT_STATUS_UPDATE_RETRY_INTERVAL_MS = + Duration.ofSeconds(5).toMillis(); + + // Dependencies + private final WfcActivationUi mActivationUi; + private final TelephonyUtils mTelephonyUtils; + private final ImsEntitlementApi mImsEntitlementApi; + private final ImsUtils mImsUtils; + private final Intent mStartIntent; + + // States + private int mEvaluateTimes = 0; + + // States for metrics + private long mStartTime; + private long mDurationMillis; + private int mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE; + private int mAppResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT; + + @MainThread + public WfcActivationController( + Context context, + WfcActivationUi wfcActivationUi, + ImsEntitlementApi imsEntitlementApi, + Intent intent) { + this.mStartIntent = intent; + this.mActivationUi = wfcActivationUi; + this.mImsEntitlementApi = imsEntitlementApi; + this.mTelephonyUtils = new TelephonyUtils(context, getSubId()); + this.mImsUtils = ImsUtils.getInstance(context, getSubId()); + } + + /** Indicates the controller to start WFC activation or emergency address update flow. */ + @MainThread + public void startFlow() { + showGeneralWaitingUi(); + evaluateEntitlementStatus(); + if (isActivationFlow()) { + mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__ACTIVATION; + } else { + mPurpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UPDATE; + } + mStartTime = mTelephonyUtils.getUptimeMillis(); + } + + /** Evaluates entitlement status for activation or update. */ + @MainThread + public void evaluateEntitlementStatus() { + if (!mTelephonyUtils.isNetworkConnected()) { + handleInitialEntitlementStatus(null); + return; + } + EntitlementUtils.entitlementCheck( + mImsEntitlementApi, result -> handleInitialEntitlementStatus(result)); + } + + /** + * Indicates the controller to re-evaluate WFC entitlement status after activation flow finished + * successfully (ie. not canceled) by user. + */ + @MainThread + public void finishFlow() { + showGeneralWaitingUi(); + reevaluateEntitlementStatus(); + } + + /** Re-evaluate entitlement status after updating. */ + @MainThread + public void reevaluateEntitlementStatus() { + EntitlementUtils.entitlementCheck( + mImsEntitlementApi, result -> handleReevaluationEntitlementStatus(result)); + } + + /** The interface for handling the entitlement check result. */ + public interface EntitlementResultCallback { + void onEntitlementResult(EntitlementResult result); + } + + /** Indicates the controller to finish on-going tasks and get ready to be destroyed. */ + @MainThread + public void finish() { + EntitlementUtils.cancelEntitlementCheck(); + + // If no duration set, set now. + if (mDurationMillis == 0L) { + mDurationMillis = mTelephonyUtils.getUptimeMillis() - mStartTime; + } + // If no result set, it must be cancelled by user pressing back button. + if (mAppResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) { + mAppResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED; + } + ImsServiceEntitlementStatsLog.write( + IMS_SERVICE_ENTITLEMENT_UPDATED, + /* carrier_id= */ mTelephonyUtils.getCarrierId(), + /* actual_carrier_id= */ mTelephonyUtils.getSpecificCarrierId(), + mPurpose, + IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI, + mAppResult, + mDurationMillis); + } + + /** + * Returns {@code true} if the app is launched for WFC activation; {@code false} for emergency + * address update. + */ + private boolean isActivationFlow() { + return ActivityConstants.isActivationFlow(mStartIntent); + } + + private int getSubId() { + return ActivityConstants.getSubId(mStartIntent); + } + + /** Returns UI title string resource ID based on {@link #isActivationFlow()}. */ + @StringRes + private int getUiTitle() { + int intention = ActivityConstants.getLaunchIntention(mStartIntent); + if (intention == ActivityConstants.LAUNCH_APP_ACTIVATE) { + return R.string.activate_title; + } + if (intention == ActivityConstants.LAUNCH_APP_SHOW_TC) { + return R.string.tos_title; + } + // LAUNCH_APP_UPDATE or otherwise + return R.string.e911_title; + } + + /** Returns general error string resource ID based on {@link #isActivationFlow()}. */ + @StringRes + private int getGeneralErrorText() { + int intention = ActivityConstants.getLaunchIntention(mStartIntent); + if (intention == ActivityConstants.LAUNCH_APP_ACTIVATE) { + return R.string.wfc_activation_error; + } else if (intention == ActivityConstants.LAUNCH_APP_SHOW_TC) { + return R.string.show_terms_and_condition_error; + } + // LAUNCH_APP_UPDATE or otherwise + return R.string.address_update_error; + } + + private void showErrorUi(@StringRes int errorMessage) { + mActivationUi.showActivationUi( + getUiTitle(), errorMessage, false, R.string.ok, WfcActivationUi.RESULT_FAILURE, 0); + } + + private void showGeneralErrorUi() { + showErrorUi(getGeneralErrorText()); + } + + private void showGeneralWaitingUi() { + mActivationUi.showActivationUi(getUiTitle(), R.string.progress_text, true, 0, 0, 0); + } + + @MainThread + private void handleInitialEntitlementStatus(@Nullable EntitlementResult result) { + Log.d(TAG, "Initial entitlement result: " + result); + if (result == null) { + showGeneralErrorUi(); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED); + return; + } + if (isActivationFlow()) { + handleEntitlementStatusForActivation(result); + } else { + handleEntitlementStatusForUpdating(result); + } + } + + @MainThread + private void handleEntitlementStatusForActivation(EntitlementResult result) { + Ts43VowifiStatus vowifiStatus = result.getVowifiStatus(); + if (vowifiStatus.vowifiEntitled()) { + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL); + mActivationUi.setResultAndFinish(Activity.RESULT_OK); + } else { + if (vowifiStatus.serverDataMissing()) { + if (!TextUtils.isEmpty(result.getTermsAndConditionsWebUrl())) { + mActivationUi.showWebview( + result.getTermsAndConditionsWebUrl(), /* postData= */ null); + } else { + mActivationUi.showWebview( + result.getEmergencyAddressWebUrl(), + result.getEmergencyAddressWebData()); + } + } else if (vowifiStatus.incompatible()) { + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__INCOMPATIBLE); + showErrorUi(R.string.failure_contact_carrier); + } else { + Log.e(TAG, "Unexpected status. Show error UI."); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT); + showGeneralErrorUi(); + } + } + } + + @MainThread + private void handleEntitlementStatusForUpdating(EntitlementResult result) { + Ts43VowifiStatus vowifiStatus = result.getVowifiStatus(); + if (vowifiStatus.vowifiEntitled()) { + int launchIntention = ActivityConstants.getLaunchIntention(mStartIntent); + if (launchIntention == ActivityConstants.LAUNCH_APP_SHOW_TC) { + mActivationUi.showWebview( + result.getTermsAndConditionsWebUrl(), /* postData= */ null); + } else { + mActivationUi.showWebview( + result.getEmergencyAddressWebUrl(), result.getEmergencyAddressWebData()); + } + } else { + if (vowifiStatus.incompatible()) { + showErrorUi(R.string.failure_contact_carrier); + turnOffWfc(() -> { + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__INCOMPATIBLE); + }); + } else { + Log.e(TAG, "Unexpected status. Show error UI."); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT); + showGeneralErrorUi(); + } + } + } + + @MainThread + private void handleReevaluationEntitlementStatus(@Nullable EntitlementResult result) { + Log.d(TAG, "Reevaluation entitlement result: " + result); + if (result == null) { // Network issue + showGeneralErrorUi(); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__FAILED); + return; + } + if (isActivationFlow()) { + handleEntitlementStatusAfterActivation(result); + } else { + handleEntitlementStatusAfterUpdating(result); + } + } + + @MainThread + private void handleEntitlementStatusAfterActivation(EntitlementResult result) { + Ts43VowifiStatus vowifiStatus = result.getVowifiStatus(); + if (vowifiStatus.vowifiEntitled()) { + mActivationUi.setResultAndFinish(Activity.RESULT_OK); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL); + } else { + if (vowifiStatus.serverDataMissing()) { + // Check again after 5s, max retry 6 times + if (mEvaluateTimes < ENTITLEMENT_STATUS_UPDATE_RETRY_MAX) { + mEvaluateTimes += 1; + postDelay( + getEntitlementStatusUpdateRetryIntervalMs(), + this::reevaluateEntitlementStatus); + } else { + mEvaluateTimes = 0; + showGeneralErrorUi(); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__TIMEOUT); + } + } else { + // These should never happen, but nothing else we can do. Show general error. + Log.e(TAG, "Unexpected status. Show error UI."); + showGeneralErrorUi(); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT); + } + } + } + + private long getEntitlementStatusUpdateRetryIntervalMs() { + return ENTITLEMENT_STATUS_UPDATE_RETRY_INTERVAL_MS; + } + + @MainThread + private void handleEntitlementStatusAfterUpdating(EntitlementResult result) { + Ts43VowifiStatus vowifiStatus = result.getVowifiStatus(); + if (vowifiStatus.vowifiEntitled()) { + mActivationUi.setResultAndFinish(Activity.RESULT_OK); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL); + } else if (vowifiStatus.serverDataMissing()) { + // Some carrier allows de-activating in updating flow. + turnOffWfc(() -> { + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED); + mActivationUi.setResultAndFinish(Activity.RESULT_OK); + }); + } else { + Log.e(TAG, "Unexpected status. Show error UI."); + showGeneralErrorUi(); + finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNEXPECTED_RESULT); + } + } + + /** Runs {@code action} on caller's thread after {@code delayMillis} ms. */ + private static void postDelay(long delayMillis, Runnable action) { + new CountDownTimer(delayMillis, delayMillis + 100) { + // Use a countDownInterval bigger than millisInFuture so onTick never fires. + @Override + public void onTick(long millisUntilFinished) { + // Do nothing + } + + @Override + public void onFinish() { + action.run(); + } + }.start(); + } + + /** Turns WFC off and then runs {@code action} on main thread. */ + @MainThread + private void turnOffWfc(Runnable action) { + ImsUtils.turnOffWfc(mImsUtils, action); + } + + private void finishStatsLog(int result) { + mAppResult = result; + mDurationMillis = mTelephonyUtils.getUptimeMillis() - mStartTime; + } +} diff --git a/src/com/android/imsserviceentitlement/WfcActivationUi.java b/src/com/android/imsserviceentitlement/WfcActivationUi.java new file mode 100644 index 0000000..259b17f --- /dev/null +++ b/src/com/android/imsserviceentitlement/WfcActivationUi.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import android.app.Activity; + +import androidx.annotation.StringRes; + +/** The interface for UI manipulation. */ +public interface WfcActivationUi { + /** Custom result code, indicating activation flow failed. */ + int RESULT_FAILURE = Activity.RESULT_FIRST_USER; + + /** Shows the basic SuW style UI and returns {@code true} on success */ + boolean showActivationUi( + @StringRes int title, + @StringRes int text, + boolean isInProgress, + @StringRes int primaryButtonText, + int primaryButtonResult, + @StringRes int secondaryButtonText); + + /** Shows the full screen webview */ + boolean showWebview(String url, String postData); + + /** + * Finishes the activity with {@code result}: + * + * <ul> + * <li>{@link Activity#RESULT_OK}: WFC should be turned on. + * <li>{@link Activity#RESULT_CANCELED}: WFC should be OFF because user cancelled. + * <li>{@link #RESULT_FAILURE}: WFC can be OFF because of failure. + * </ul> + */ + void setResultAndFinish(int result); + + /** Returns the WfcActivationController associated with the UI. */ + WfcActivationController getController(); +} diff --git a/src/com/android/imsserviceentitlement/WfcWebPortalFragment.java b/src/com/android/imsserviceentitlement/WfcWebPortalFragment.java new file mode 100644 index 0000000..7249fc3 --- /dev/null +++ b/src/com/android/imsserviceentitlement/WfcWebPortalFragment.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnAttachStateChangeListener; +import android.view.ViewGroup; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ProgressBar; + +import androidx.fragment.app.Fragment; + +import java.util.concurrent.Executor; + +/** A fragment of WebView to render WFC T&C and emergency address web portal */ +public class WfcWebPortalFragment extends Fragment { + private static final String TAG = "IMSSE-WfcWebPortalFragment"; + + private static final String KEY_URL_STRING = "url"; + private static final String KEY_POST_DATA_STRING = "post_data"; + // Javascript object associated with the webview callback functions. See TS.43 v5.0 section 3.4 + private static final String JS_CONTROLLER_NAME = "VoWiFiWebServiceFlow"; + private static final String URL_WITH_PDF_FILE_EXTENSION = ".pdf"; + + private WebView mWebView; + private boolean mFinishFlow = false; + + /** Public static constructor */ + public static WfcWebPortalFragment newInstance(String url, String postData) { + WfcWebPortalFragment frag = new WfcWebPortalFragment(); + + Bundle args = new Bundle(); + args.putString(KEY_URL_STRING, url); + args.putString(KEY_POST_DATA_STRING, postData); + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_webview, container, false); + + Bundle arguments = getArguments(); + Log.d(TAG, "Webview arguments: " + arguments); + String url = arguments.getString(KEY_URL_STRING, ""); + String postData = arguments.getString(KEY_POST_DATA_STRING, ""); + + ProgressBar spinner = v.findViewById(R.id.loadingbar); + mWebView = v.findViewById(R.id.webview); + mWebView.setWebViewClient( + new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return false; // Let WebView handle redirected URL + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + spinner.setVisibility(View.VISIBLE); + } + + @Override + public void onPageFinished(WebView view, String url) { + spinner.setVisibility(View.GONE); + super.onPageFinished(view, url); + } + }); + mWebView.addOnAttachStateChangeListener( + new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + Log.d(TAG, "#onViewDetachedFromWindow"); + if (!mFinishFlow) { + ((WfcActivationUi) getActivity()).setResultAndFinish( + Activity.RESULT_CANCELED); + } + } + }); + mWebView.addJavascriptInterface(new JsInterface(getActivity()), JS_CONTROLLER_NAME); + WebSettings settings = mWebView.getSettings(); + settings.setDomStorageEnabled(true); + settings.setJavaScriptEnabled(true); + + if (TextUtils.isEmpty(postData)) { + mWebView.loadUrl(url); + } else { + mWebView.postUrl(url, postData.getBytes()); + } + return v; + } + + /** + * To support webview handle back key to go back previous page. + * + * @return {@code true} let activity not do anything for this key down. + * {@code false} activity should handle key down. + */ + public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + if (mWebView != null + && mWebView.canGoBack() + && mWebView.getUrl().toLowerCase().endsWith(URL_WITH_PDF_FILE_EXTENSION)) { + mWebView.goBack(); + return true; + } + } + return false; + } + + /** Emergency address websheet javascript callback. */ + private class JsInterface { + private final WfcActivationUi mUi; + private final Executor mMainExecutor; + + JsInterface(Activity activity) { + mUi = (WfcActivationUi) activity; + mMainExecutor = activity.getMainExecutor(); + } + + /** + * Callback function when the VoWiFi service flow ends properly between the device and the + * VoWiFi portal web server. + */ + @JavascriptInterface + public void entitlementChanged() { + Log.d(TAG, "#entitlementChanged"); + mFinishFlow = true; + mMainExecutor.execute(() -> mUi.getController().finishFlow()); + } + + /** + * Callback function when the VoWiFi service flow ends prematurely, either by user + * action or due to a web sheet or network error. + */ + @JavascriptInterface + public void dismissFlow() { + Log.d(TAG, "#dismissFlow"); + mUi.setResultAndFinish(Activity.RESULT_CANCELED); + } + } +} diff --git a/src/com/android/imsserviceentitlement/debug/DebugUtils.java b/src/com/android/imsserviceentitlement/debug/DebugUtils.java new file mode 100644 index 0000000..8936948 --- /dev/null +++ b/src/com/android/imsserviceentitlement/debug/DebugUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.debug; + +import android.os.Build; +import android.os.SystemProperties; +import android.text.TextUtils; + +import java.util.Optional; + +/** Provides API for debugging and not allow to debug on user build. */ +public final class DebugUtils { + private static final String PROP_PII_LOGGABLE = "dbg.imsse.pii_loggable"; + private static final String PROP_SERVER_URL_OVERRIDE = "persist.dbg.imsse.server_url"; + private static final String BUILD_TYPE_USER = "user"; + + private DebugUtils() {} + + /** + * Tells if current build is user-debug or eng build which is debuggable. + * + * @see {@link android.os.Build.TYPE} + */ + public static boolean isDebugBuild() { + return !BUILD_TYPE_USER.equals(Build.TYPE); + } + + /** Returns {@code true} if allow to print PII data for debugging. */ + public static boolean isPiiLoggable() { + if (!isDebugBuild()) { + return false; + } + + return SystemProperties.getBoolean(PROP_PII_LOGGABLE, false); + } + + /** + * Returns {@link Optional} if testing server url was set in system property. + */ + public static Optional<String> getOverrideServerUrl() { + if (!isDebugBuild()) { + return Optional.empty(); + } + + String urlOverride = SystemProperties.get(PROP_SERVER_URL_OVERRIDE, ""); + if (TextUtils.isEmpty(urlOverride)) { + return Optional.empty(); + } + + return Optional.of(urlOverride); + } +} diff --git a/src/com/android/imsserviceentitlement/entitlement/EntitlementConfiguration.java b/src/com/android/imsserviceentitlement/entitlement/EntitlementConfiguration.java new file mode 100644 index 0000000..c2c8abb --- /dev/null +++ b/src/com/android/imsserviceentitlement/entitlement/EntitlementConfiguration.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.entitlement; + +import android.content.Context; + +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode; +import com.android.imsserviceentitlement.utils.XmlDoc; +import com.android.libraries.entitlement.ServiceEntitlement; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** Provides the entitlement characteristic which stored from previous query. */ +public class EntitlementConfiguration { + /** Default value of version for VERS characteristic. */ + private static final int DEFAULT_VERSION = 0; + /** Default value of validity for VERS and TOKEN characteristics. */ + private static final long DEFAULT_VALIDITY = 0; + /** Default value of VoLTE/VoWifi/SMSoverIP entitlemenet status. */ + private static final int INCOMPATIBLE_STATE = 2; + + private final EntitlementConfigurationsDataStore mConfigurationsDataStore; + + private XmlDoc mXmlDoc = new XmlDoc(null); + + public EntitlementConfiguration(Context context, int subId) { + mConfigurationsDataStore = EntitlementConfigurationsDataStore.getInstance(context, subId); + mConfigurationsDataStore.get().ifPresent(rawXml -> mXmlDoc = new XmlDoc(rawXml)); + } + + /** Update VERS characteristics with given version and validity. */ + public void update(String rawXml) { + mConfigurationsDataStore.set(rawXml); + mXmlDoc = new XmlDoc(rawXml); + } + + /** + * Returns VoLTE entitlement status from the {@link EntitlementConfigurationsDataStore}. If no + * data exist then return the default value {@link #INCOMPATIBLE_STATE}. + */ + public int getVolteStatus() { + return mXmlDoc.get( + ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ENTITLEMENT_STATUS, + ServiceEntitlement.APP_VOLTE) + .map(Integer::parseInt) + .orElse(INCOMPATIBLE_STATE); + } + + /** + * Returns VoWiFi entitlement status from the {@link EntitlementConfigurationsDataStore}. If no + * data exist then return the default value {@link #INCOMPATIBLE_STATE}. + */ + public int getVoWifiStatus() { + return mXmlDoc.get( + ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ENTITLEMENT_STATUS, + ServiceEntitlement.APP_VOWIFI) + .map(Integer::parseInt) + .orElse(INCOMPATIBLE_STATE); + } + + /** + * Returns SMSoIP entitlement status from the {@link EntitlementConfigurationsDataStore}. If no + * data exist then return the default value {@link #INCOMPATIBLE_STATE}. + */ + public int getSmsOverIpStatus() { + return mXmlDoc.get( + ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ENTITLEMENT_STATUS, + ServiceEntitlement.APP_SMSOIP) + .map(Integer::parseInt) + .orElse(INCOMPATIBLE_STATE); + } + + /** + * Returns token stored in the {@link EntitlementConfigurationsDataStore} if it is in validity + * period. Returns {@link Optional#empty()} if the token was expired or the value of token + * validity not positive. + */ + public Optional<String> getToken() { + return isTokenInValidityPeriod() + ? mXmlDoc.get(ResponseXmlNode.TOKEN, ResponseXmlAttributes.TOKEN, null) + : Optional.empty(); + } + + private boolean isTokenInValidityPeriod() { + long queryTimeMillis = mConfigurationsDataStore.getQueryTimeMillis(); + long tokenValidityMillis = TimeUnit.SECONDS.toMillis(getTokenValidity()); + + if (queryTimeMillis <= 0) { + // False if the query time not been set. + return false; + } + + // When the token validity is set to 0, the Entitlement Client shall store the token without + // any limitation of duration. + if (tokenValidityMillis <= 0) { + return true; + } + + return (System.currentTimeMillis() - queryTimeMillis) < tokenValidityMillis; + } + + /** + * Returns the validity of the token, in seconds. The validity is counted from the time it is + * received by the client. If no data exist then returns default value 0. + */ + public long getTokenValidity() { + return mXmlDoc.get( + ResponseXmlNode.TOKEN, + ResponseXmlAttributes.VALIDITY, + null) + .map(Long::parseLong) + .orElse(DEFAULT_VALIDITY); + } + + /** Returns version stored in the {@link EntitlementCharacteristicDataStore}. */ + public Optional<String> getVersion() { + return mXmlDoc.get(ResponseXmlNode.VERS, ResponseXmlAttributes.VERSION, null); + } + + /** + * Returns the validity of the version, in seconds. The validity is counted from the time it is + * received by the client. If no data exist then returns default value 0. + */ + public long getVersValidity() { + return mXmlDoc.get( + ResponseXmlNode.VERS, + ResponseXmlAttributes.VALIDITY, + null) + .map(Long::parseLong) + .orElse(DEFAULT_VALIDITY); + } + + public enum ClientBehavior { + /** Unknown behavior. */ + UNKNOWN_BEHAVIOR, + /** Entitlement data is valid during validity seconds. */ + VALID_DURING_VALIDITY, + /** Entitlement data is valid without any limitation of duration. */ + VALID_WITHOUT_DURATION, + /** Entitlement data has to be reset to default configuration */ + NEEDS_TO_RESET, + /** + * Entitlement data has to be reset to default configuration except for version and + * validity. The Entitlement Client shall not perform client configuration requests anymore + * for the services just configured. + */ + NEEDS_TO_RESET_EXCEPT_VERS, + /** + * entitlement data has to be reset to default configuration except for version and + * validity. The Entitlement Client shall not perform client configuration requests anymore + * for the services just configured, until the end-user switches the setting to On. + */ + NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON, + } + + /** Returns {@link ClientBehavior} for the service to be configured. */ + public ClientBehavior entitlementValidation() { + int version = mXmlDoc.get( + ResponseXmlNode.VERS, + ResponseXmlAttributes.VERSION, + null) + .map(Integer::parseInt) + .orElse(DEFAULT_VERSION); + long validity = mXmlDoc.get( + ResponseXmlNode.VERS, + ResponseXmlAttributes.VALIDITY, + null) + .map(Long::parseLong) + .orElse(DEFAULT_VALIDITY); + + if (version > 0 && validity > 0) { + return ClientBehavior.VALID_DURING_VALIDITY; + } else if (version > 0 && validity == 0) { + return ClientBehavior.VALID_WITHOUT_DURATION; + } else if (version == 0 && validity == 0) { + return ClientBehavior.NEEDS_TO_RESET; + } else if (version == -1 && validity == -1) { + return ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS; + } else if (version == -2 && validity == -2) { + return ClientBehavior.NEEDS_TO_RESET_EXCEPT_VERS_UNTIL_SETTING_ON; + } + + // Should never reach. + return ClientBehavior.UNKNOWN_BEHAVIOR; + } + + /** + * Removes the stored configuration and reverts to the default configuration when: + * <ul> + * <li>on SIM card change + * <li>on menu Factory reset + * <li>if a service is barred by the Service Provider (i.e. configuration version set to 0, + * -1, -2). In that case, version and validity are not reset + * </ul> + */ + public void reset() { + // Default configuration of the Characteristics: + // - VERS.version = 0 + // - VERS.validity = 0 + // - TOKEN.token = null (i.e. no value assigned) + // - TOKEN.validity =0 + // - VoLTE.EntitlementStatus=2 (INCOMPATIBLE_STATE) + // - VoWiFi.EntitlementStatus=2 (INCOMPATIBLE_STATE) + // - SMSoIP.EntitlementStatus=2 (INCOMPATIBLE_STATE) + update(null); + } + + /** Reverts to the default configurations except the version and validity. */ + public void resetConfigsExceptVers() { + String rawXml = + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"" + getVersion().get() + "\"/>" + + " <parm name=\"validity\" value=\"" + getVersValidity() + "\"/>" + + " </characteristic>" + + " <characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"\"/>" + + " <parm name=\"validity\" value=\"0\"/>" + + " </characteristic>" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2003\"/>" + + " <parm name=\"EntitlementStatus\" value=\"2\"/>" + + " </characteristic>" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2004\"/>" + + " <parm name=\"EntitlementStatus\" value=\"2\"/>" + + " </characteristic>" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2005\"/>" + + " <parm name=\"EntitlementStatus\" value=\"2\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + update(rawXml); + } +} diff --git a/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationsDataStore.java b/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationsDataStore.java new file mode 100644 index 0000000..6947e2b --- /dev/null +++ b/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationsDataStore.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.entitlement; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.SparseArray; + +import java.util.Optional; + +class EntitlementConfigurationsDataStore { + private static final String PREFERENCE_ENTITLEMENT_CHARACTERISTICS = + "ENTITLEMENT_CHARACTERISTICS"; + private static final String XML_DOCUMENT = "XML_DOCUMENT"; + private static final String QUERY_TIME_MILLIS = "QUERY_TIME_MILLIS"; + + private final SharedPreferences mPreferences; + + private static final SparseArray<EntitlementConfigurationsDataStore> sInstances = + new SparseArray<>(); + + public static EntitlementConfigurationsDataStore getInstance(Context context, int subId) { + if (sInstances.get(subId) == null) { + sInstances.put(subId, new EntitlementConfigurationsDataStore(context, subId)); + } + return sInstances.get(subId); + } + + private EntitlementConfigurationsDataStore(Context context, int subId) { + this.mPreferences = context.getSharedPreferences( + PREFERENCE_ENTITLEMENT_CHARACTERISTICS + "_" + subId, + Context.MODE_PRIVATE); + } + + public void set(String characteristics) { + mPreferences + .edit() + .putString(XML_DOCUMENT, characteristics) + .putLong(QUERY_TIME_MILLIS, System.currentTimeMillis()) + .apply(); + } + + public Optional<String> get() { + return Optional.ofNullable(mPreferences.getString(XML_DOCUMENT, null)); + } + + public long getQueryTimeMillis() { + return mPreferences.getLong(QUERY_TIME_MILLIS, 0); + } +} diff --git a/src/com/android/imsserviceentitlement/entitlement/EntitlementResult.java b/src/com/android/imsserviceentitlement/entitlement/EntitlementResult.java new file mode 100644 index 0000000..480d78a --- /dev/null +++ b/src/com/android/imsserviceentitlement/entitlement/EntitlementResult.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.entitlement; + +import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus; +import com.android.imsserviceentitlement.ts43.Ts43VolteStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; + +import com.google.auto.value.AutoValue; + +/** The result of the entitlement status check. */ +@AutoValue +public abstract class EntitlementResult { + private static final Ts43VowifiStatus INACTIVE_VOWIFI_STATUS = + Ts43VowifiStatus.builder() + .setEntitlementStatus(Ts43VowifiStatus.EntitlementStatus.INCOMPATIBLE) + .setTcStatus(Ts43VowifiStatus.TcStatus.NOT_AVAILABLE) + .setAddrStatus(Ts43VowifiStatus.AddrStatus.NOT_AVAILABLE) + .setProvStatus(Ts43VowifiStatus.ProvStatus.NOT_PROVISIONED) + .build(); + + private static final Ts43VolteStatus INACTIVE_VOLTE_STATUS = + Ts43VolteStatus.builder() + .setEntitlementStatus(Ts43VolteStatus.EntitlementStatus.INCOMPATIBLE) + .build(); + + private static final Ts43SmsOverIpStatus INACTIVE_SMSOVERIP_STATUS = + Ts43SmsOverIpStatus.builder() + .setEntitlementStatus(Ts43SmsOverIpStatus.EntitlementStatus.INCOMPATIBLE) + .build(); + + /** Returns a new {@link Builder} object. */ + public static Builder builder() { + return new AutoValue_EntitlementResult.Builder() + .setVowifiStatus(INACTIVE_VOWIFI_STATUS) + .setVolteStatus(INACTIVE_VOLTE_STATUS) + .setSmsoveripStatus(INACTIVE_SMSOVERIP_STATUS) + .setEmergencyAddressWebUrl("") + .setEmergencyAddressWebData("") + .setTermsAndConditionsWebUrl("") + .setRetryAfterSeconds(-1); + } + + /** The entitlement and service status of VoWiFi. */ + public abstract Ts43VowifiStatus getVowifiStatus(); + /** The entitlement and service status of VoLTE. */ + public abstract Ts43VolteStatus getVolteStatus(); + /** The entitlement and service status of SMSoIP. */ + public abstract Ts43SmsOverIpStatus getSmsoveripStatus(); + /** The URL to the WFC emergency address web form. */ + public abstract String getEmergencyAddressWebUrl(); + /** The data associated with the POST request to the WFC emergency address web form. */ + public abstract String getEmergencyAddressWebData(); + /** The URL to the WFC T&C web form. */ + public abstract String getTermsAndConditionsWebUrl(); + /** Service temporary unavailable, retry the status check after a delay in seconds. */ + public abstract long getRetryAfterSeconds(); + + /** Builder of {@link EntitlementResult}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract EntitlementResult build(); + public abstract Builder setVowifiStatus(Ts43VowifiStatus vowifiStatus); + public abstract Builder setVolteStatus(Ts43VolteStatus volteStatus); + public abstract Builder setSmsoveripStatus(Ts43SmsOverIpStatus smsoveripStatus); + public abstract Builder setEmergencyAddressWebUrl(String emergencyAddressWebUrl); + public abstract Builder setEmergencyAddressWebData(String emergencyAddressWebData); + public abstract Builder setTermsAndConditionsWebUrl(String termsAndConditionsWebUrl); + public abstract Builder setRetryAfterSeconds(long retryAfter); + } + + @Override + public final String toString() { + StringBuilder builder = new StringBuilder("EntitlementResult{"); + builder.append(",getVowifiStatus=").append(getVowifiStatus()); + builder.append(",getVolteStatus=").append(getVolteStatus()); + builder.append(",getSmsoveripStatus=").append(getSmsoveripStatus()); + builder.append(",getEmergencyAddressWebUrl=").append(opaque(getEmergencyAddressWebUrl())); + builder.append(",getEmergencyAddressWebData=").append(opaque(getEmergencyAddressWebData())); + builder.append(",getTermsAndConditionsWebUrl=").append(getTermsAndConditionsWebUrl()); + builder.append(",getRetryAfter=").append(getRetryAfterSeconds()); + builder.append("}"); + return builder.toString(); + } + + private static String opaque(String string) { + if (string == null) { + return "null"; + } + return "string_of_length_" + string.length(); + } +} diff --git a/src/com/android/imsserviceentitlement/fcm/FcmRegistrationReceiver.java b/src/com/android/imsserviceentitlement/fcm/FcmRegistrationReceiver.java new file mode 100644 index 0000000..9c72f39 --- /dev/null +++ b/src/com/android/imsserviceentitlement/fcm/FcmRegistrationReceiver.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** A {@link BroadcastReceiver} that triggers FCM registration jobs. */ +public class FcmRegistrationReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + FcmRegistrationService.enqueueJob(context); + } + } +} diff --git a/src/com/android/imsserviceentitlement/fcm/FcmRegistrationService.java b/src/com/android/imsserviceentitlement/fcm/FcmRegistrationService.java new file mode 100644 index 0000000..8aaf419 --- /dev/null +++ b/src/com/android/imsserviceentitlement/fcm/FcmRegistrationService.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.imsserviceentitlement.R; +import com.android.imsserviceentitlement.job.JobManager; +import com.android.imsserviceentitlement.utils.TelephonyUtils; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.messaging.FirebaseMessaging; + +import java.io.IOException; + +/** A {@link JobService} that gets a FCM tokens for all active SIMs. */ +public class FcmRegistrationService extends JobService { + private static final String TAG = "IMSSE-FcmRegistrationService"; + + private FirebaseInstanceId mFakeInstanceID = null; + private FirebaseApp mApp = null; + + @VisibleForTesting AsyncTask<JobParameters, Void, Void> mOngoingTask; + + /** Enqueues a job for FCM registration. */ + public static void enqueueJob(Context context) { + ComponentName componentName = new ComponentName(context, FcmRegistrationService.class); + // No subscription id associated job, use {@link + // SubscriptionManager#INVALID_SUBSCRIPTION_ID}. + JobManager jobManager = + JobManager.getInstance( + context, componentName, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + jobManager.registerFcmOnceNetworkReady(); + } + + @VisibleForTesting + void setFakeInstanceID(FirebaseInstanceId instanceID) { + mFakeInstanceID = instanceID; + } + + @Override + @VisibleForTesting + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + } + + @Override + public void onCreate() { + super.onCreate(); + try { + mApp = FirebaseApp.getInstance(); + } catch (IllegalStateException e) { + Log.d(TAG, "initialize FirebaseApp"); + mApp = FirebaseApp.initializeApp( + this, + new FirebaseOptions.Builder() + .setApplicationId(getResources().getString(R.string.fcm_app_id)) + .setProjectId(getResources().getString(R.string.fcm_project_id)) + .setApiKey(getResources().getString(R.string.fcm_api_key)) + .build()); + } + } + + @Override + public boolean onStartJob(JobParameters params) { + mOngoingTask = new AsyncTask<JobParameters, Void, Void>() { + @Override + protected Void doInBackground(JobParameters... params) { + onHandleWork(params[0]); + return null; + } + }; + mOngoingTask.execute(params); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return true; // Always re-run if job stopped. + } + + /** + * Registers to receive FCM messages published to subscribe topics under the retrieved token. + * The token changes when the InstanceID becomes invalid (e.g. app data is deleted). + */ + protected void onHandleWork(JobParameters params) { + boolean wantsReschedule = false; + FirebaseInstanceId instanceID = getFirebaseInstanceId(); + if (instanceID == null) { + Log.d(TAG, "Cannot get fcm token because FirebaseInstanceId is null"); + return; + } + for (int subId : TelephonyUtils.getSubIdsWithFcmSupported(this)) { + if (!updateFcmToken(instanceID, subId)) { + wantsReschedule = true; + } + } + + jobFinished(params, wantsReschedule); + } + + /** Returns {@code false} if failed to get token. */ + private boolean updateFcmToken(FirebaseInstanceId instanceID, int subId) { + Log.d(TAG, "FcmRegistrationService.updateFcmToken: subId=" + subId); + String token = getTokenForSubId(instanceID, subId); + if (token == null) { + Log.d(TAG, "getToken null"); + return false; + } + Log.d(TAG, "FCM token: " + token + " subId: " + subId); + FcmTokenStore.setToken(this, subId, token); + return true; + } + + private FirebaseInstanceId getFirebaseInstanceId() { + return (mFakeInstanceID != null) ? mFakeInstanceID : FirebaseInstanceId.getInstance(mApp); + } + + private String getTokenForSubId(FirebaseInstanceId instanceID, int subId) { + String token = null; + try { + token = instanceID.getToken( + TelephonyUtils.getFcmSenderId(this, subId), + FirebaseMessaging.INSTANCE_ID_SCOPE); + } catch (IOException e) { + Log.e(TAG, "Failed to get a new FCM token: " + e); + } + return token; + } +} diff --git a/src/com/android/imsserviceentitlement/fcm/FcmService.java b/src/com/android/imsserviceentitlement/fcm/FcmService.java new file mode 100644 index 0000000..9ab33fc --- /dev/null +++ b/src/com/android/imsserviceentitlement/fcm/FcmService.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import android.content.ComponentName; +import android.content.Context; +import android.util.Log; + +import com.android.imsserviceentitlement.ImsEntitlementPollingService; +import com.android.imsserviceentitlement.job.JobManager; +import com.android.imsserviceentitlement.utils.TelephonyUtils; +import com.android.libraries.entitlement.ServiceEntitlement; + +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +import java.util.Map; + +/** Service for handling Firebase Cloud Messaging.*/ +public class FcmService extends FirebaseMessagingService { + private static final String TAG = "IMSSE-FcmService"; + + private static final String DATA_APP_KEY = "app"; + private static final String DATA_TIMESTAMP_KEY = "timestamp"; + + private JobManager mJobManager; + + @Override + @VisibleForTesting + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + } + + /** + * Called when a new token for the default Firebase project is generated. + * + * @param token the token used for sending messages to this application instance. + */ + @Override + public void onNewToken(String token) { + Log.d(TAG, "New token: " + token); + + // TODO(b/182560867): check if we need to update the new token to server. + + // Note we cannot directly save the new token, as we don't know which subId + // it's associated with. + FcmRegistrationService.enqueueJob(this); + } + + /** + * Handles FCM message for entitlement. + * + * @param message holds the message received from Firebase Cloud Messaging. + */ + @Override + public void onMessageReceived(RemoteMessage message) { + // Not testable. + onMessageReceived(message.getSenderId(), message.getData()); + } + + @VisibleForTesting + void onMessageReceived(String fcmSenderId, Map<String, String> fcmData) { + Log.d(TAG, "onMessageReceived, SenderId:" + fcmSenderId); + if (!isTs43EntitlementsChangeEvent(fcmData)) { + Log.i(TAG, "Ignore message not related to entitlements change."); + return; + } + // A corner case: a FCM received after SIM is removed, and SIM inserted back later. + // We missed the FCM in this case. + scheduleEntitlementStatusCheckForSubIdAssociatedWithSenderId(fcmSenderId); + } + + private static boolean isTs43EntitlementsChangeEvent(Map<String, String> dataMap) { + if (dataMap == null) { + return false; + } + Log.v(TAG, "The payload data: " + dataMap); + + // Based on GSMA TS.43 2.4.2 Messaging Infrastructure-Based Notifications, the notification + // payload for multiple applications follows: + // "data": + // { + // "app": ["ap2003", "ap2004", "ap2005"], + // "timestamp": "2019-01-29T13:15:31-08:00" + // } + if (!dataMap.containsKey(DATA_APP_KEY) || !dataMap.containsKey(DATA_TIMESTAMP_KEY)) { + Log.d(TAG, "data format error"); + return false; + } + // Check if APP_VOWIFI i.e. "ap2004" is in notification data. + if (dataMap.get(DATA_APP_KEY).contains(ServiceEntitlement.APP_VOWIFI)) { + return true; + } + return false; + } + + @VisibleForTesting + void setMockJobManager(JobManager jobManager) { + mJobManager = jobManager; + } + + private JobManager getJobManager(int subId) { + return (mJobManager != null) + ? mJobManager + : JobManager.getInstance( + this, + ImsEntitlementPollingService.COMPONENT_NAME, + subId); + } + + private void scheduleEntitlementStatusCheckForSubIdAssociatedWithSenderId(String msgSenderId) { + for (int subId : TelephonyUtils.getSubIdsWithFcmSupported(this)) { + String configSenderId = TelephonyUtils.getFcmSenderId(this, subId); + if (msgSenderId.equals(configSenderId)) { + Log.d(TAG, "check entitlement status for subscription id(" + subId + ")"); + getJobManager(subId).queryEntitlementStatusOnceNetworkReady(); + } + } + } +} diff --git a/src/com/android/imsserviceentitlement/fcm/FcmTokenStore.java b/src/com/android/imsserviceentitlement/fcm/FcmTokenStore.java new file mode 100644 index 0000000..a972fb7 --- /dev/null +++ b/src/com/android/imsserviceentitlement/fcm/FcmTokenStore.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +/** Stores FCM token. */ +public final class FcmTokenStore { + private static final String TAG = "IMSSE-FcmTokenStore"; + + private static final String FCM_TOKEN_FILE = "FCM_TOKEN"; + private static final String FCM_TOKEN_KEY = "FCM_TOKEN_SUB_"; + + private FcmTokenStore() {} + + /** Returns FCM token or empty string if not available. */ + public static String getToken(Context context, int subId) { + return getFcmTokenFile(context).getString(FCM_TOKEN_KEY + subId, ""); + } + + /** Returns {@code true} if FCM token available. */ + public static boolean hasToken(Context context, int subId) { + return !TextUtils.isEmpty(getToken(context, subId)); + } + + /** Saves the FCM token into data store. */ + @WorkerThread + public static boolean setToken(Context context, int subId, String token) { + if (!TextUtils.isEmpty(token)) { + return getFcmTokenFile(context) + .edit() + .putString(FCM_TOKEN_KEY + subId, token) + .commit(); + } else { + return getFcmTokenFile(context) + .edit() + .remove(FCM_TOKEN_KEY + subId) + .commit(); + } + } + + /** Registers a listener for FCM token update. */ + public static void registerTokenUpdateListener( + Context context, OnSharedPreferenceChangeListener listener) { + Log.d(TAG, "registerTokenUpdateListener"); + // Since FCM_TOKEN_FILE only contains one item FCM_TOKEN_KEY, a change to FCM_TOKEN_FILE + // means a change to FCM_TOKEN_KEY. The listener can ignore its arguments. + getFcmTokenFile(context).registerOnSharedPreferenceChangeListener(listener); + } + + /** Unregisters a listener for FCM token update. */ + public static void unregisterTokenUpdateListener( + Context context, OnSharedPreferenceChangeListener listener) { + Log.d(TAG, "unregisterTokenUpdateListener"); + // Since FCM_TOKEN_FILE only contains one item FCM_TOKEN_KEY, a change to FCM_TOKEN_FILE + // means a change to FCM_TOKEN_KEY. The listener can ignore its arguments. + getFcmTokenFile(context).unregisterOnSharedPreferenceChangeListener(listener); + } + + private static SharedPreferences getFcmTokenFile(Context context) { + return context.getSharedPreferences(FCM_TOKEN_FILE, Context.MODE_PRIVATE); + } +} diff --git a/src/com/android/imsserviceentitlement/fcm/FcmUtils.java b/src/com/android/imsserviceentitlement/fcm/FcmUtils.java new file mode 100644 index 0000000..70ec276 --- /dev/null +++ b/src/com/android/imsserviceentitlement/fcm/FcmUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +import java.util.concurrent.CountDownLatch; + +/** Convenience methods for FCM. */ +public final class FcmUtils { + public static final String LOG_TAG = "IMSSE-FcmUtils"; + + private static final long TOKEN_UPDATE_WAITING_SECONDS = 25L; + + private FcmUtils() {} + + /** Fetches FCM token, if it's not available via {@link FcmTokenStore#getToken}. */ + @WorkerThread + public static void fetchFcmToken(Context context, int subId) { + if (FcmTokenStore.hasToken(context, subId)) { + Log.d(LOG_TAG, "FCM token available."); + return; + } + + Log.d(LOG_TAG, "FCM token unavailable. Try to update..."); + final CountDownLatch tokenUpdated = new CountDownLatch(1); + final SharedPreferences.OnSharedPreferenceChangeListener listener = + (s, k) -> { + Log.d(LOG_TAG, "FCM preference changed."); + if (FcmTokenStore.hasToken(context, subId)) { + tokenUpdated.countDown(); + } + }; + FcmTokenStore.registerTokenUpdateListener(context, listener); + + // Starts a JobIntentService to update FCM token by calling FCM API on a worker thread. + FcmRegistrationService.enqueueJob(context); + + try { + // Wait for 25s. If FCM token update failed/timeout, we will let user see + // the error message returned by server. Then user can manually retry. + if (tokenUpdated.await(TOKEN_UPDATE_WAITING_SECONDS, SECONDS)) { + Log.d(LOG_TAG, "FCM token updated."); + } else { + Log.d(LOG_TAG, "FCM token update failed."); + } + } catch (InterruptedException e) { + // Do nothing + Log.d(LOG_TAG, "FCM token update interrupted."); + } + FcmTokenStore.unregisterTokenUpdateListener(context, listener); + } +} diff --git a/src/com/android/imsserviceentitlement/job/JobManager.java b/src/com/android/imsserviceentitlement/job/JobManager.java new file mode 100644 index 0000000..4bbc5d6 --- /dev/null +++ b/src/com/android/imsserviceentitlement/job/JobManager.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.job; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.SubscriptionManager; +import android.util.ArrayMap; +import android.util.Log; + +import androidx.annotation.GuardedBy; + +import com.android.imsserviceentitlement.utils.TelephonyUtils; + +import java.time.Duration; + +/** Manages all scheduled jobs and provides common job scheduler. */ +public class JobManager { + private static final String TAG = "IMSSE-JobManager"; + + private static final int JOB_ID_BASE_INDEX = 1000; + + // Query entitlement status + public static final int QUERY_ENTITLEMENT_STATUS_JOB_ID = 1; + // Register FCM to listen push notification, this job not associated with subscription id. + public static final int REGISTER_FCM_JOB_ID = 2; + + public static final String EXTRA_SLOT_ID = "SLOT_ID"; + public static final String EXTRA_RETRY_COUNT = "RETRY_COUNT"; + + private final Context mContext; + private final int mSubId; + private final JobScheduler mJobScheduler; + private final ComponentName mComponentName; + + // Cache subscription id associated {@link JobManager} objects for reusing. + @GuardedBy("JobManager.class") + private static final ArrayMap<String, JobManager> sInstances = new ArrayMap<>(); + + private JobManager(Context context, ComponentName componentName, int subId) { + this.mContext = context; + this.mComponentName = componentName; + this.mJobScheduler = context.getSystemService(JobScheduler.class); + this.mSubId = subId; + } + + /** Returns {@link JobManager} instance. */ + public static synchronized JobManager getInstance( + Context context, ComponentName componentName, int subId) { + String key = componentName.flattenToShortString() + "." + subId; + JobManager instance = sInstances.get(key); + if (instance != null) { + return instance; + } + + instance = new JobManager(context, componentName, subId); + sInstances.put(key, instance); + return instance; + } + + private JobInfo.Builder newJobInfoBuilder(int jobId) { + return newJobInfoBuilder(jobId, 0 /* retryCount */); + } + + private JobInfo.Builder newJobInfoBuilder(int jobId, int retryCount) { + JobInfo.Builder builder = new JobInfo.Builder(getJobIdWithSubId(jobId), mComponentName); + putSubIdAndRetryExtra(builder, retryCount); + return builder; + } + + /** + * Returns a new job id with {@code JOB_ID_BASE_INDEX} for separating job for different + * subscription id, in order to avoid job be overrided for different SIM on multi SIM device. + * Returns original {@code jobId} if the subscription id invalid. For example, if subscription + * id be 8, the job id would be 8001, 8002, etc; if subscription id be -1, the job id would be + * 1, 2, etc. + */ + private int getJobIdWithSubId(int jobId) { + if (SubscriptionManager.isValidSubscriptionId(mSubId)) { + return JOB_ID_BASE_INDEX * mSubId + jobId; + } + return jobId; + } + + /** Returns job id which remove {@code JOB_ID_BASE_INDEX}. */ + public static int getPureJobId(int jobId) { + return jobId % JOB_ID_BASE_INDEX; + } + + private void putSubIdAndRetryExtra(JobInfo.Builder builder, int retryCount) { + PersistableBundle bundle = new PersistableBundle(); + bundle.putInt(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, mSubId); + bundle.putInt(EXTRA_SLOT_ID, TelephonyUtils.getSlotId(mContext, mSubId)); + bundle.putInt(EXTRA_RETRY_COUNT, retryCount); + builder.setExtras(bundle); + } + + /** Checks Entitlement Status once has network connection without retry and delay. */ + public void queryEntitlementStatusOnceNetworkReady() { + queryEntitlementStatusOnceNetworkReady(/* retryCount= */ 0, Duration.ofSeconds(0)); + } + + /** Checks Entitlement Status once has network connection with retry count. */ + public void queryEntitlementStatusOnceNetworkReady(int retryCount) { + queryEntitlementStatusOnceNetworkReady(retryCount, Duration.ofSeconds(0)); + } + + /** Checks Entitlement Status once has network connection with retry count and delay. */ + public void queryEntitlementStatusOnceNetworkReady(int retryCount, Duration delay) { + Log.d( + TAG, + "schedule QUERY_ENTITLEMENT_STATUS_JOB_ID once has network connection, " + + "retryCount=" + + retryCount + + ", delay=" + + delay); + JobInfo job = + newJobInfoBuilder(QUERY_ENTITLEMENT_STATUS_JOB_ID, retryCount) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setMinimumLatency(delay.toMillis()) + .build(); + mJobScheduler.schedule(job); + } + + /** Registers FCM service to listen push notification once has network connection. */ + public void registerFcmOnceNetworkReady() { + Log.d(TAG, "Schedule REGISTER_FCM_JOB_ID once has network connection."); + JobInfo job = + newJobInfoBuilder(REGISTER_FCM_JOB_ID) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .build(); + mJobScheduler.schedule(job); + } + + /** + * Returns {@code true} if this job's subscription id still actived and still on same slot. + * Returns {@code false} otherwise. + */ + public static boolean isValidJob(Context context, final JobParameters params) { + PersistableBundle bundle = params.getExtras(); + int subId = + bundle.getInt( + SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + int slotId = bundle.getInt(EXTRA_SLOT_ID, SubscriptionManager.INVALID_SIM_SLOT_INDEX); + + // Avoids to do anything after user removed or swapped SIM + if (!TelephonyUtils.isActivedSubId(context, subId)) { + Log.d(TAG, "Stop reason: SUBID(" + subId + ") not point to active SIM."); + return false; + } + + // For example, the job scheduled for slot 1 then SIM been swapped to slot 2 and then start + // this job. So, let's ignore this case. + if (TelephonyUtils.getSlotId(context, subId) != slotId) { + Log.d(TAG, "Stop reason: SLOTID(" + slotId + ") not matched."); + return false; + } + + return true; + } +} diff --git a/src/com/android/imsserviceentitlement/ts43/Ts43Constants.java b/src/com/android/imsserviceentitlement/ts43/Ts43Constants.java new file mode 100644 index 0000000..22d6022 --- /dev/null +++ b/src/com/android/imsserviceentitlement/ts43/Ts43Constants.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.ts43; + +/** Constants to be used in GSMA TS.43 protocol. */ +public final class Ts43Constants { + private Ts43Constants() {} + + /** Node types of XML response content. */ + public static final class ResponseXmlNode { + private ResponseXmlNode() {} + + /** Node name of token. */ + public static final String TOKEN = "TOKEN"; + /** Node name of application. */ + public static final String APPLICATION = "APPLICATION"; + /** Node name of vers. */ + public static final String VERS = "VERS"; + } + + /** Attribute names of XML response content. */ + public static final class ResponseXmlAttributes { + private ResponseXmlAttributes() {} + + /** XML attribute name of token. */ + public static final String TOKEN = "token"; + /** XML attribute name of application identifier. */ + public static final String APP_ID = "AppID"; + /** XML attribute name of entitlement status. */ + public static final String ENTITLEMENT_STATUS = "EntitlementStatus"; + /** XML attribute name of E911 address status. */ + public static final String ADDR_STATUS = "AddrStatus"; + /** XML attribute name of terms and condition status. */ + public static final String TC_STATUS = "TC_Status"; + /** XML attribute name of provision status. */ + public static final String PROVISION_STATUS = "ProvStatus"; + /** XML attribute name of entitlement server URL. */ + public static final String SERVER_FLOW_URL = "ServiceFlow_URL"; + /** XML attribute name of entitlement server user data. */ + public static final String SERVER_FLOW_USER_DATA = "ServiceFlow_UserData"; + /** XML attribute name of version. */ + public static final String VERSION = "version"; + /** XML attribute name of validity. */ + public static final String VALIDITY = "validity"; + } +} diff --git a/src/com/android/imsserviceentitlement/ts43/Ts43SmsOverIpStatus.java b/src/com/android/imsserviceentitlement/ts43/Ts43SmsOverIpStatus.java new file mode 100644 index 0000000..cdbd435 --- /dev/null +++ b/src/com/android/imsserviceentitlement/ts43/Ts43SmsOverIpStatus.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.ts43; + +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode; +import com.android.imsserviceentitlement.utils.XmlDoc; +import com.android.libraries.entitlement.ServiceEntitlement; + +import com.google.auto.value.AutoValue; + +/** + * Implementation of SMSoIP entitlement status and server data availability for TS.43 entitlement + * solution. This class is only used to report the entitlement status of SMSoIP. + */ +@AutoValue +public abstract class Ts43SmsOverIpStatus { + /** The entitlement status of SMSoIP service. */ + public static class EntitlementStatus { + public EntitlementStatus() {} + + public static final int DISABLED = 0; + public static final int ENABLED = 1; + public static final int INCOMPATIBLE = 2; + public static final int PROVISIONING = 3; + } + + /** The entitlement status of SMSoIP service. */ + public abstract int entitlementStatus(); + + public static Ts43SmsOverIpStatus.Builder builder() { + return new AutoValue_Ts43SmsOverIpStatus.Builder() + .setEntitlementStatus(EntitlementStatus.DISABLED); + } + + public static Ts43SmsOverIpStatus.Builder builder(XmlDoc doc) { + return builder() + .setEntitlementStatus( + doc.get(ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ENTITLEMENT_STATUS, + ServiceEntitlement.APP_SMSOIP) + .map(status -> Integer.parseInt(status)) + .orElse(EntitlementStatus.INCOMPATIBLE)); + } + + /** Builder of {@link Ts43SmsOverIpStatus}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Ts43SmsOverIpStatus build(); + + public abstract Builder setEntitlementStatus(int entitlementStatus); + } + + public boolean isActive() { + return entitlementStatus() == EntitlementStatus.ENABLED; + } + + public final String toString() { + return "Ts43SmsOverIpStatus {" + + "entitlementStatus=" + + entitlementStatus() + + "}"; + } +} diff --git a/src/com/android/imsserviceentitlement/ts43/Ts43VolteStatus.java b/src/com/android/imsserviceentitlement/ts43/Ts43VolteStatus.java new file mode 100644 index 0000000..d324c22 --- /dev/null +++ b/src/com/android/imsserviceentitlement/ts43/Ts43VolteStatus.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.ts43; + +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode; +import com.android.imsserviceentitlement.utils.XmlDoc; +import com.android.libraries.entitlement.ServiceEntitlement; + +import com.google.auto.value.AutoValue; + +/** + * Implementation of Volte entitlement status and server data availability for TS.43 entitlement + * solution. This class is only used to report the entitlement status of Volte. + */ +@AutoValue +public abstract class Ts43VolteStatus { + /** The entitlement status of Volte service. */ + public static class EntitlementStatus { + public EntitlementStatus() {} + + public static final int DISABLED = 0; + public static final int ENABLED = 1; + public static final int INCOMPATIBLE = 2; + public static final int PROVISIONING = 3; + } + + /** The entitlement status of Volte service. */ + public abstract int entitlementStatus(); + + public static Ts43VolteStatus.Builder builder() { + return new AutoValue_Ts43VolteStatus.Builder() + .setEntitlementStatus(EntitlementStatus.DISABLED); + } + + public static Ts43VolteStatus.Builder builder(XmlDoc doc) { + return builder() + .setEntitlementStatus( + doc.get(ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ENTITLEMENT_STATUS, + ServiceEntitlement.APP_VOLTE) + .map(status -> Integer.parseInt(status)) + .orElse(EntitlementStatus.INCOMPATIBLE)); + } + + /** Builder of {@link Ts43VolteStatus}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Ts43VolteStatus build(); + + public abstract Builder setEntitlementStatus(int entitlementStatus); + } + + public boolean isActive() { + return entitlementStatus() == EntitlementStatus.ENABLED; + } + + public final String toString() { + return "Ts43VolteStatus {" + + "entitlementStatus=" + + entitlementStatus() + + "}"; + } +} diff --git a/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatus.java b/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatus.java new file mode 100644 index 0000000..b202102 --- /dev/null +++ b/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatus.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.ts43; + +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes; +import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode; +import com.android.imsserviceentitlement.utils.XmlDoc; +import com.android.libraries.entitlement.ServiceEntitlement; + +import com.google.auto.value.AutoValue; + +/** + * Implementation of Vowifi entitlement status and server data availability for TS.43 entitlement + * solution. This class is only used to report the entitlement status of Vowifi. + */ +@AutoValue +public abstract class Ts43VowifiStatus { + /** The entitlement status of Vowifi service. */ + public static class EntitlementStatus { + public EntitlementStatus() {} + + public static final int DISABLED = 0; + public static final int ENABLED = 1; + public static final int INCOMPATIBLE = 2; + public static final int PROVISIONING = 3; + } + + /** The emergency address status of vowifi service. */ + public static class AddrStatus { + public AddrStatus() {} + + public static final int NOT_AVAILABLE = 0; + public static final int AVAILABLE = 1; + public static final int NOT_REQUIRED = 2; + public static final int IN_PROGRESS = 3; + } + + /** The terms and condition status of vowifi service. */ + public static class TcStatus { + public TcStatus() {} + + public static final int NOT_AVAILABLE = 0; + public static final int AVAILABLE = 1; + public static final int NOT_REQUIRED = 2; + public static final int IN_PROGRESS = 3; + } + + /** The provision status of vowifi service. */ + public static class ProvStatus { + public ProvStatus() {} + + public static final int NOT_PROVISIONED = 0; + public static final int PROVISIONED = 1; + public static final int NOT_REQUIRED = 2; + public static final int IN_PROGRESS = 3; + } + + /** The entitlement status of vowifi service. */ + public abstract int entitlementStatus(); + /** The terms and condition status of vowifi service. */ + public abstract int tcStatus(); + /** The emergency address status of vowifi service. */ + public abstract int addrStatus(); + /** The provision status of vowifi service. */ + public abstract int provStatus(); + + public static Ts43VowifiStatus.Builder builder() { + return new AutoValue_Ts43VowifiStatus.Builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setTcStatus(TcStatus.NOT_AVAILABLE) + .setAddrStatus(AddrStatus.NOT_AVAILABLE) + .setProvStatus(ProvStatus.NOT_PROVISIONED); + } + + public static Ts43VowifiStatus.Builder builder(XmlDoc doc) { + return builder() + .setEntitlementStatus( + doc.get(ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ENTITLEMENT_STATUS, + ServiceEntitlement.APP_VOWIFI) + .map(status -> Integer.parseInt(status)) + .orElse(EntitlementStatus.INCOMPATIBLE)) + .setTcStatus( + doc.get(ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.TC_STATUS, + ServiceEntitlement.APP_VOWIFI) + .map(status -> Integer.parseInt(status)) + .orElse(TcStatus.NOT_REQUIRED)) + .setAddrStatus( + doc.get(ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.ADDR_STATUS, + ServiceEntitlement.APP_VOWIFI) + .map(status -> Integer.parseInt(status)) + .orElse(AddrStatus.NOT_REQUIRED)) + .setProvStatus( + doc.get(ResponseXmlNode.APPLICATION, + ResponseXmlAttributes.PROVISION_STATUS, + ServiceEntitlement.APP_VOWIFI) + .map(status -> Integer.parseInt(status)) + .orElse(ProvStatus.NOT_REQUIRED)); + } + + /** Builder of {@link Ts43VowifiStatus}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Ts43VowifiStatus build(); + + public abstract Builder setEntitlementStatus(int entitlementStatus); + + public abstract Builder setTcStatus(int tcStatus); + + public abstract Builder setAddrStatus(int addrStatus); + + public abstract Builder setProvStatus(int provStatus); + } + + public boolean vowifiEntitled() { + return entitlementStatus() == EntitlementStatus.ENABLED + && (provStatus() == ProvStatus.PROVISIONED + || provStatus() == ProvStatus.NOT_REQUIRED) + && (tcStatus() == TcStatus.AVAILABLE || tcStatus() == TcStatus.NOT_REQUIRED) + && (addrStatus() == AddrStatus.AVAILABLE + || addrStatus() == AddrStatus.NOT_REQUIRED); + } + + public boolean serverDataMissing() { + return entitlementStatus() == EntitlementStatus.DISABLED + && (tcStatus() == TcStatus.NOT_AVAILABLE + || addrStatus() == AddrStatus.NOT_AVAILABLE); + } + + public boolean inProgress() { + return entitlementStatus() == EntitlementStatus.PROVISIONING + || (entitlementStatus() == EntitlementStatus.DISABLED + && (tcStatus() == TcStatus.IN_PROGRESS || addrStatus() == AddrStatus.IN_PROGRESS)) + || (entitlementStatus() == EntitlementStatus.DISABLED + && (provStatus() == ProvStatus.NOT_PROVISIONED + || provStatus() == ProvStatus.IN_PROGRESS) + && (tcStatus() == TcStatus.AVAILABLE || tcStatus() == TcStatus.NOT_REQUIRED) + && (addrStatus() == AddrStatus.AVAILABLE + || addrStatus() == AddrStatus.NOT_REQUIRED)); + } + + public boolean incompatible() { + return entitlementStatus() == EntitlementStatus.INCOMPATIBLE; + } + + @Override + public final String toString() { + return "Ts43VowifiStatus {" + + "entitlementStatus=" + + entitlementStatus() + + ",tcStatus=" + + tcStatus() + + ",addrStatus=" + + addrStatus() + + ",provStatus=" + + provStatus() + + "}"; + } +}
\ No newline at end of file diff --git a/src/com/android/imsserviceentitlement/utils/Executors.java b/src/com/android/imsserviceentitlement/utils/Executors.java new file mode 100644 index 0000000..3f8e68f --- /dev/null +++ b/src/com/android/imsserviceentitlement/utils/Executors.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.utils; + +import static android.os.AsyncTask.THREAD_POOL_EXECUTOR; + +import java.util.concurrent.Executor; + +/** Provides executors for running the tasks asynchronized. */ +public final class Executors { + /** + * Whether to execute entitlementCheck in caller's thread, set to true via reflection for test. + */ + private static boolean sUseDirectExecutorForTest = false; + + private static final Executor ASYNC_EXECUTOR = THREAD_POOL_EXECUTOR; + private static final Executor DIRECT_EXECUTOR = Runnable::run; + + private Executors() {} + + /** Returns {@link Executor} executing tasks asynchronously. */ + public static Executor getAsyncExecutor() { + return sUseDirectExecutorForTest ? DIRECT_EXECUTOR : ASYNC_EXECUTOR; + } + + /** Returns {@link Executor} executing tasks from the caller's thread. */ + public static Executor getDirectExecutor() { + return DIRECT_EXECUTOR; + } +} diff --git a/src/com/android/imsserviceentitlement/utils/ImsUtils.java b/src/com/android/imsserviceentitlement/utils/ImsUtils.java new file mode 100644 index 0000000..2ae94d8 --- /dev/null +++ b/src/com/android/imsserviceentitlement/utils/ImsUtils.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.utils; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.ims.ImsMmTelManager; +import android.telephony.ims.ProvisioningManager; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; + +/** A helper class for IMS relevant APIs with subscription id. */ +public class ImsUtils { + private static final String TAG = "IMSSE-ImsUtils"; + + private final CarrierConfigManager mCarrierConfigManager; + private final ImsMmTelManager mImsMmTelManager; + private final ProvisioningManager mProvisioningManager; + private final int mSubId; + + /** + * Turns Volte provisioning status ON/OFF. + * Value is in Integer format. ON (1), OFF(0). + * Key is from {@link ProvisioningManager#KEY_VOLTE_PROVISIONING_STATUS}. + */ + private static final int KEY_VOLTE_PROVISIONING_STATUS = 10; + + /** + * Turns SMS over IP ON/OFF on the device. + * Value is in Integer format. ON (1), OFF(0). + * Key is from {@link ProvisioningManager#KEY_SMS_OVER_IP_ENABLED}. + */ + private static final int KEY_SMS_OVER_IP_ENABLED = 14; + + /** + * Enable voice over wifi on device. + * Value is in Integer format. Enabled (1), or Disabled (0). + * Key is from {@link ProvisioningManager#KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE}. + */ + private static final int KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE = 28; + + // Cache subscription id associated {@link ImsUtils} objects for reusing. + @GuardedBy("ImsUtils.class") + private static SparseArray<ImsUtils> sInstances = new SparseArray<ImsUtils>(); + + private ImsUtils(Context context, int subId) { + mCarrierConfigManager = + (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE); + mImsMmTelManager = getImsMmTelManager(context, subId); + mProvisioningManager = getProvisioningManager(subId); + this.mSubId = subId; + } + + /** Returns {@link ImsUtils} instance. */ + public static synchronized ImsUtils getInstance(Context context, int subId) { + ImsUtils instance = sInstances.get(subId); + if (instance != null) { + return instance; + } + + instance = new ImsUtils(context, subId); + sInstances.put(subId, instance); + return instance; + } + + /** Changes persistent WFC enabled setting. */ + public void setWfcSetting(boolean enabled, boolean force) { + try { + if (force) { + mImsMmTelManager.setVoWiFiSettingEnabled(enabled); + } + } catch (RuntimeException e) { + // ignore this exception, possible exception should be NullPointerException or + // RemoteException. + } + } + + /** Sets whether VoWiFi is provisioned. */ + public void setVowifiProvisioned(boolean value) { + try { + mProvisioningManager.setProvisioningIntValue( + KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE, value + ? ProvisioningManager.PROVISIONING_VALUE_ENABLED + : ProvisioningManager.PROVISIONING_VALUE_DISABLED); + } catch (RuntimeException e) { + // ignore this exception, possible exception should be NullPointerException or + // RemoteException. + } + } + + /** Sets whether Volte is provisioned. */ + public void setVolteProvisioned(boolean value) { + try { + mProvisioningManager.setProvisioningIntValue( + KEY_VOLTE_PROVISIONING_STATUS, value + ? ProvisioningManager.PROVISIONING_VALUE_ENABLED + : ProvisioningManager.PROVISIONING_VALUE_DISABLED); + } catch (RuntimeException e) { + // ignore this exception, possible exception should be NullPointerException or + // RemoteException. + } + } + + /** Sets whether SMSoIP is provisioned. */ + public void setSmsoipProvisioned(boolean value) { + try { + mProvisioningManager.setProvisioningIntValue( + KEY_SMS_OVER_IP_ENABLED, value + ? ProvisioningManager.PROVISIONING_VALUE_ENABLED + : ProvisioningManager.PROVISIONING_VALUE_DISABLED); + } catch (RuntimeException e) { + // ignore this exception, possible exception should be NullPointerException or + // RemoteException. + } + } + + /** Disables WFC and reset WFC mode to carrier default value */ + public void disableAndResetVoWiFiImsSettings() { + try { + disableWfc(); + + // Reset WFC mode to carrier default value + if (mCarrierConfigManager != null) { + PersistableBundle b = mCarrierConfigManager.getConfigForSubId(mSubId); + if (b != null) { + mImsMmTelManager.setVoWiFiModeSetting( + b.getInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT)); + mImsMmTelManager.setVoWiFiRoamingModeSetting( + b.getInt( + CarrierConfigManager + .KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT)); + } + } + } catch (RuntimeException e) { + // ignore this exception, possible exception should be NullPointerException or + // RemoteException. + } + } + + /** + * Returns {@link ImsMmTelManager} with specific subscription id. + * Returns {@code null} if provided subscription id invalid. + */ + @Nullable + public static ImsMmTelManager getImsMmTelManager(Context context, int subId) { + try { + return ImsMmTelManager.createForSubscriptionId(subId); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't get ImsMmTelManager, IllegalArgumentException: subId = " + subId); + } + + return null; + } + + /** + * Returns {@link ProvisioningManager} with specific subscription id. + * Returns {@code null} if provided subscription id invalid. + */ + @Nullable + public static ProvisioningManager getProvisioningManager(int subId) { + try { + return ProvisioningManager.createForSubscriptionId(subId); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't get ProvisioningManager, IllegalArgumentException: subId = " + subId); + } + return null; + } + + /** Returns whether WFC is enabled by user for current subId */ + public boolean isWfcEnabledByUser() { + try { + return mImsMmTelManager.isVoWiFiSettingEnabled(); + } catch (RuntimeException e) { + // ignore this exception, possible exception should be NullPointerException or + // RemoteException. + } + return false; + } + + /** Calls {@link #disableAndResetVoWiFiImsSettings()} in background thread. */ + public static void turnOffWfc(ImsUtils imsUtils, Runnable action) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + imsUtils.disableAndResetVoWiFiImsSettings(); + return null; // To satisfy compiler + } + + @Override + protected void onPostExecute(Void result) { + action.run(); + } + }.execute(); + } + + /** Disables WFC */ + public void disableWfc() { + setWfcSetting(false, false); + } +} diff --git a/src/com/android/imsserviceentitlement/utils/TelephonyUtils.java b/src/com/android/imsserviceentitlement/utils/TelephonyUtils.java new file mode 100644 index 0000000..2601fd8 --- /dev/null +++ b/src/com/android/imsserviceentitlement/utils/TelephonyUtils.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.google.common.collect.ImmutableSet; + +import java.util.List; + +/** This class implements Telephony helper methods. */ +public class TelephonyUtils { + public static final String TAG = "IMSSE-TelephonyUtils"; + + private final ConnectivityManager mConnectivityManager; + private final TelephonyManager mTelephonyManager; + + public TelephonyUtils(Context context) { + this(context, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } + + public TelephonyUtils(Context context, int subId) { + if (SubscriptionManager.isValidSubscriptionId(subId)) { + mTelephonyManager = + context.getSystemService(TelephonyManager.class).createForSubscriptionId(subId); + } else { + mTelephonyManager = context.getSystemService(TelephonyManager.class); + } + mConnectivityManager = context.getSystemService(ConnectivityManager.class); + } + + /** Returns device timestamp in milliseconds. */ + public long getTimeStamp() { + return System.currentTimeMillis(); + } + + /** Returns device uptime in milliseconds. */ + public long getUptimeMillis() { + return android.os.SystemClock.uptimeMillis(); + } + + /** Returns device model name. */ + public String getDeviceName() { + return Build.MODEL; + } + + /** Returns device OS version. */ + public String getDeviceOsVersion() { + return Build.VERSION.RELEASE; + } + + /** Returns {@code true} if network is connected (cellular or WiFi). */ + public boolean isNetworkConnected() { + NetworkInfo activeNetwork = mConnectivityManager.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + + /** + * Returns the response of EAP-AKA authetication {@code data} or {@code null} on failure. + * + * <p>Requires permission: READ_PRIVILEGED_PHONE_STATE + */ + public String getEapAkaAuthentication(String data) { + return mTelephonyManager.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, TelephonyManager.AUTHTYPE_EAP_AKA, data); + } + + /** Returns carrier ID. */ + public int getCarrierId() { + return mTelephonyManager.getSimCarrierId(); + } + + /** Returns fine-grained carrier ID. */ + public int getSpecificCarrierId() { + return mTelephonyManager.getSimSpecificCarrierId(); + } + + /** + * Returns {@code true} if the {@code subId} still point to a actived SIM; {@code false} + * otherwise. + */ + public static boolean isActivedSubId(Context context, int subId) { + SubscriptionManager subscriptionManager = + (SubscriptionManager) context.getSystemService( + Context.TELEPHONY_SUBSCRIPTION_SERVICE); + SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId); + return subInfo != null; + } + + /** + * Returns the slot index for the actived {@code subId}; {@link + * SubscriptionManager#INVALID_SIM_SLOT_INDEX} otherwise. + */ + public static int getSlotId(Context context, int subId) { + SubscriptionManager subscriptionManager = + (SubscriptionManager) context.getSystemService( + Context.TELEPHONY_SUBSCRIPTION_SERVICE); + SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId); + if (subInfo != null) { + return subInfo.getSimSlotIndex(); + } + Log.d(TAG, "Can't find actived subscription for " + subId); + return SubscriptionManager.INVALID_SIM_SLOT_INDEX; + } + + /** Returns carrier config for the {@code subId}. */ + private static PersistableBundle getConfigForSubId(Context context, int subId) { + CarrierConfigManager carrierConfigManager = + context.getSystemService(CarrierConfigManager.class); + PersistableBundle carrierConfig = carrierConfigManager.getConfigForSubId(subId); + if (carrierConfig == null) { + Log.d(TAG, "getDefaultConfig"); + carrierConfig = CarrierConfigManager.getDefaultConfig(); + } + return carrierConfig; + } + + /** + * Returns FCM sender id for the {@code subId} or a default empty string if it is not available. + */ + public static String getFcmSenderId(Context context, int subId) { + return getConfigForSubId(context, subId).getString( + CarrierConfigManager.ImsServiceEntitlement.KEY_FCM_SENDER_ID_STRING, + "" + ); + } + + /** + * Returns entitlement server url for the {@code subId} or + * a default empty string if it is not available. + */ + public static String getEntitlementServerUrl(Context context, int subId) { + return getConfigForSubId(context, subId).getString( + CarrierConfigManager.ImsServiceEntitlement.KEY_ENTITLEMENT_SERVER_URL_STRING, + "" + ); + } + + /** + * Returns true if app needs to do IMS (VoLTE/VoWiFi/SMSoIP) provisioning in the background + * or false if it doesn't need to do. + */ + public static boolean isImsProvisioningRequired(Context context, int subId) { + return getConfigForSubId(context, subId).getBoolean( + CarrierConfigManager.ImsServiceEntitlement.KEY_IMS_PROVISIONING_BOOL, + false + ); + } + + /** Returns SubIds which support FCM. */ + public static ImmutableSet<Integer> getSubIdsWithFcmSupported(Context context) { + SubscriptionManager subscriptionManager = + context.getSystemService(SubscriptionManager.class); + List<SubscriptionInfo> infos = subscriptionManager.getActiveSubscriptionInfoList(); + if (infos == null) { + return ImmutableSet.of(); + } + + ImmutableSet.Builder<Integer> builder = ImmutableSet.builder(); + for (SubscriptionInfo info : infos) { + int subId = info.getSubscriptionId(); + if (isFcmPushNotificationSupported(context, subId)) { + builder.add(subId); + } + } + return builder.build(); + } + + private static boolean isFcmPushNotificationSupported(Context context, int subId) { + return !TelephonyUtils.getFcmSenderId(context, subId).isEmpty(); + } +} diff --git a/src/com/android/imsserviceentitlement/utils/XmlDoc.java b/src/com/android/imsserviceentitlement/utils/XmlDoc.java new file mode 100644 index 0000000..26299e9 --- /dev/null +++ b/src/com/android/imsserviceentitlement/utils/XmlDoc.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.utils; + +import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes.APP_ID; +import static com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode.APPLICATION; + +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.imsserviceentitlement.debug.DebugUtils; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; +import java.util.Optional; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** Wrap the raw content and parse it into nodes. */ +public class XmlDoc { + private static final String TAG = "IMSSE-XmlDoc"; + + private static final String NODE_CHARACTERISTIC = "characteristic"; + private static final String NODE_PARM = "parm"; + private static final String PARM_NAME = "name"; + private static final String PARM_VALUE = "value"; + + private final Map<String, Map<String, String>> mNodesMap = new ArrayMap<>(); + + public XmlDoc(String responseBody) { + parseXmlResponse(responseBody); + } + + /** Returns param value for given node and key. */ + public Optional<String> get(String node, String key, @Nullable String appId) { + Map<String, String> paramsMap = mNodesMap.get(combineKeyWithAppId(node, appId)); + return Optional.ofNullable(paramsMap == null ? null : paramsMap.get(key)); + } + + private String combineKeyWithAppId(String node, @Nullable String appId) { + return APPLICATION.equals(node) && !TextUtils.isEmpty(appId) ? node + "_" + appId : node; + } + + /** + * Parses the response body as per format defined in TS.43 2.7.2 New Characteristics for + * XML-Based Document. + */ + private void parseXmlResponse(String responseBody) { + if (responseBody == null) { + return; + } + + // Workaround: some server doesn't escape "&" in XML response and that will cause XML parser + // failure later. + // This is a quick impl of escaping w/o intorducing a ton of new dependencies. + responseBody = responseBody.replace("&", "&").replace("&amp;", "&"); + + try { + InputSource inputSource = new InputSource(new StringReader(responseBody)); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = builderFactory.newDocumentBuilder(); + Document doc = docBuilder.parse(inputSource); + doc.getDocumentElement().normalize(); + + if (DebugUtils.isPiiLoggable()) { + Log.d( + TAG, + "parseXmlResponseForNode() Root element: " + + doc.getDocumentElement().getNodeName()); + } + + NodeList nodeList = doc.getElementsByTagName(NODE_CHARACTERISTIC); + for (int i = 0; i < nodeList.getLength(); i++) { + NamedNodeMap map = nodeList.item(i).getAttributes(); + if (DebugUtils.isPiiLoggable()) { + Log.d( + TAG, + "parseAuthenticateResponse() node name=" + + nodeList.item(i).getNodeName() + + " node value=" + + map.item(0).getNodeValue()); + } + Map<String, String> paramsMap = new ArrayMap<>(); + Element element = (Element) nodeList.item(i); + paramsMap.putAll(parseParams(element.getElementsByTagName(NODE_PARM))); + mNodesMap.put( + combineKeyWithAppId(map.item(0).getNodeValue(), paramsMap.get(APP_ID)), + paramsMap); + } + } catch (ParserConfigurationException | IOException | SAXException e) { + Log.e(TAG, "Failed to parse XML node. " + e); + } + } + + private static Map<String, String> parseParams(NodeList nodeList) { + Map<String, String> nameValue = new ArrayMap<>(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + NamedNodeMap map = node.getAttributes(); + String name = ""; + String value = ""; + for (int j = 0; j < map.getLength(); j++) { + if (PARM_NAME.equals(map.item(j).getNodeName())) { + name = map.item(j).getNodeValue(); + } else if (PARM_VALUE.equals(map.item(j).getNodeName())) { + value = map.item(j).getNodeValue(); + } + } + if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value)) { + continue; + } + nameValue.put(name, value); + + if (DebugUtils.isPiiLoggable()) { + Log.d(TAG, "parseParams() put name '" + name + "' with value " + value); + } + } + return nameValue; + } +} diff --git a/tests/unittests/Android.bp b/tests/unittests/Android.bp new file mode 100644 index 0000000..262aa1e --- /dev/null +++ b/tests/unittests/Android.bp @@ -0,0 +1,38 @@ +// +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "ImsServiceEntitlementUnitTests", + srcs: ["src/**/*.java"], + manifest: "AndroidManifest.xml", + resource_dirs: [], + static_libs: [ + "ImsServiceEntitlementLib", + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.rules", + "mockito-target-minus-junit4", + "platform-test-annotations", + "testables", + "testng", + "truth-prebuilt", + ], + certificate: "platform", + test_suites: ["device-tests"], +} diff --git a/tests/unittests/AndroidManifest.xml b/tests/unittests/AndroidManifest.xml new file mode 100644 index 0000000..be8024f --- /dev/null +++ b/tests/unittests/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.imsserviceentitlement.tests"> + <application> + <uses-library android:name="android.test.runner" /> + </application> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.imsserviceentitlement" + android:label="IMS Service Entitlement App Tests"> + </instrumentation> +</manifest> diff --git a/tests/unittests/src/com/android/imsserviceentitlement/EntitlementUtilsTest.java b/tests/unittests/src/com/android/imsserviceentitlement/EntitlementUtilsTest.java new file mode 100644 index 0000000..474e755 --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/EntitlementUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.WfcActivationController.EntitlementResultCallback; +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.utils.Executors; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.lang.reflect.Field; + +@RunWith(AndroidJUnit4.class) +public class EntitlementUtilsTest { + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + @Mock private ImsEntitlementApi mMockImsEntitlementApi; + @Mock private EntitlementResultCallback mEntitlementResultCallback; + @Mock private EntitlementResult mEntitlementResult; + + @Before + public void setup() throws Exception { + Field field = Executors.class.getDeclaredField("sUseDirectExecutorForTest"); + field.setAccessible(true); + field.set(null, true); + } + + @Test + public void entitlementCheck_checkEntitlementStatusPass_onEntitlementResult() { + when(mMockImsEntitlementApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + + EntitlementUtils.entitlementCheck(mMockImsEntitlementApi, mEntitlementResultCallback); + + verify(mEntitlementResultCallback).onEntitlementResult(mEntitlementResult); + } + + @Test + public void entitlementCheck_checkEntitlementStatusWithRuntimeException_onFailure() { + when(mMockImsEntitlementApi.checkEntitlementStatus()).thenThrow(new RuntimeException()); + + EntitlementUtils.entitlementCheck(mMockImsEntitlementApi, mEntitlementResultCallback); + + verify(mEntitlementResultCallback, never()).onEntitlementResult(mEntitlementResult); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementApiTest.java b/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementApiTest.java new file mode 100644 index 0000000..d0ea3ee --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementApiTest.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior.NEEDS_TO_RESET; +import static com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior.VALID_DURING_VALIDITY; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration; +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.fcm.FcmTokenStore; +import com.android.imsserviceentitlement.utils.TelephonyUtils; +import com.android.libraries.entitlement.ServiceEntitlement; +import com.android.libraries.entitlement.ServiceEntitlementException; +import com.android.libraries.entitlement.ServiceEntitlementRequest; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.text.SimpleDateFormat; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +@RunWith(AndroidJUnit4.class) +public class ImsEntitlementApiTest { + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + + @Spy private Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock private ServiceEntitlement mMockServiceEntitlement; + @Mock private EntitlementConfiguration mMockEntitlementConfiguration; + @Mock private CarrierConfigManager mCarrierConfigManager; + + private static final int SUB_ID = 1; + private static final String FCM_TOKEN = "FCM_TOKEN"; + private static final String RAW_XML = + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\"1728000\"/>" + + " </characteristic>" + + " <characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX\"/>" + + " <parm name=\"validity\" value=\"3600\"/>" + + " </characteristic>" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2004\"/>" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + private static final String RAW_XML_NEW_TOKEN = + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\"1728000\"/>" + + " </characteristic>" + + " <characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"NEW_TOKEN\"/>" + + " <parm name=\"validity\" value=\"3600\"/>" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2004\"/>" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + + private static final String MULTIPLE_APPIDS_RAW_XML = + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\"1728000\"/>" + + " </characteristic>" + + " <characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX\"/>" + + " <parm name=\"validity\" value=\"3600\"/>" + + " </characteristic>" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2003\"/>" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2004\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>" + + " </characteristic>" + + " <characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2005\"/>" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + + private final EntitlementConfiguration mEntitlementConfiguration = + new EntitlementConfiguration(ApplicationProvider.getApplicationContext(), SUB_ID); + + private ImsEntitlementApi mImsEntitlementApi; + + @Before + public void setUp() { + setImsProvisioningBool(true); + FcmTokenStore.setToken(mContext, SUB_ID, FCM_TOKEN); + mEntitlementConfiguration.reset(); + } + + @Test + public void checkEntitlementStatus_verifyVowifiStatus() throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mEntitlementConfiguration); + when(mMockServiceEntitlement.queryEntitlementStatus( + eq(ImmutableList.of(ServiceEntitlement.APP_VOWIFI)), any())).thenReturn(RAW_XML); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(result.getVowifiStatus().vowifiEntitled()).isTrue(); + } + + @Test + public void checkEntitlementStatus_verifyImsAppsStatus() throws Exception { + setupImsEntitlementApi(mEntitlementConfiguration); + when(mMockServiceEntitlement.queryEntitlementStatus( + eq(ImmutableList.of( + ServiceEntitlement.APP_VOWIFI, + ServiceEntitlement.APP_VOLTE, + ServiceEntitlement.APP_SMSOIP)), any()) + ).thenReturn(MULTIPLE_APPIDS_RAW_XML); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(result.getVowifiStatus().vowifiEntitled()).isTrue(); + assertThat(result.getVolteStatus().isActive()).isTrue(); + assertThat(result.getSmsoveripStatus().isActive()).isTrue(); + } + + @Test + public void checkEntitlementStatus_verifyConfigs() throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mEntitlementConfiguration); + when(mMockServiceEntitlement.queryEntitlementStatus( + eq(ImmutableList.of(ServiceEntitlement.APP_VOWIFI)), + any())).thenReturn(RAW_XML); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(mEntitlementConfiguration.getVoWifiStatus()).isEqualTo(1); + assertThat(mEntitlementConfiguration.getVolteStatus()).isEqualTo(2); + assertThat(mEntitlementConfiguration.getSmsOverIpStatus()).isEqualTo(2); + assertThat(mEntitlementConfiguration.getToken().get()).isEqualTo( + "kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"); + assertThat(mEntitlementConfiguration.getTokenValidity()).isEqualTo(3600); + assertThat(mEntitlementConfiguration.entitlementValidation()).isEqualTo( + VALID_DURING_VALIDITY); + } + + @Test + public void checkEntitlementStatus_resultNull_verifyVowifiStatusAndConfigs() throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mEntitlementConfiguration); + when(mMockServiceEntitlement.queryEntitlementStatus( + eq(ImmutableList.of(ServiceEntitlement.APP_VOWIFI)), any())).thenReturn(null); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(result.getVowifiStatus().vowifiEntitled()).isFalse(); + assertThat(mEntitlementConfiguration.getVoWifiStatus()).isEqualTo(2); + assertThat(mEntitlementConfiguration.getVolteStatus()).isEqualTo(2); + assertThat(mEntitlementConfiguration.getSmsOverIpStatus()).isEqualTo(2); + assertThat(mEntitlementConfiguration.getToken().isPresent()).isFalse(); + assertThat(mEntitlementConfiguration.getTokenValidity()).isEqualTo(0); + assertThat(mEntitlementConfiguration.entitlementValidation()).isEqualTo(NEEDS_TO_RESET); + } + + @Test + public void checkEntitlementStatus_httpResponse511_dataStoreReset() throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mMockEntitlementConfiguration); + when(mMockServiceEntitlement.queryEntitlementStatus( + eq(ImmutableList.of(ServiceEntitlement.APP_VOWIFI)), any())) + .thenThrow( + new ServiceEntitlementException( + ERROR_HTTP_STATUS_NOT_SUCCESS, 511, "Invalid connection response")); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + verify(mMockEntitlementConfiguration).reset(); + assertThat(result).isNull(); + } + + @Test + public void checkEntitlementStatus_httpResponse511_fullAuthnDone() throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mEntitlementConfiguration); + mEntitlementConfiguration.update(RAW_XML); + // While perform fast-authn, throws exception with code 511 + when(mMockServiceEntitlement.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + authenticationRequest("kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"))) + .thenThrow( + new ServiceEntitlementException( + ERROR_HTTP_STATUS_NOT_SUCCESS, 511, "Invalid connection response")); + // While perform full-authn, return the result + when(mMockServiceEntitlement.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + authenticationRequest(null))) + .thenReturn(RAW_XML_NEW_TOKEN); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(result).isNotNull(); + assertThat(mEntitlementConfiguration.getToken().get()).isEqualTo("NEW_TOKEN"); + } + + @Test + public void checkEntitlementStatus_httpResponse503WithDateTime_returnsRetryAfter() + throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mEntitlementConfiguration); + mEntitlementConfiguration.update(RAW_XML); + Clock fixedClock = Clock.fixed(Instant.ofEpochSecond(0), ZoneOffset.UTC); + ImsEntitlementApi.sClock = fixedClock; + + // While perform fast-authn, throws exception with code 503 + when(mMockServiceEntitlement.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + authenticationRequest("kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"))) + .thenThrow( + new ServiceEntitlementException( + ERROR_HTTP_STATUS_NOT_SUCCESS, + 503, + getDateTimeAfter(120, fixedClock), + "Invalid connection response")); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(result).isNotNull(); + assertThat(result.getRetryAfterSeconds()).isEqualTo(120); + } + + @Test + public void checkEntitlementStatus_httpResponse503WithNumericValue_returnsRetryAfter() + throws Exception { + setImsProvisioningBool(false); + setupImsEntitlementApi(mEntitlementConfiguration); + mEntitlementConfiguration.update(RAW_XML); + // While perform fast-authn, throws exception with code 503 + when(mMockServiceEntitlement.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + authenticationRequest("kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"))) + .thenThrow( + new ServiceEntitlementException( + ERROR_HTTP_STATUS_NOT_SUCCESS, + 503, + "120", + "Invalid connection response")); + + EntitlementResult result = mImsEntitlementApi.checkEntitlementStatus(); + + assertThat(result).isNotNull(); + assertThat(result.getRetryAfterSeconds()).isEqualTo(120); + } + + private ServiceEntitlementRequest authenticationRequest(String token) { + ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder(); + if (token != null) { + requestBuilder.setAuthenticationToken(token); + } + requestBuilder.setNotificationToken(FcmTokenStore.getToken(mContext, SUB_ID)); + requestBuilder.setTerminalVendor("vendorX"); + requestBuilder.setTerminalModel("modelY"); + requestBuilder.setTerminalSoftwareVersion("versionZ"); + requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML); + return requestBuilder.build(); + } + + private void setupImsEntitlementApi(EntitlementConfiguration entitlementConfiguration) { + mImsEntitlementApi = new ImsEntitlementApi( + mContext, + SUB_ID, + TelephonyUtils.isImsProvisioningRequired(mContext, SUB_ID), + mMockServiceEntitlement, + entitlementConfiguration); + } + + private void setImsProvisioningBool(boolean provisioning) { + PersistableBundle carrierConfig = new PersistableBundle(); + carrierConfig.putBoolean( + CarrierConfigManager.ImsServiceEntitlement.KEY_IMS_PROVISIONING_BOOL, + provisioning + ); + when(mCarrierConfigManager.getConfigForSubId(SUB_ID)).thenReturn(carrierConfig); + when(mContext.getSystemService(CarrierConfigManager.class)) + .thenReturn(mCarrierConfigManager); + } + + private String getDateTimeAfter(long seconds, Clock fixedClock) { + SimpleDateFormat dateFormat = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormat.format(Date.from(fixedClock.instant().plusSeconds(seconds))); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementPollingServiceTest.java b/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementPollingServiceTest.java new file mode 100644 index 0000000..ce71f2c --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementPollingServiceTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.SparseArray; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.job.JobManager; +import com.android.imsserviceentitlement.ts43.Ts43SmsOverIpStatus; +import com.android.imsserviceentitlement.ts43.Ts43VolteStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.AddrStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.EntitlementStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.ProvStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.TcStatus; +import com.android.imsserviceentitlement.utils.ImsUtils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.lang.reflect.Field; + +@RunWith(AndroidJUnit4.class) +public class ImsEntitlementPollingServiceTest { + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + + @Spy private Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock private ImsUtils mImsUtils; + @Mock private JobParameters mJobParameters; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private SubscriptionInfo mSubscriptionInfo; + @Mock private ImsEntitlementApi mImsEntitlementApi; + @Mock private CarrierConfigManager mCarrierConfigManager; + + private ImsEntitlementPollingService mService; + private JobScheduler mScheduler; + + private static final int SUB_ID = 1; + private static final int SLOT_ID = 0; + + @Before + public void setUp() throws Exception { + mService = new ImsEntitlementPollingService(); + mService.attachBaseContext(mContext); + mService.onCreate(); + mService.onBind(null); + mService.injectImsEntitlementApi(mImsEntitlementApi); + mScheduler = mContext.getSystemService(JobScheduler.class); + setActivedSubscription(); + setupImsUtils(); + setJobParameters(); + setWfcEnabledByUser(true); + setImsProvisioningBool(false); + } + + @Test + public void doEntitlementCheck_isWfcEnabledByUserFalse_doNothing() throws Exception { + setWfcEnabledByUser(false); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsEntitlementApi, never()).checkEntitlementStatus(); + } + + + @Test + public void doEntitlementCheck_shouldTurnOffWfc_disableWfc() throws Exception { + EntitlementResult entitlementResult = getEntitlementResult(sDisableVoWiFi); + when(mImsEntitlementApi.checkEntitlementStatus()).thenReturn(entitlementResult); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsUtils).disableWfc(); + } + + @Test + public void doEntitlementCheck_shouldNotTurnOffWfc_enableWfc() throws Exception { + EntitlementResult entitlementResult = getEntitlementResult(sEnableVoWiFi); + when(mImsEntitlementApi.checkEntitlementStatus()).thenReturn(entitlementResult); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsUtils, never()).disableWfc(); + } + + @Test + public void doEntitlementCheck_shouldTurnOffImsApps_setAllProvisionedFalse() throws Exception { + setImsProvisioningBool(true); + EntitlementResult entitlementResult = getImsEntitlementResult( + sDisableVoWiFi, + sDisableVoLte, + sDisableSmsoverip + ); + when(mImsEntitlementApi.checkEntitlementStatus()).thenReturn(entitlementResult); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsUtils).setVolteProvisioned(false); + verify(mImsUtils).setVowifiProvisioned(false); + verify(mImsUtils).setSmsoipProvisioned(false); + } + + @Test + public void doEntitlementCheck_shouldTurnOnImsApps_setAllProvisionedTrue() throws Exception { + setImsProvisioningBool(true); + EntitlementResult entitlementResult = getImsEntitlementResult( + sEnableVoWiFi, + sEnableVoLte, + sEnableSmsoverip + ); + when(mImsEntitlementApi.checkEntitlementStatus()).thenReturn(entitlementResult); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsUtils).setVolteProvisioned(true); + verify(mImsUtils).setVowifiProvisioned(true); + verify(mImsUtils).setSmsoipProvisioned(true); + } + + @Test + public void doEntitlementCheck_ImsEntitlementShouldRetry_rescheduleJob() throws Exception { + setImsProvisioningBool(true); + EntitlementResult entitlementResult = + EntitlementResult.builder().setRetryAfterSeconds(120).build(); + when(mImsEntitlementApi.checkEntitlementStatus()).thenReturn(entitlementResult); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsUtils, never()).setVolteProvisioned(anyBoolean()); + verify(mImsUtils, never()).setVowifiProvisioned(anyBoolean()); + verify(mImsUtils, never()).setSmsoipProvisioned(anyBoolean()); + assertThat( + mScheduler.getPendingJob( + jobIdWithSubId(JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID, SUB_ID))) + .isNotNull(); + } + + @Test + public void doEntitlementCheck_WfcEntitlementShouldRetry_rescheduleJob() throws Exception { + EntitlementResult entitlementResult = + EntitlementResult.builder().setRetryAfterSeconds(120).build(); + when(mImsEntitlementApi.checkEntitlementStatus()).thenReturn(entitlementResult); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + verify(mImsUtils, never()).setVolteProvisioned(anyBoolean()); + verify(mImsUtils, never()).setVowifiProvisioned(anyBoolean()); + verify(mImsUtils, never()).setSmsoipProvisioned(anyBoolean()); + assertThat( + mScheduler.getPendingJob( + jobIdWithSubId(JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID, SUB_ID))) + .isNotNull(); + } + + @Test + public void enqueueJob_hasJob() { + ImsEntitlementPollingService.enqueueJob(mContext, SUB_ID, 0); + + assertThat( + mScheduler.getPendingJob( + jobIdWithSubId(JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID, SUB_ID))) + .isNotNull(); + } + + private void setActivedSubscription() { + when(mSubscriptionInfo.getSimSlotIndex()).thenReturn(SLOT_ID); + when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(mSubscriptionInfo); + when(mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)) + .thenReturn(mSubscriptionManager); + } + + private void setupImsUtils() throws Exception { + SparseArray<ImsUtils> imsUtilsInstances = new SparseArray<>(); + imsUtilsInstances.put(SUB_ID, mImsUtils); + Field field = ImsUtils.class.getDeclaredField("sInstances"); + field.setAccessible(true); + field.set(null, imsUtilsInstances); + } + + private void setWfcEnabledByUser(boolean isEnabled) { + when(mImsUtils.isWfcEnabledByUser()).thenReturn(isEnabled); + } + + private void setJobParameters() { + PersistableBundle bundle = new PersistableBundle(); + bundle.putInt(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, SUB_ID); + bundle.putInt(JobManager.EXTRA_SLOT_ID, SLOT_ID); + when(mJobParameters.getExtras()).thenReturn(bundle); + when(mJobParameters.getJobId()).thenReturn(JobManager.QUERY_ENTITLEMENT_STATUS_JOB_ID); + } + + private void setImsProvisioningBool(boolean provisioning) { + PersistableBundle carrierConfig = new PersistableBundle(); + carrierConfig.putBoolean( + CarrierConfigManager.ImsServiceEntitlement.KEY_IMS_PROVISIONING_BOOL, + provisioning + ); + when(mCarrierConfigManager.getConfigForSubId(SUB_ID)).thenReturn(carrierConfig); + when(mContext.getSystemService(CarrierConfigManager.class)) + .thenReturn(mCarrierConfigManager); + } + + private static EntitlementResult getEntitlementResult(Ts43VowifiStatus vowifiStatus) { + return EntitlementResult.builder() + .setVowifiStatus(vowifiStatus) + .build(); + } + + private static EntitlementResult getImsEntitlementResult( + Ts43VowifiStatus vowifiStatus, + Ts43VolteStatus volteStatus, + Ts43SmsOverIpStatus smsOverIpStatus) { + return EntitlementResult.builder() + .setVowifiStatus(vowifiStatus) + .setVolteStatus(volteStatus) + .setSmsoveripStatus(smsOverIpStatus) + .build(); + } + + private int jobIdWithSubId(int jobId, int subId) { + return 1000 * subId + jobId; + } + + private static final Ts43VowifiStatus sDisableVoWiFi = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setTcStatus(TcStatus.NOT_AVAILABLE) + .setAddrStatus(AddrStatus.NOT_AVAILABLE) + .setProvStatus(ProvStatus.NOT_PROVISIONED) + .build(); + + private static final Ts43VowifiStatus sEnableVoWiFi = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setTcStatus(TcStatus.AVAILABLE) + .setAddrStatus(AddrStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + private static final Ts43VolteStatus sDisableVoLte = + Ts43VolteStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .build(); + + private static final Ts43VolteStatus sEnableVoLte = + Ts43VolteStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .build(); + + private static final Ts43SmsOverIpStatus sDisableSmsoverip = + Ts43SmsOverIpStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .build(); + + private static final Ts43SmsOverIpStatus sEnableSmsoverip = + Ts43SmsOverIpStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .build(); +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementReceiverTest.java b/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementReceiverTest.java new file mode 100644 index 0000000..dbea9a1 --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementReceiverTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.PersistableBundle; +import android.os.UserManager; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration; +import com.android.imsserviceentitlement.job.JobManager; +import com.android.imsserviceentitlement.utils.Executors; +import com.android.imsserviceentitlement.utils.TelephonyUtils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.lang.reflect.Field; + +@RunWith(AndroidJUnit4.class) +public class ImsEntitlementReceiverTest { + private static final int SUB_ID = 1; + private static final int LAST_SUB_ID = 2; + private static final String RAW_XML = + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2004\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>\n"; + + private static final String RAW_XML_VERSION_0_VALIDITY_0 = + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"0\"/>\n" + + " <parm name=\"validity\" value=\"0\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>\n"; + + private static final String RAW_XML_INVALID_VERS = + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"-1\"/>\n" + + " <parm name=\"validity\" value=\"-1\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>\n"; + private static final ComponentName POLLING_SERVICE_COMPONENT_NAME = + ComponentName.unflattenFromString( + "com.android.imsserviceentitlement/.ImsEntitlementPollingService"); + + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + + @Mock private TelephonyUtils mMockTelephonyUtils; + @Mock private UserManager mMockUserManager; + @Mock private CarrierConfigManager mCarrierConfigManager; + @Mock private JobManager mMockJobManager; + + @Spy private final Context mContext = ApplicationProvider.getApplicationContext(); + + private ImsEntitlementReceiver mReceiver; + private boolean mIsBootUp; + + @Before + public void setUp() throws Exception { + mReceiver = new ImsEntitlementReceiver() { + @Override + protected Dependencies createDependency(Context context, int subId) { + Dependencies dependencies = new Dependencies(); + dependencies.userManager = mMockUserManager; + dependencies.telephonyUtils = mMockTelephonyUtils; + dependencies.jobManager = mMockJobManager; + return dependencies; + } + + @Override + protected boolean isBootUp(Context context, int slotId) { + return mIsBootUp; + } + }; + mIsBootUp = false; + + new EntitlementConfiguration(mContext, LAST_SUB_ID).update(RAW_XML); + new EntitlementConfiguration(mContext, SUB_ID).reset(); + + when(mMockUserManager.isSystemUser()).thenReturn(true); + + setLastSubId(LAST_SUB_ID, 0); + setupCarrierConfig(); + useDirectExecutor(); + } + + @Test + public void onReceive_simChanged_dataReset() { + mReceiver.onReceive(mContext, getCarrierConfigChangedIntent(SUB_ID, /* slotId= */ 0)); + + assertThat( + new EntitlementConfiguration(mContext, LAST_SUB_ID).getVoWifiStatus()).isEqualTo(2); + verify(mMockJobManager, times(1)).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_theSameSim_dataNotReset() { + mReceiver.onReceive( + mContext, getCarrierConfigChangedIntent(LAST_SUB_ID, /* slotId= */ 0)); + + assertThat( + new EntitlementConfiguration(mContext, LAST_SUB_ID).getVoWifiStatus()).isEqualTo(1); + verify(mMockJobManager, never()).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_differentSlot_dataNotReset() { + setLastSubId(LAST_SUB_ID, 1); + + mReceiver.onReceive( + mContext, getCarrierConfigChangedIntent(LAST_SUB_ID, /* slotId= */ 1)); + + assertThat( + new EntitlementConfiguration(mContext, LAST_SUB_ID).getVoWifiStatus()).isEqualTo(1); + verify(mMockJobManager, never()).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_simChangedAndDifferentSlotId_dataReset() { + setLastSubId(LAST_SUB_ID, 1); + + mReceiver.onReceive(mContext, getCarrierConfigChangedIntent(SUB_ID, /* slotId= */ 1)); + + assertThat( + new EntitlementConfiguration(mContext, LAST_SUB_ID).getVoWifiStatus()).isEqualTo(2); + verify(mMockJobManager).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_isSystemUser_jobScheduled() { + when(mMockUserManager.isSystemUser()).thenReturn(true); + + mReceiver.onReceive( + mContext, getCarrierConfigChangedIntent(SUB_ID, /* slotId= */ 0)); + + verify(mMockJobManager).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_notSystemUser_noJobScheduled() { + when(mMockUserManager.isSystemUser()).thenReturn(false); + + mReceiver.onReceive( + mContext, getCarrierConfigChangedIntent(SUB_ID, /* slotId= */ 0)); + + verify(mMockJobManager, never()).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_deviceBootUp_jobScheduled() { + new EntitlementConfiguration(mContext, LAST_SUB_ID).update(RAW_XML_VERSION_0_VALIDITY_0); + mIsBootUp = true; + + mReceiver.onReceive(mContext, getCarrierConfigChangedIntent(LAST_SUB_ID, /* slotId= */ 0)); + + verify(mMockJobManager).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onReceive_bootCompleteInvalidVers_noJobScheduled() { + new EntitlementConfiguration(mContext, LAST_SUB_ID).update(RAW_XML_INVALID_VERS); + mIsBootUp = true; + + mReceiver.onReceive(mContext, getCarrierConfigChangedIntent(LAST_SUB_ID, /* slotId= */ 0)); + + verify(mMockJobManager, never()).queryEntitlementStatusOnceNetworkReady(); + } + + private Intent getCarrierConfigChangedIntent(int subId, int slotId) { + Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId); + intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotId); + return intent; + } + + private void setupCarrierConfig() { + PersistableBundle carrierConfig = new PersistableBundle(); + carrierConfig.putBoolean( + CarrierConfigManager.ImsServiceEntitlement.KEY_IMS_PROVISIONING_BOOL, true); + when(mContext.getSystemService(CarrierConfigManager.class)) + .thenReturn(mCarrierConfigManager); + when(mCarrierConfigManager.getConfigForSubId(SUB_ID)).thenReturn(carrierConfig); + when(mCarrierConfigManager.getConfigForSubId(LAST_SUB_ID)).thenReturn(carrierConfig); + } + + private void setLastSubId(int subId, int slotId) { + SharedPreferences preferences = + mContext.getSharedPreferences("PREFERENCE_ACTIVATION_INFO", Context.MODE_PRIVATE); + preferences.edit().putInt("last_sub_id_" + slotId, subId).apply(); + } + + private void useDirectExecutor() throws Exception { + Field field = Executors.class.getDeclaredField("sUseDirectExecutorForTest"); + field.setAccessible(true); + field.set(null, true); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/WfcActivationControllerTest.java b/tests/unittests/src/com/android/imsserviceentitlement/WfcActivationControllerTest.java new file mode 100644 index 0000000..72b9341 --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/WfcActivationControllerTest.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.entitlement.EntitlementResult; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.AddrStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.EntitlementStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.ProvStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.TcStatus; +import com.android.imsserviceentitlement.utils.Executors; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.lang.reflect.Field; + +// TODO(b/176127289) add tests +@RunWith(AndroidJUnit4.class) +public class WfcActivationControllerTest { + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + @Mock private TelephonyManager mTelephonyManager; + @Mock private ImsEntitlementApi mActivationApi; + @Mock private WfcActivationUi mActivationUi; + @Mock private ConnectivityManager mConnectivityManager; + @Mock private NetworkInfo mNetworkInfo; + + private static final int SUB_ID = 1; + private static final String EMERGENCY_ADDRESS_WEB_URL = "webUrl"; + private static final String EMERGENCY_ADDRESS_WEB_DATA = "webData"; + private static final String TERMS_AND_CONDITION_WEB_URL = "tncUrl"; + private static final String WEBVIEW_JS_CONTROLLER_NAME = "webviewJsControllerName"; + + private WfcActivationController mWfcActivationController; + private Context mContext; + private Instrumentation mInstrumentation; + + @Before + public void setUp() throws Exception { + mContext = spy(ApplicationProvider.getApplicationContext()); + + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); + when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager); + setNetworkConnected(true); + + Field field = Executors.class.getDeclaredField("sUseDirectExecutorForTest"); + field.setAccessible(true); + field.set(null, true); + } + + @Test + public void startFlow_launchAppForActivation_setPurposeActivation() { + InOrder mOrderVerifier = inOrder(mActivationUi); + setNetworkConnected(false); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.startFlow(); + + verifyGeneralWaitingUiInOrder(mOrderVerifier, R.string.activate_title); + verifyErrorUiInOrder( + mOrderVerifier, + R.string.activate_title, + R.string.wfc_activation_error); + } + + @Test + public void startFlow_launchAppForUpdate_setPurposeUpdate() { + InOrder mOrderVerifier = inOrder(mActivationUi); + setNetworkConnected(false); + buildActivity(ActivityConstants.LAUNCH_APP_UPDATE); + + mWfcActivationController.startFlow(); + + verifyGeneralWaitingUiInOrder(mOrderVerifier, R.string.e911_title); + verifyErrorUiInOrder(mOrderVerifier, R.string.e911_title, R.string.address_update_error); + } + + @Test + public void startFlow_launchAppForShowTc_setPurposeUpdate() { + InOrder mOrderVerifier = inOrder(mActivationUi); + setNetworkConnected(false); + buildActivity(ActivityConstants.LAUNCH_APP_SHOW_TC); + + mWfcActivationController.startFlow(); + + verifyGeneralWaitingUiInOrder(mOrderVerifier, R.string.tos_title); + verifyErrorUiInOrder( + mOrderVerifier, + R.string.tos_title, + R.string.show_terms_and_condition_error); + } + + @Test + public void finishFlow_isFinishing_showGeneralWaitingUi() { + InOrder mOrderVerifier = inOrder(mActivationUi); + when(mActivationApi.checkEntitlementStatus()).thenReturn(null); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.finishFlow(); + + mOrderVerifier + .verify(mActivationUi) + .showActivationUi( + R.string.activate_title, + R.string.progress_text, + true, + 0, + Activity.RESULT_CANCELED, + 0); + mOrderVerifier + .verify(mActivationUi) + .showActivationUi( + R.string.activate_title, + R.string.wfc_activation_error, + false, + R.string.ok, + WfcActivationUi.RESULT_FAILURE, + 0); + } + + @Test + public void handleEntitlementStatusForActivation_isVowifiEntitledTrue_setActivityResultOk() { + EntitlementResult mEntitlementResult = + EntitlementResult.builder() + .setVowifiStatus( + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setTcStatus(TcStatus.AVAILABLE) + .setAddrStatus(AddrStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build()) + .build(); + when(mActivationApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.evaluateEntitlementStatus(); + + verify(mActivationUi).setResultAndFinish(Activity.RESULT_OK); + } + + @Test + public void handleEntitlementStatusForActivation_isServerDataMissingTrue_showWebview() { + EntitlementResult mEntitlementResult = + EntitlementResult.builder() + .setVowifiStatus( + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setTcStatus(TcStatus.NOT_AVAILABLE) + .setAddrStatus(AddrStatus.NOT_AVAILABLE) + .build()) + .setEmergencyAddressWebUrl(EMERGENCY_ADDRESS_WEB_URL) + .setEmergencyAddressWebData(EMERGENCY_ADDRESS_WEB_DATA) + .build(); + when(mActivationApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.evaluateEntitlementStatus(); + + verify(mActivationUi).showWebview(EMERGENCY_ADDRESS_WEB_URL, EMERGENCY_ADDRESS_WEB_DATA); + } + + @Test + public void handleEntitlementStatusForActivation_isIncompatibleTrue_showErrorUi() { + EntitlementResult mEntitlementResult = + EntitlementResult.builder() + .setVowifiStatus( + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.INCOMPATIBLE) + .build()) + .build(); + when(mActivationApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.evaluateEntitlementStatus(); + + verifyErrorUi(R.string.activate_title, R.string.failure_contact_carrier); + } + + @Test + public void handleEntitlementStatusForActivation_unexpectedStatus_showGeneralErrorUi() { + EntitlementResult mEntitlementResult = + EntitlementResult.builder() + .setVowifiStatus( + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setTcStatus(TcStatus.IN_PROGRESS) + .setAddrStatus(AddrStatus.IN_PROGRESS) + .build()) + .build(); + when(mActivationApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.evaluateEntitlementStatus(); + + verifyErrorUi(R.string.activate_title, R.string.wfc_activation_error); + } + + @Test + public void handleEntitlementStatusAfterActivation_isVowifiEntitledTrue_setActivityResultOk() { + EntitlementResult mEntitlementResult = + EntitlementResult.builder() + .setVowifiStatus( + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setTcStatus(TcStatus.AVAILABLE) + .setAddrStatus(AddrStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build()) + .build(); + when(mActivationApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.reevaluateEntitlementStatus(); + + verify(mActivationUi).setResultAndFinish(Activity.RESULT_OK); + } + + @Test + public void handleEntitlementStatusAfterActivation_unexpectedStatus_showGeneralErrorUi() { + EntitlementResult mEntitlementResult = + EntitlementResult.builder() + .setVowifiStatus( + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setTcStatus(TcStatus.IN_PROGRESS) + .setAddrStatus(AddrStatus.IN_PROGRESS) + .build()) + .build(); + when(mActivationApi.checkEntitlementStatus()).thenReturn(mEntitlementResult); + buildActivity(ActivityConstants.LAUNCH_APP_ACTIVATE); + + mWfcActivationController.reevaluateEntitlementStatus(); + + verifyErrorUi(R.string.activate_title, R.string.wfc_activation_error); + } + + private void buildActivity(int extraLaunchCarrierApp) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, SUB_ID); + intent.putExtra(ActivityConstants.EXTRA_LAUNCH_CARRIER_APP, extraLaunchCarrierApp); + mWfcActivationController = + new WfcActivationController(mContext, mActivationUi, mActivationApi, intent); + } + + private void setNetworkConnected(boolean isConnected) { + when(mNetworkInfo.isConnected()).thenReturn(isConnected); + when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn( + mConnectivityManager); + when(mConnectivityManager.getActiveNetworkInfo()).thenReturn(mNetworkInfo); + when(mNetworkInfo.isConnected()).thenReturn(isConnected); + } + + private void verifyErrorUi(int title, int errorMesssage) { + verify(mActivationUi) + .showActivationUi( + title, + errorMesssage, + false, R.string.ok, + WfcActivationUi.RESULT_FAILURE, + 0); + } + + private void verifyErrorUiInOrder(InOrder inOrder, int title, int errorMesssage) { + inOrder.verify(mActivationUi) + .showActivationUi( + title, + errorMesssage, + false, R.string.ok, + WfcActivationUi.RESULT_FAILURE, + 0); + } + + private void verifyGeneralWaitingUiInOrder(InOrder inOrder, int title) { + inOrder.verify(mActivationUi) + .showActivationUi(title, R.string.progress_text, true, 0, 0, 0); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationTest.java b/tests/unittests/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationTest.java new file mode 100644 index 0000000..4260e5e --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.entitlement; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.entitlement.EntitlementConfiguration.ClientBehavior; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class EntitlementConfigurationTest { + private static final String RAW_XML = + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"1728000\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX\"/>\n" + + " <parm name=\"validity\" value=\"3600\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2004\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2003\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"0\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>\n"; + private static final String RAW_XML_NO_TOKEN_VALIDITY = + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"1728000\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2004\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"1\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2003\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"0\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>\n"; + private static final int SUB_ID = 1; + + private Context mContext; + private EntitlementConfiguration mConfiguration; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + mConfiguration = new EntitlementConfiguration(mContext, SUB_ID); + mConfiguration.reset(); + } + + @Test + public void updateConfigurations_verifyConfigs() { + mConfiguration.update(RAW_XML); + + assertThat(mConfiguration.getVolteStatus()).isEqualTo(0); + assertThat(mConfiguration.getVoWifiStatus()).isEqualTo(1); + assertThat(mConfiguration.getSmsOverIpStatus()).isEqualTo(2); + assertThat(mConfiguration.getToken().get()).isEqualTo("kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"); + assertThat(mConfiguration.getTokenValidity()).isEqualTo(3600); + assertThat(mConfiguration.entitlementValidation()).isEqualTo( + ClientBehavior.VALID_DURING_VALIDITY); + } + + @Test + public void updateConfigurations_reset_verifyDefaultValues() { + mConfiguration.update(RAW_XML); + mConfiguration.reset(); + + assertThat(mConfiguration.getVolteStatus()).isEqualTo(2); + assertThat(mConfiguration.getVoWifiStatus()).isEqualTo(2); + assertThat(mConfiguration.getSmsOverIpStatus()).isEqualTo(2); + assertThat(mConfiguration.getToken().isPresent()).isFalse(); + assertThat(mConfiguration.getTokenValidity()).isEqualTo(0); + assertThat(mConfiguration.entitlementValidation()).isEqualTo(ClientBehavior.NEEDS_TO_RESET); + } + + @Test + public void updateConfigurations_noTokenValidity_tokenValid() { + mConfiguration.update(RAW_XML_NO_TOKEN_VALIDITY); + + assertThat(mConfiguration.getToken().get()).isEqualTo("kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"); + assertThat(mConfiguration.getTokenValidity()).isEqualTo(0); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmRegistrationServiceTest.java b/tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmRegistrationServiceTest.java new file mode 100644 index 0000000..7dcb0f1 --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmRegistrationServiceTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.job.JobManager; + +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.messaging.FirebaseMessaging; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class FcmRegistrationServiceTest { + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + + @Spy private Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock private JobParameters mJobParameters; + @Mock private FirebaseInstanceId mInstanceID; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private SubscriptionInfo mSubscriptionInfo; + @Mock private CarrierConfigManager mCarrierConfigManager; + + private FcmRegistrationService mService; + private JobScheduler mScheduler; + + private static final int SUB_ID = 1; + private static final String TOKEN = "TEST_TOKEN"; + private static final String SENDER_ID = "SENDER_ID"; + + @Before + public void setup() throws Exception { + setActiveSubscriptionInfoList(); + setFcmSenderIdString(SENDER_ID); + mService = new FcmRegistrationService(); + mService.attachBaseContext(mContext); + mService.onCreate(); + mService.onBind(null); + mScheduler = mContext.getSystemService(JobScheduler.class); + FcmTokenStore.setToken(mContext, SUB_ID, ""); + } + + @Test + public void enqueueJob_getPendingJob_registerFcmOnceNetworkReady() { + mService.enqueueJob(mContext); + + assertThat(mScheduler.getPendingJob(JobManager.REGISTER_FCM_JOB_ID)).isNotNull(); + } + + @Test + public void onStartJob_setToken_getToken() throws Exception { + when(mInstanceID.getToken(SENDER_ID, FirebaseMessaging.INSTANCE_ID_SCOPE)) + .thenReturn(TOKEN); + mService.setFakeInstanceID(mInstanceID); + + mService.onStartJob(mJobParameters); + mService.mOngoingTask.get(); // wait for job finish. + + assertThat(FcmTokenStore.getToken(mContext, SUB_ID)).isEqualTo(TOKEN); + } + + @Test + public void onStopJob_alwaysRetunedTrue() { + assertThat(mService.onStopJob(mJobParameters)).isTrue(); + } + + private void setActiveSubscriptionInfoList() { + when(mSubscriptionInfo.getSimSlotIndex()).thenReturn(0); + when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(mSubscriptionInfo); + when(mSubscriptionInfo.getSubscriptionId()).thenReturn(SUB_ID); + List<SubscriptionInfo> mSubscriptionInfoList = new ArrayList<>(); + mSubscriptionInfoList.add(mSubscriptionInfo); + when(mSubscriptionManager.getActiveSubscriptionInfoList()) + .thenReturn(mSubscriptionInfoList); + when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); + } + + private void setFcmSenderIdString(String senderId) { + PersistableBundle carrierConfig = new PersistableBundle(); + carrierConfig.putString( + CarrierConfigManager.ImsServiceEntitlement.KEY_FCM_SENDER_ID_STRING, + senderId + ); + when(mCarrierConfigManager.getConfigForSubId(SUB_ID)).thenReturn(carrierConfig); + when(mContext.getSystemService(CarrierConfigManager.class)) + .thenReturn(mCarrierConfigManager); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmServiceTest.java b/tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmServiceTest.java new file mode 100644 index 0000000..9a4024d --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmServiceTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.fcm; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.job.JobManager; +import com.android.libraries.entitlement.ServiceEntitlement; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class FcmServiceTest { + @Rule public final MockitoRule rule = MockitoJUnit.rule(); + + @Spy private Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private SubscriptionInfo mSubscriptionInfo; + @Mock private CarrierConfigManager mCarrierConfigManager; + @Mock private JobManager mJobManager; + + private FcmService mService; + + private static final String DATA_APP_KEY = "app"; + private static final String ERROR_DATA_APP_KEY = "error_app"; + private static final String DATA_TIMESTAMP_KEY = "timestamp"; + private static final String TIME_STAMP = "2019-01-29T13:15:31-08:00"; + private static final String SENDER_ID = "SENDER_ID"; + private static final int SUB_ID = 1; + + @Before + public void setup() throws Exception { + setActiveSubscriptionInfoList(); + setFcmSenderIdString(SENDER_ID); + mService = new FcmService(); + mService.attachBaseContext(mContext); + mService.onCreate(); + mService.setMockJobManager(mJobManager); + } + + @Test + public void onMessageReceived_isFcmSupported_queryEntitlementStatusOnceNetworkReady() { + Map<String, String> dataMap = setFcmData(DATA_APP_KEY, ServiceEntitlement.APP_VOWIFI); + + mService.onMessageReceived(SENDER_ID, dataMap); + + verify(mJobManager).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onMessageReceived_isNotTs43EntitlementsChangeEvent_noJobs() { + Map<String, String> dataMap = setFcmData(ERROR_DATA_APP_KEY, ServiceEntitlement.APP_VOWIFI); + + mService.onMessageReceived(SENDER_ID, dataMap); + + verify(mJobManager, never()).queryEntitlementStatusOnceNetworkReady(); + } + + @Test + public void onMessageReceived_emptySenderId_isNotFcmSupported() { + setFcmSenderIdString(""); + Map<String, String> dataMap = setFcmData(DATA_APP_KEY, ServiceEntitlement.APP_VOWIFI); + + mService.onMessageReceived(SENDER_ID, dataMap); + + verify(mJobManager, never()).queryEntitlementStatusOnceNetworkReady(); + } + + private Map<String, String> setFcmData(String dataAppKey, String dataAppValue) { + Map<String, String> dataMap = Map.of( + dataAppKey, dataAppValue, + DATA_TIMESTAMP_KEY, TIME_STAMP); + return dataMap; + } + + private void setActiveSubscriptionInfoList() { + when(mSubscriptionInfo.getSubscriptionId()).thenReturn(SUB_ID); + List<SubscriptionInfo> mSubscriptionInfoList = new ArrayList<>(); + mSubscriptionInfoList.add(mSubscriptionInfo); + when(mSubscriptionManager.getActiveSubscriptionInfoList()) + .thenReturn(mSubscriptionInfoList); + when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); + } + + private void setFcmSenderIdString(String senderId) { + PersistableBundle carrierConfig = new PersistableBundle(); + carrierConfig.putString( + CarrierConfigManager.ImsServiceEntitlement.KEY_FCM_SENDER_ID_STRING, + senderId + ); + when(mCarrierConfigManager.getConfigForSubId(SUB_ID)).thenReturn(carrierConfig); + when(mContext.getSystemService(CarrierConfigManager.class)) + .thenReturn(mCarrierConfigManager); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatusTest.java b/tests/unittests/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatusTest.java new file mode 100644 index 0000000..b39fcf4 --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatusTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.ts43; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.AddrStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.EntitlementStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.ProvStatus; +import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus.TcStatus; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class Ts43VowifiStatusTest { + @Test + public void ts43VowifiStatus_vowifiAvailable_vowifiEntitled() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isTrue(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_addressNotRequired_vowifiEntitled() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setAddrStatus(AddrStatus.NOT_REQUIRED) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isTrue(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_tcStatusNotRequired_vowifiEntitled() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.NOT_REQUIRED) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isTrue(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_provisionNotRequired_vowifiEntitled() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.NOT_REQUIRED) + .build(); + + assertThat(status.vowifiEntitled()).isTrue(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_addressNotAvailable_serverDataMissing() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.NOT_AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isTrue(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_tcStatusAvailable_serverDataMissing() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.NOT_AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isTrue(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_entitlementStatusProvisioning_inProgress() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.PROVISIONING) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isTrue(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_addressInProgress_inProgress() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.IN_PROGRESS) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isTrue(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_tcStatusInProgress_inProgress() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.IN_PROGRESS) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isTrue(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_provStatusNotProvisioned_inProgress() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.NOT_PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isTrue(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_provStatusInProgress_inProgress() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.IN_PROGRESS) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isTrue(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_provStatusInProgress_incompatible() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.INCOMPATIBLE) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isTrue(); + } + + @Test + public void ts43VowifiStatus_entitlementStatusEnabledAndServerDataMissing_noAnyMatches() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setAddrStatus(AddrStatus.NOT_AVAILABLE) + .setTcStatus(TcStatus.NOT_AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void ts43VowifiStatus_entitlementStatusDisabledAndServerDataNotRequired_noAnyMatches() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.DISABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.vowifiEntitled()).isFalse(); + assertThat(status.serverDataMissing()).isFalse(); + assertThat(status.inProgress()).isFalse(); + assertThat(status.incompatible()).isFalse(); + } + + @Test + public void toString_vowifiAvailable_statusLogged() { + Ts43VowifiStatus status = + Ts43VowifiStatus.builder() + .setEntitlementStatus(EntitlementStatus.ENABLED) + .setAddrStatus(AddrStatus.AVAILABLE) + .setTcStatus(TcStatus.AVAILABLE) + .setProvStatus(ProvStatus.PROVISIONED) + .build(); + + assertThat(status.toString()) + .isEqualTo("Ts43VowifiStatus {" + + "entitlementStatus=1,tcStatus=1,addrStatus=1,provStatus=1}"); + } +} diff --git a/tests/unittests/src/com/android/imsserviceentitlement/utils/XmlDocTest.java b/tests/unittests/src/com/android/imsserviceentitlement/utils/XmlDocTest.java new file mode 100644 index 0000000..986436f --- /dev/null +++ b/tests/unittests/src/com/android/imsserviceentitlement/utils/XmlDocTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.imsserviceentitlement.utils; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class XmlDocTest { + // XML sample from vendor A + private static final String AUTH_RESPONSE_XML = + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"1728000\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX\"/>\n" + + " <parm name=\"validity\" value=\"3600\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2004\"/>\n" + + " <parm name=\"Name\" value=\"VoWiFi Entitlement settings\"/>\n" + + " <parm name=\"EntitlementStatus\" value=\"0\"/>\n" + + " <parm name=\"AddrStatus\" value=\"0\"/>\n" + + " <parm name=\"TC_Status\" value=\"0\"/>\n" + + " <parm name=\"ProvStatus\" value=\"2\"/>\n" + + " <parm name=\"ServiceFlow_URL\"" + + " value=\"http://vm-host:8180/self-prov-websheet/rcs\"/>\n" + + " <parm name=\"ServiceFlow_UserData\"" + + " value=\"token=Y5vcmc%3D&entitlementStatus=0&protocol=TS43&" + + "provStatus=2&deviceId=358316079424742&subscriberId=0311580718847611" + + "%40nai.epc.mnc130.mcc310.3gppnetwork.org&ShowAddress=true\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>\n"; + + // XML sample from vendor B, note unescaped "&" in ServiceFlow_UserData + private static final String AUTH_RESPONSE_XML_2 = + "<?xml version=\"1.0\"?>" + + "<wap-provisioningdoc version=\"1.1\">" + + "<characteristic type=\"VERS\">" + + "<parm name=\"version\" value=\"4\"/>" + + "<parm name=\"validity\" value=\"172800\"/>" + + "</characteristic>" + + "<characteristic type=\"TOKEN\">" + + "<parm name=\"token\" value=\"kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX\"/>" + + "</characteristic>" + + "<characteristic type=\"APPLICATION\">" + + "<parm name=\"AppID\" value=\"ap2004\"/>" + + "<parm name=\"Name\" value=\"VoWiFi Entitlement settings\"/>" + + "<parm name=\"MessageForIncompatible\" value=\"99\"/>" + + "<parm name=\"EntitlementStatus\" value=\"0\"/>" + + "<parm name=\"ServiceFlow_URL\" value=\"" + + "https://deg.cspire.com/VoWiFi/CheckPostData\"/>" + + "<parm name=\"ServiceFlow_UserData\" value=\"" + + "PostData=U6%2FbQ%2BEP&req_locale=en_US\"/>" + + "<parm name=\"AddrStatus\" value=\"0\"/>" + + "<parm name=\"TC_Status\" value=\"0\"/>" + + "<parm name=\"ProvStatus\" value=\"0\"/>" + + "</characteristic>" + + "</wap-provisioningdoc>"; + + // A XML sample with "&amp;" - unlikely to happen in practice but good to test + private static final String AUTH_RESPONSE_XML_3 = + "<wap-provisioningdoc version=\"1.1\">" + + "<characteristic type=\"APPLICATION\">" + + "<parm name=\"AppID\" value=\"ap2004\"/>" + + "<parm name=\"ServiceFlow_UserData\" value=\"" + + "PostData=U6%2FbQ%2BEP&amp;l=en_US\"/>" + + "</characteristic>" + + "</wap-provisioningdoc>"; + + // A XML sample with server URL and user data unset. + private static final String AUTH_RESPONSE_XML_4 = + "<wap-provisioningdoc version=\"1.1\">" + + "<characteristic type=\"APPLICATION\">" + + "<parm name=\"AppID\" value=\"ap2004\"/>" + + "<parm name=\"ServiceFlow_URL\" value=\"\"" + + "<parm name=\"ServiceFlow_UserData\" value=\"\"/>" + + "</characteristic>" + + "</wap-provisioningdoc>"; + + // A XML sample with multiple appIDs + private static final String AUTH_RESPONSE_XML_5 = + "<wap-provisioningdoc version=\"1.1\">" + + "<characteristic type=\"APPLICATION\">" + + "<parm name=\"AppID\" value=\"ap2004\"/>" + + "<parm name=\"EntitlementStatus\" value=\"0\"/>" + + "</characteristic>" + + "<characteristic type=\"APPLICATION\">" + + "<parm name=\"AppID\" value=\"ap2005\"/>" + + "<parm name=\"EntitlementStatus\" value=\"1\"/>" + + "</characteristic>" + + "</wap-provisioningdoc>"; + + private static final String TOKEN = "kZYfCEpSsMr88KZVmab5UsZVzl+nWSsX"; + + @Test + public void parseAuthenticateResponse() { + XmlDoc xmlDoc = new XmlDoc(AUTH_RESPONSE_XML); + + assertThat(xmlDoc.get("TOKEN", "token", "ap2004").get()).isEqualTo(TOKEN); + // Note "&" in input XML are un-escaped to "&". + assertThat(xmlDoc.get("APPLICATION", "ServiceFlow_UserData", "ap2004").get()) + .isEqualTo("token=Y5vcmc%3D" + + "&entitlementStatus=0" + + "&protocol=TS43" + + "&provStatus=2" + + "&deviceId=358316079424742" + + "&subscriberId=0311580718847611%40nai.epc.mnc130.mcc310.3gppnetwork.org" + + "&ShowAddress=true"); + } + + @Test + public void parseAuthenticateResponse2() { + XmlDoc xmlDoc = new XmlDoc(AUTH_RESPONSE_XML_2); + + assertThat(xmlDoc.get("TOKEN", "token", "ap2004").get()).isEqualTo(TOKEN); + // Note the "&" in input XML is kept as is + assertThat(xmlDoc.get("APPLICATION", "ServiceFlow_UserData", "ap2004").get()) + .isEqualTo("PostData=U6%2FbQ%2BEP&req_locale=en_US"); + } + + @Test + public void parseAuthenticateResponse3() { + XmlDoc xmlDoc = new XmlDoc(AUTH_RESPONSE_XML_3); + + // Note the "&amp;" in input XML is un-escaped to "&" + assertThat(xmlDoc.get("APPLICATION", "ServiceFlow_UserData", "ap2004").get()) + .isEqualTo("PostData=U6%2FbQ%2BEP&l=en_US"); + } + + @Test + public void parseAuthenticateResponse4() { + XmlDoc xmlDoc = new XmlDoc(AUTH_RESPONSE_XML_4); + + assertThat(xmlDoc.get("APPLICATION", "ServiceFlow_URL", "ap2004").isPresent()).isFalse(); + assertThat( + xmlDoc.get("APPLICATION", "ServiceFlow_UserData", "ap2004").isPresent()).isFalse(); + } + + @Test + public void parseAuthenticateResponse5() { + XmlDoc xmlDoc = new XmlDoc(AUTH_RESPONSE_XML_5); + + assertThat(xmlDoc.get("APPLICATION", "EntitlementStatus", "ap2004").get()).isEqualTo("0"); + assertThat(xmlDoc.get("APPLICATION", "EntitlementStatus", "ap2005").get()).isEqualTo("1"); + } +} |