summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2021-10-06 22:53:55 +0000
committerXin Li <delphij@google.com>2021-10-06 22:53:55 +0000
commit3b0e1ef3e5f6c1e0eb18cc4e0e3e39439cf36cc3 (patch)
treea02088ca955f2ab31aad58fdc2c40f57b1d80bde
parent8b96dede56e916bb8dd58c02372b3833403803a4 (diff)
parent104894e9086c8563b91f6488d97428b80c742781 (diff)
downloadImsServiceEntitlement-3b0e1ef3e5f6c1e0eb18cc4e0e3e39439cf36cc3.tar.gz
Merge Android 12
Bug: 202323961 Merged-In: I8abe9ad7f1ddd26e960c2f1e1f08e71c278df027 Change-Id: I8c020f916580005ebd3e8a3db79e99287e15f564
-rw-r--r--Android.bp80
-rw-r--r--AndroidManifest.xml77
-rw-r--r--OWNERS3
-rw-r--r--PREUPLOAD.cfg2
-rw-r--r--TEST_MAPPING12
-rw-r--r--proguard.flags4
-rw-r--r--res/drawable/ic_phone_in_talk.xml26
-rw-r--r--res/drawable/stroke.xml28
-rw-r--r--res/layout/activity_wfc_activation.xml28
-rw-r--r--res/layout/fragment_suw_ui.xml46
-rw-r--r--res/layout/fragment_webview.xml36
-rw-r--r--res/values/config.xml29
-rw-r--r--res/values/dimens.xml21
-rw-r--r--res/values/integers.xml21
-rw-r--r--res/values/strings.xml62
-rw-r--r--res/values/styles.xml70
-rw-r--r--src/com/android/imsserviceentitlement/ActivityConstants.java71
-rw-r--r--src/com/android/imsserviceentitlement/EntitlementUtils.java93
-rw-r--r--src/com/android/imsserviceentitlement/ImsEntitlementApi.java224
-rw-r--r--src/com/android/imsserviceentitlement/ImsEntitlementPollingService.java380
-rw-r--r--src/com/android/imsserviceentitlement/ImsEntitlementReceiver.java173
-rw-r--r--src/com/android/imsserviceentitlement/SuwUiFragment.java134
-rw-r--r--src/com/android/imsserviceentitlement/WfcActivationActivity.java156
-rw-r--r--src/com/android/imsserviceentitlement/WfcActivationController.java374
-rw-r--r--src/com/android/imsserviceentitlement/WfcActivationUi.java53
-rw-r--r--src/com/android/imsserviceentitlement/WfcWebPortalFragment.java171
-rw-r--r--src/com/android/imsserviceentitlement/debug/DebugUtils.java66
-rw-r--r--src/com/android/imsserviceentitlement/entitlement/EntitlementConfiguration.java254
-rw-r--r--src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationsDataStore.java64
-rw-r--r--src/com/android/imsserviceentitlement/entitlement/EntitlementResult.java106
-rw-r--r--src/com/android/imsserviceentitlement/fcm/FcmRegistrationReceiver.java32
-rw-r--r--src/com/android/imsserviceentitlement/fcm/FcmRegistrationService.java154
-rw-r--r--src/com/android/imsserviceentitlement/fcm/FcmService.java135
-rw-r--r--src/com/android/imsserviceentitlement/fcm/FcmTokenStore.java83
-rw-r--r--src/com/android/imsserviceentitlement/fcm/FcmUtils.java73
-rw-r--r--src/com/android/imsserviceentitlement/job/JobManager.java180
-rw-r--r--src/com/android/imsserviceentitlement/ts43/Ts43Constants.java60
-rw-r--r--src/com/android/imsserviceentitlement/ts43/Ts43SmsOverIpStatus.java78
-rw-r--r--src/com/android/imsserviceentitlement/ts43/Ts43VolteStatus.java78
-rw-r--r--src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatus.java175
-rw-r--r--src/com/android/imsserviceentitlement/utils/Executors.java44
-rw-r--r--src/com/android/imsserviceentitlement/utils/ImsUtils.java219
-rw-r--r--src/com/android/imsserviceentitlement/utils/TelephonyUtils.java195
-rw-r--r--src/com/android/imsserviceentitlement/utils/XmlDoc.java148
-rw-r--r--tests/unittests/Android.bp38
-rw-r--r--tests/unittests/AndroidManifest.xml25
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/EntitlementUtilsTest.java70
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementApiTest.java338
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementPollingServiceTest.java304
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/ImsEntitlementReceiverTest.java236
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/WfcActivationControllerTest.java314
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/entitlement/EntitlementConfigurationTest.java116
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmRegistrationServiceTest.java127
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/fcm/FcmServiceTest.java132
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/ts43/Ts43VowifiStatusTest.java271
-rw-r--r--tests/unittests/src/com/android/imsserviceentitlement/utils/XmlDocTest.java167
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>
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..a167e8d
--- /dev/null
+++ b/OWNERS
@@ -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("&", "&amp;").replace("&amp;amp;", "&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&amp;entitlementStatus=0&amp;protocol=TS43&amp;"
+ + "provStatus=2&amp;deviceId=358316079424742&amp;subscriberId=0311580718847611"
+ + "%40nai.epc.mnc130.mcc310.3gppnetwork.org&amp;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;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;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 "&amp;" 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;amp;" in input XML is un-escaped to "&amp;"
+ assertThat(xmlDoc.get("APPLICATION", "ServiceFlow_UserData", "ap2004").get())
+ .isEqualTo("PostData=U6%2FbQ%2BEP&amp;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");
+ }
+}