summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordanielwbhuang <danielwbhuang@google.com>2021-01-08 15:04:15 +0800
committerMeng Wang <mewan@google.com>2021-01-15 17:11:03 -0800
commita1ecd874379f178a57946d01ccdfbaf88df6e5dc (patch)
tree559840eb5502cf186e8412ac8bc57bcdbfeaab9f
parent53875cf2b9ea5d638c81d0c2f7b9af09f70da1d6 (diff)
downloadImsServiceEntitlement-a1ecd874379f178a57946d01ccdfbaf88df6e5dc.tar.gz
Implement ImsServiceEntitlement app
Implement GSMA TS.43 based IMS service entitlement app in Android. Bug: 176127289 Test: build pass Change-Id: Ia4ff4ea9b8284225f946f739c2adc48f3c9df106
-rw-r--r--Android.bp41
-rw-r--r--AndroidManifest.xml40
-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.xml21
-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.java98
-rw-r--r--src/com/android/ImsServiceEntitlement/SuwUiFragment.java134
-rw-r--r--src/com/android/ImsServiceEntitlement/WfcActivationActivity.java151
-rw-r--r--src/com/android/ImsServiceEntitlement/WfcActivationApi.java144
-rw-r--r--src/com/android/ImsServiceEntitlement/WfcActivationController.java389
-rw-r--r--src/com/android/ImsServiceEntitlement/WfcActivationUi.java53
-rw-r--r--src/com/android/ImsServiceEntitlement/WfcWebPortalFragment.java169
-rw-r--r--src/com/android/ImsServiceEntitlement/debug/DebugUtils.java68
-rw-r--r--src/com/android/ImsServiceEntitlement/entitlement/EntitlementResult.java109
-rw-r--r--src/com/android/ImsServiceEntitlement/entitlement/VowifiStatus.java32
-rw-r--r--src/com/android/ImsServiceEntitlement/ts43/Ts43Constants.java54
-rw-r--r--src/com/android/ImsServiceEntitlement/ts43/Ts43VowifiStatus.java167
-rw-r--r--src/com/android/ImsServiceEntitlement/utils/ImsUtils.java131
-rw-r--r--src/com/android/ImsServiceEntitlement/utils/TelephonyUtils.java137
-rw-r--r--src/com/android/ImsServiceEntitlement/utils/XmlDoc.java140
29 files changed, 2488 insertions, 3 deletions
diff --git a/Android.bp b/Android.bp
index e4ca567..0f927eb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,4 +1,3 @@
-//
// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +15,42 @@
genrule {
name: "statslog-imsentitlement-java-gen",
tools: ["stats-log-api-gen"],
- cmd: "$(location stats-log-api-gen) --java $(out) --module imsentitlement --javaPackage com.android.imsentitlement --javaClass ImsentitlementStatsLog",
- out: ["com/android/imsentitlement/ImsentitlementStatsLog.java"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module imsentitlement --javaPackage com.android.imsserviceentitlement --javaClass ImsServiceEntitlementStatsLog",
+ out: ["com/android/imsserviceentitlement/ImsServiceEntitlementStatsLog.java"],
}
+
+android_app {
+ name: "ImsServiceEntitlement",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "android-support-v4",
+ "androidx.legacy_legacy-support-v4",
+ "service-entitlement",
+ "setupdesign",
+ "guava",
+ ],
+
+ libs: [
+ "auto_value_annotations",
+ ],
+
+ plugins: ["auto_value_plugin"],
+
+ resource_dirs: ["res"],
+
+ srcs: [
+ "src/**/*.java",
+ ":statslog-imsentitlement-java-gen",
+ ],
+
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
+
+ product_specific: true,
+ sdk_version: "system_current",
+ certificate: "platform",
+ privileged: true,
+ required: ["privapp_whitelist_com.android.imsserviceentitlement"],
+} \ No newline at end of file
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..7abcb69
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?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
+ android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
+ tools:replace="android:appComponentFactory">
+
+ <activity
+ android:name=".WfcActivationActivity"
+ android:exported="true"
+ android:screenOrientation="nosensor"
+ android:theme="@style/SudThemeGlif.Light">
+ </activity>
+ </application>
+</manifest> \ No newline at end of file
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..46d4c18
--- /dev/null
+++ b/res/values/config.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>
+ <!-- A test FCM token -->
+ <string name="fcm_token" translatable="false">erqKraIJ2mU%3AAPA91bHhjawbD_JHyIqkDZmpr8POX9HldCRTnapq_0NrhhBvODGwxNVHaT1d36r_OsZfGbRDa3HnO9VsMx09LNDtCSna3M2MoCGTIY6QIbHamOM2QnUSZYZhqJbsoVlsesP5DfGcw9sP</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..9fd29af
--- /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 = "WfcActivationActivity";
+
+ /** 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() {}
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/EntitlementUtils.java b/src/com/android/ImsServiceEntitlement/EntitlementUtils.java
new file mode 100644
index 0000000..2430d13
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/EntitlementUtils.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util.Log;
+
+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;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+/** Handle entitlement check */
+public class EntitlementUtils {
+
+ public static final String LOG_TAG = "WfcActivationActivity";
+
+ private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
+ private static final ExecutorService DIRECT_EXECUTOR_SERVICE =
+ MoreExecutors.newDirectExecutorService();
+ private static ListenableFuture<EntitlementResult> checkEntitlementFuture;
+
+ /**
+ * Whether to execute entitlementCheck in caller's thread, set to true via reflection for test.
+ */
+ private static boolean useDirectExecutorForTest = false;
+
+ private EntitlementUtils() {}
+
+ public static void entitlementCheck(
+ WfcActivationApi activationApi, EntitlementResultCallback callback) {
+ ListeningExecutorService service =
+ MoreExecutors.listeningDecorator(
+ useDirectExecutorForTest ? DIRECT_EXECUTOR_SERVICE : EXECUTOR_SERVICE);
+ checkEntitlementFuture = service.submit(() -> getEntitlementStatus(activationApi));
+ Futures.addCallback(
+ checkEntitlementFuture,
+ new FutureCallback<EntitlementResult>() {
+ @Override
+ public void onSuccess(EntitlementResult result) {
+ callback.onEntitlementResult(result);
+ checkEntitlementFuture = null;
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.w(LOG_TAG, "get entitlement status failed.", t);
+ checkEntitlementFuture = null;
+ }
+ },
+ DIRECT_EXECUTOR_SERVICE);
+ }
+
+ public static void cancelEntitlementCheck() {
+ if (checkEntitlementFuture != null) {
+ Log.i(LOG_TAG, "cancel entitlement status check.");
+ checkEntitlementFuture.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(WfcActivationApi activationApi) {
+ try {
+ return activationApi.checkEntitlementStatus();
+ } catch (RuntimeException e) {
+ Log.e("WfcActivationActivity", "getEntitlementStatus failed.", e);
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/SuwUiFragment.java b/src/com/android/ImsServiceEntitlement/SuwUiFragment.java
new file mode 100644
index 0000000..196832d
--- /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.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+import androidx.annotation.StringRes;
+
+/** 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);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/WfcActivationActivity.java b/src/com/android/ImsServiceEntitlement/WfcActivationActivity.java
new file mode 100644
index 0000000..d28b59d
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/WfcActivationActivity.java
@@ -0,0 +1,151 @@
+/*
+ * 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.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import com.google.android.setupdesign.util.ThemeResolver;
+
+import androidx.annotation.StringRes;
+
+/** The UI for WFC activation. */
+public class WfcActivationActivity extends FragmentActivity implements WfcActivationUi {
+ private static final String TAG = "WfcActivationActivity";
+
+ // Dependencies
+ private WfcActivationController wfcActivationController;
+ private WfcWebPortalFragment wfcWebPortalFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ createDependeny();
+ setSuwTheme();
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_wfc_activation);
+
+ int subId = ActivityConstants.getSubId(getIntent());
+ wfcActivationController.startFlow();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ wfcActivationController.finish();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (wfcWebPortalFragment != null && wfcWebPortalFragment.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, String jsControllerName) {
+ runOnUiThreadIfAlive(
+ () -> {
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ wfcWebPortalFragment = WfcWebPortalFragment.newInstance(
+ url,
+ postData,
+ jsControllerName);
+ ft.replace(R.id.wfc_activation_container, wfcWebPortalFragment);
+ // 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 wfcActivationController;
+ }
+
+ private void setSuwTheme() {
+ int theme =
+ ThemeResolver.getDefault().resolve(
+ SystemProperties.get("setupwizard.theme"),
+ false);
+ setTheme(theme != 0 ? theme : R.style.SudThemeGlif_Light);
+ }
+
+ private void createDependeny() {
+ Log.d(TAG, "Loading dependencies...");
+ // TODO(b/177495634) Use DependencyInjector
+ if (wfcActivationController == null) {
+ // Default initialization
+ Log.d(TAG, "Default WfcActivationController initialization");
+ Intent startIntent = this.getIntent();
+ int subId = ActivityConstants.getSubId(startIntent);
+ wfcActivationController =
+ new WfcActivationController(
+ /* context = */ this,
+ /* wfcActivationUi = */ this,
+ new WfcActivationApi(this, subId),
+ this.getIntent());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/WfcActivationApi.java b/src/com/android/ImsServiceEntitlement/WfcActivationApi.java
new file mode 100644
index 0000000..482920d
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/WfcActivationApi.java
@@ -0,0 +1,144 @@
+/*
+ * 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.Context;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.imsserviceentitlement.entitlement.EntitlementResult;
+import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes;
+import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode;
+import com.android.imsserviceentitlement.ts43.Ts43VowifiStatus;
+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 androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+/** Implementation of the entitlement API. */
+public class WfcActivationApi {
+ private static final String TAG = "WfcActivationActivity";
+ private static final String JS_CONTROLLER_NAME = "VoWiFiWebServiceFlow";
+
+ private final Context context;
+ private final int subId;
+ private final ServiceEntitlement serviceEntitlement;
+
+ private String mCachedAccessToken;
+
+ public WfcActivationApi(Context context, int subId) {
+ this.context = context;
+ this.subId = subId;
+ CarrierConfig carrierConfig = getCarrierConfig(context);
+ this.serviceEntitlement = new ServiceEntitlement(context, carrierConfig, subId);
+ }
+
+ @VisibleForTesting
+ WfcActivationApi(Context context, int subId, ServiceEntitlement serviceEntitlement) {
+ this.context = context;
+ this.subId = subId;
+ this.serviceEntitlement = serviceEntitlement;
+ }
+
+ /**
+ * 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() {
+ return voWifiEntitlementStatus();
+ }
+
+ /** Returns the name of JS controller object used in emergency address webview. */
+ public String getWebviewJsControllerName() {
+ return JS_CONTROLLER_NAME;
+ }
+
+ /**
+ * Runs when WFC is going to be turned ON/OFF.
+ *
+ * @param setting {@code true} for WFC ON, {@code false} for WFC OFF.
+ * @param entitlement The entitlement check result that results in WFC setting change, returned
+ * by {@code #checkEntitlementStatus()}.
+ */
+ public void onWfcSettingChanged(boolean setting, @Nullable EntitlementResult entitlement) {}
+
+ /** Query for status of {@link AppId#VOWIFI}). */
+ @VisibleForTesting
+ EntitlementResult voWifiEntitlementStatus() {
+ Log.d(TAG, "voWifiEntitlementStatus subId=" + subId);
+
+ ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder();
+ if (!TextUtils.isEmpty(mCachedAccessToken)) {
+ requestBuilder.setAuthenticationToken(mCachedAccessToken);
+ }
+ // TODO(b/177499703): Add FCM support and remove this hard-coded token.
+ requestBuilder.setNotificationToken(context.getString(R.string.fcm_token));
+ // Set fake device info to avoid leaking
+ requestBuilder.setTerminalVendor("vendorX");
+ requestBuilder.setTerminalModel("modelY");
+ requestBuilder.setTerminalSoftwareVersion("versionZ");
+ ServiceEntitlementRequest request = requestBuilder.build();
+
+ XmlDoc entitlementXmlDoc = null;
+ try {
+ entitlementXmlDoc =
+ new XmlDoc(
+ serviceEntitlement.queryEntitlementStatus(
+ ServiceEntitlement.APP_VOWIFI,
+ request));
+ // While finishing the initial AuthN, save the token
+ // and to be used next time for fast AuthN.
+ mCachedAccessToken = entitlementXmlDoc.get(
+ ResponseXmlNode.TOKEN, ResponseXmlAttributes.TOKEN);
+ } catch (ServiceEntitlementException e) {
+ Log.e(TAG, "queryEntitlementStatus failed", e);
+ }
+ return entitlementXmlDoc == null ? null : toEntitlementResult(entitlementXmlDoc);
+ }
+
+ private static EntitlementResult toEntitlementResult(XmlDoc doc) {
+ return EntitlementResult.builder()
+ .setSuccess(true)
+ .setVowifiStatus(Ts43VowifiStatus.builder(doc).build())
+ .setEmergencyAddressWebUrl(
+ doc.get(
+ ResponseXmlNode.APPLICATION,
+ ResponseXmlAttributes.SERVER_FLOW_URL))
+ .setEmergencyAddressWebData(
+ doc.get(
+ ResponseXmlNode.APPLICATION,
+ ResponseXmlAttributes.SERVER_FLOW_USER_DATA))
+ .build();
+ }
+
+ private CarrierConfig getCarrierConfig(Context context) {
+ CarrierConfigManager carrierConfigManager =
+ (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ PersistableBundle config = carrierConfigManager.getConfigForSubId(subId);
+ String aseUrl =
+ config.getString(CarrierConfigManager.ImsServiceEntitlement.KEY_AES_URL_STRING);
+ return CarrierConfig.builder().setServerUrl(aseUrl).build();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/WfcActivationController.java b/src/com/android/ImsServiceEntitlement/WfcActivationController.java
new file mode 100644
index 0000000..a4dc29b
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/WfcActivationController.java
@@ -0,0 +1,389 @@
+/*
+ * 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__INCOMPATIBLE;
+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__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 com.android.imsserviceentitlement.entitlement.EntitlementResult;
+import com.android.imsserviceentitlement.entitlement.VowifiStatus;
+import com.android.imsserviceentitlement.utils.ImsUtils;
+import com.android.imsserviceentitlement.utils.TelephonyUtils;
+
+import java.time.Duration;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+/**
+ * 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 = "WfcActivationActivity";
+
+ // 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();
+ private static final long ENTITLEMENT_STATUS_UPDATE_RETRY_INTERVAL_MS_ATT =
+ Duration.ofMinutes(30).toMillis();
+
+ // Dependencies
+ private final Context context;
+ private final WfcActivationUi activationUi;
+ private final TelephonyUtils telephonyUtils;
+ private final WfcActivationApi activationApi;
+ private final ImsUtils imsUtils;
+ private final Intent startIntent;
+
+ // States
+ private int evaluateTimes = 0;
+
+ // States for metrics
+ private long startTime;
+ private long durationMillis;
+ private int purpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UNKNOWN_PURPOSE;
+ private int appResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT;
+
+ @MainThread
+ public WfcActivationController(
+ Context context,
+ WfcActivationUi wfcActivationUi,
+ WfcActivationApi activationApi,
+ Intent intent) {
+ startIntent = intent;
+ this.context = context;
+ this.activationUi = wfcActivationUi;
+ this.activationApi = activationApi;
+ telephonyUtils = new TelephonyUtils(context, getSubId());
+ imsUtils = ImsUtils.getInstance(context, getSubId());
+ }
+
+ /** Indicates the controller to start WFC activation or emergency address update flow. */
+ @MainThread
+ public void startFlow() {
+ showGeneralWaitingUi();
+ evaluateEntitlementStatus();
+ if (isActivationFlow()) {
+ purpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__ACTIVATION;
+ } else {
+ purpose = IMS_SERVICE_ENTITLEMENT_UPDATED__PURPOSE__UPDATE;
+ }
+ startTime = telephonyUtils.getUptimeMillis();
+ }
+
+ /** Evaluates entitlement status for activation or update. */
+ @MainThread
+ public void evaluateEntitlementStatus() {
+ if (!telephonyUtils.isNetworkConnected()) {
+ handleInitialEntitlementStatus(null);
+ return;
+ }
+ EntitlementUtils.entitlementCheck(
+ activationApi, 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(
+ activationApi, 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 (durationMillis == 0L) {
+ durationMillis = telephonyUtils.getUptimeMillis() - startTime;
+ }
+ // If no result set, it must be cancelled by user pressing back button.
+ if (appResult == IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__UNKNOWN_RESULT) {
+ appResult = IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__CANCELED;
+ }
+ ImsServiceEntitlementStatsLog.write(
+ IMS_SERVICE_ENTITLEMENT_UPDATED,
+ /* carrier_id= */ telephonyUtils.getCarrierId(),
+ /* actual_carrier_id= */ telephonyUtils.getSpecificCarrierId(),
+ purpose,
+ IMS_SERVICE_ENTITLEMENT_UPDATED__SERVICE_TYPE__VOWIFI,
+ appResult,
+ durationMillis);
+ }
+
+ /**
+ * Returns {@code true} if the app is launched for WFC activation; {@code false} for emergency
+ * address update.
+ */
+ private boolean isActivationFlow() {
+ return ActivityConstants.isActivationFlow(startIntent);
+ }
+
+ private int getSubId() {
+ return ActivityConstants.getSubId(startIntent);
+ }
+
+ /** Returns UI title string resource ID based on {@link #isActivationFlow()}. */
+ @StringRes
+ private int getUiTitle() {
+ int intention = ActivityConstants.getLaunchIntention(startIntent);
+ 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(startIntent);
+ 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) {
+ activationUi.showActivationUi(
+ getUiTitle(), errorMessage, false, R.string.ok, WfcActivationUi.RESULT_FAILURE, 0);
+ }
+
+ private void showGeneralErrorUi() {
+ showErrorUi(getGeneralErrorText());
+ }
+
+ private void showGeneralWaitingUi() {
+ activationUi.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) {
+ VowifiStatus vowifiStatus = result.getVowifiStatus();
+ if (vowifiStatus.vowifiEntitled()) {
+ activationApi.onWfcSettingChanged(true, result);
+ finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__SUCCESSFUL);
+ activationUi.setResultAndFinish(Activity.RESULT_OK);
+ } else {
+ if (vowifiStatus.serverDataMissing()) {
+ if (!TextUtils.isEmpty(result.getTermsAndConditionsWebUrl())) {
+ activationUi.showWebview(
+ result.getTermsAndConditionsWebUrl(),
+ /* postData= */ null,
+ activationApi.getWebviewJsControllerName());
+ } else {
+ activationUi.showWebview(
+ result.getEmergencyAddressWebUrl(),
+ result.getEmergencyAddressWebData(),
+ activationApi.getWebviewJsControllerName());
+ }
+ } 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) {
+ VowifiStatus vowifiStatus = result.getVowifiStatus();
+ if (vowifiStatus.vowifiEntitled()) {
+ int launchIntention = ActivityConstants.getLaunchIntention(startIntent);
+ if (launchIntention == ActivityConstants.LAUNCH_APP_SHOW_TC) {
+ activationUi.showWebview(
+ result.getTermsAndConditionsWebUrl(),
+ /* postData= */ null,
+ activationApi.getWebviewJsControllerName());
+ } else {
+ activationUi.showWebview(
+ result.getEmergencyAddressWebUrl(),
+ result.getEmergencyAddressWebData(),
+ activationApi.getWebviewJsControllerName());
+ }
+ } else {
+ if (vowifiStatus.incompatible()) {
+ showErrorUi(R.string.failure_contact_carrier);
+ turnOffWfc(() -> {
+ activationApi.onWfcSettingChanged(false, result);
+ 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) {
+ VowifiStatus vowifiStatus = result.getVowifiStatus();
+ if (vowifiStatus.vowifiEntitled()) {
+ activationApi.onWfcSettingChanged(true, result);
+ activationUi.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 (evaluateTimes < ENTITLEMENT_STATUS_UPDATE_RETRY_MAX) {
+ evaluateTimes += 1;
+ postDelay(
+ getEntitlementStatusUpdateRetryIntervalMs(),
+ this::reevaluateEntitlementStatus);
+ } else {
+ evaluateTimes = 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) {
+ VowifiStatus vowifiStatus = result.getVowifiStatus();
+ if (vowifiStatus.vowifiEntitled()) {
+ activationUi.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(() -> {
+ activationApi.onWfcSettingChanged(false, result);
+ finishStatsLog(IMS_SERVICE_ENTITLEMENT_UPDATED__APP_RESULT__DISABLED);
+ activationUi.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(imsUtils, action);
+ }
+
+ private void finishStatsLog(int result) {
+ appResult = result;
+ durationMillis = telephonyUtils.getUptimeMillis() - startTime;
+ }
+}
diff --git a/src/com/android/ImsServiceEntitlement/WfcActivationUi.java b/src/com/android/ImsServiceEntitlement/WfcActivationUi.java
new file mode 100644
index 0000000..d8a9e2c
--- /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, String jsControllerName);
+
+ /**
+ * 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();
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/WfcWebPortalFragment.java b/src/com/android/ImsServiceEntitlement/WfcWebPortalFragment.java
new file mode 100644
index 0000000..ebe05bf
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/WfcWebPortalFragment.java
@@ -0,0 +1,169 @@
+/*
+ * 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.support.v4.app.Fragment;
+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;
+
+/** A fragment of WebView to render WFC T&C and emergency address web portal */
+public class WfcWebPortalFragment extends Fragment {
+ private static final String TAG = "WfcActivationActivity";
+
+ private static final String KEY_URL_STRING = "url";
+ private static final String KEY_POST_DATA_STRING = "post_data";
+ private static final String KEY_JS_CALLBACK_OBJECT_STRING = "js_callback_object";
+
+ private static final String URL_WITH_PDF_FILE_EXTENSION = ".pdf";
+
+ private WebView webView;
+ private boolean finishFlow = false;
+
+ /** Public static constructor */
+ public static WfcWebPortalFragment newInstance(
+ String url, String postData, String jsControllerName) {
+ WfcWebPortalFragment frag = new WfcWebPortalFragment();
+
+ Bundle args = new Bundle();
+ args.putString(KEY_URL_STRING, url);
+ args.putString(KEY_POST_DATA_STRING, postData);
+ args.putString(KEY_JS_CALLBACK_OBJECT_STRING, jsControllerName);
+ 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, "");
+ String jsCallbackObject = arguments.getString(KEY_JS_CALLBACK_OBJECT_STRING, "");
+
+ ProgressBar spinner = v.findViewById(R.id.loadingbar);
+ webView = v.findViewById(R.id.webview);
+ webView.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);
+ }
+ });
+ webView.addOnAttachStateChangeListener(
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ Log.d(TAG, "#onViewDetachedFromWindow");
+ if (!finishFlow) {
+ ((WfcActivationUi) getActivity()).setResultAndFinish(
+ Activity.RESULT_CANCELED);
+ }
+ }
+ });
+ webView.addJavascriptInterface(new JsInterface(getActivity()), jsCallbackObject);
+ WebSettings settings = webView.getSettings();
+ settings.setDomStorageEnabled(true);
+ settings.setJavaScriptEnabled(true);
+
+ if (TextUtils.isEmpty(postData)) {
+ webView.loadUrl(url);
+ } else {
+ webView.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 (webView != null
+ && webView.canGoBack()
+ && webView.getUrl().toLowerCase().endsWith(URL_WITH_PDF_FILE_EXTENSION)) {
+ webView.goBack();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Emergency address websheet javascript callback. */
+ private class JsInterface {
+ private final WfcActivationUi ui;
+
+ JsInterface(Activity activity) {
+ ui = (WfcActivationUi) activity;
+ }
+
+ /**
+ * 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");
+ finishFlow = true;
+ ui.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");
+ ui.setResultAndFinish(Activity.RESULT_CANCELED);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/debug/DebugUtils.java b/src/com/android/ImsServiceEntitlement/debug/DebugUtils.java
new file mode 100644
index 0000000..fd1187b
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/debug/DebugUtils.java
@@ -0,0 +1,68 @@
+/*
+ * 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 TAG = "WfcActivationActivity";
+
+ private static final String PROP_PII_LOGGABLE = "dbg.wfc.pii_loggable";
+ private static final String PROP_SERVER_URL_OVERRIDE = "persist.dbg.wfc.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);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/entitlement/EntitlementResult.java b/src/com/android/ImsServiceEntitlement/entitlement/EntitlementResult.java
new file mode 100644
index 0000000..48fc871
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/entitlement/EntitlementResult.java
@@ -0,0 +1,109 @@
+/*
+ * 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.google.auto.value.AutoValue;
+
+/** The result of the entitlement status check. */
+@AutoValue
+public abstract class EntitlementResult {
+ private static final VowifiStatus INACTIVE_VOWIFI_STATUS =
+ new VowifiStatus() {
+ @Override
+ public boolean vowifiEntitled() {
+ return false;
+ }
+
+ @Override
+ public boolean serverDataMissing() {
+ return false;
+ }
+
+ @Override
+ public boolean inProgress() {
+ return true;
+ }
+
+ @Override
+ public boolean incompatible() {
+ return false;
+ }
+ };
+
+ public static Builder builder() {
+ return new AutoValue_EntitlementResult.Builder()
+ .setSuccess(false)
+ .setVowifiStatus(INACTIVE_VOWIFI_STATUS)
+ .setPollInterval(0)
+ .setEmergencyAddressWebUrl("")
+ .setEmergencyAddressWebData("")
+ .setTermsAndConditionsWebUrl("");
+ }
+
+ /** Indicates this entitlement query succeeded or failed. */
+ public abstract boolean isSuccess();
+ /** The entitlement and service status of Vowifi. */
+ public abstract VowifiStatus getVowifiStatus();
+ /** The interval for scheduling polling job. */
+ public abstract int getPollInterval();
+ /** 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();
+
+ /** Builder of {@link EntitlementResult}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract EntitlementResult build();
+ public abstract Builder setSuccess(boolean success);
+ public abstract Builder setVowifiStatus(VowifiStatus vowifiStatus);
+ public abstract Builder setPollInterval(int pollInterval);
+ public abstract Builder setEmergencyAddressWebUrl(String emergencyAddressWebUrl);
+ public abstract Builder setEmergencyAddressWebData(String emergencyAddressWebData);
+ public abstract Builder setTermsAndConditionsWebUrl(String termsAndConditionsWebUrl);
+ }
+
+ /** Returns failure EntitlementResult. */
+ public static EntitlementResult getFailureResult() {
+ return builder()
+ .setSuccess(false)
+ .setVowifiStatus(INACTIVE_VOWIFI_STATUS)
+ .build();
+ }
+
+ @Override
+ public final String toString() {
+ StringBuilder builder = new StringBuilder("EntitlementResult{");
+ builder.append("isSuccess=").append(isSuccess());
+ builder.append(",getVowifiStatus=").append(getVowifiStatus());
+ builder.append(",getEmergencyAddressWebUrl=").append(opaque(getEmergencyAddressWebUrl()));
+ builder.append(",getEmergencyAddressWebData=").append(opaque(getEmergencyAddressWebData()));
+ builder.append(",getPollInterval=").append(getPollInterval());
+ builder.append(",getTermsAndConditionsWebUrl=").append(getTermsAndConditionsWebUrl());
+ builder.append("}");
+ return builder.toString();
+ }
+
+ private static String opaque(String string) {
+ if (string == null) {
+ return "null";
+ }
+ return "string_of_length_" + string.length();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/entitlement/VowifiStatus.java b/src/com/android/ImsServiceEntitlement/entitlement/VowifiStatus.java
new file mode 100644
index 0000000..a20e438
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/entitlement/VowifiStatus.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.entitlement;
+
+/** Interfaces of retrieving the Vowifi entitlement statues. */
+public interface VowifiStatus {
+ /** Returns {@code true} if vowifi service is available. */
+ boolean vowifiEntitled();
+
+ /** Returns {@code true} if the T&C or address needs to be updated. */
+ boolean serverDataMissing();
+
+ /** Returns {@code true} if the service is being provisioned. */
+ boolean inProgress();
+
+ /** Returns {@code true} if the service cannot be offered. */
+ boolean incompatible();
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/ts43/Ts43Constants.java b/src/com/android/ImsServiceEntitlement/ts43/Ts43Constants.java
new file mode 100644
index 0000000..4e34a08
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/ts43/Ts43Constants.java
@@ -0,0 +1,54 @@
+/*
+ * 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 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";
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/ts43/Ts43VowifiStatus.java b/src/com/android/ImsServiceEntitlement/ts43/Ts43VowifiStatus.java
new file mode 100644
index 0000000..856e2e4
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/ts43/Ts43VowifiStatus.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.ts43;
+
+import com.android.imsserviceentitlement.entitlement.VowifiStatus;
+import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlAttributes;
+import com.android.imsserviceentitlement.ts43.Ts43Constants.ResponseXmlNode;
+import com.android.imsserviceentitlement.utils.XmlDoc;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Implementation of WFC entitlement status and server data availability for TS.43 entitlement
+ * solution.
+ */
+@AutoValue
+public abstract class Ts43VowifiStatus implements VowifiStatus {
+ static class EntitlementStatus {
+ private EntitlementStatus() {}
+
+ static final int DISABLED = 0;
+ static final int ENABLED = 1;
+ static final int INCOMPATIBLE = 2;
+ static final int PROVISIONING = 3;
+ }
+
+ static class AddrStatus {
+ private AddrStatus() {}
+
+ static final int NOT_AVAILABLE = 0;
+ static final int AVAILABLE = 1;
+ static final int NOT_REQUIRED = 2;
+ static final int IN_PROGRESS = 3;
+ }
+
+ static class TcStatus {
+ private TcStatus() {}
+
+ static final int NOT_AVAILABLE = 0;
+ static final int AVAILABLE = 1;
+ static final int NOT_REQUIRED = 2;
+ static final int IN_PROGRESS = 3;
+ }
+
+ static class ProvStatus {
+ private ProvStatus() {}
+
+ static final int NOT_PROVISIONED = 0;
+ static final int PROVISIONED = 1;
+ static final int NOT_REQUIRED = 2;
+ 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(
+ Integer.parseInt(
+ doc.get(ResponseXmlNode.APPLICATION,
+ ResponseXmlAttributes.ENTITLEMENT_STATUS)))
+ .setTcStatus(
+ Integer.parseInt(
+ doc.get(ResponseXmlNode.APPLICATION,
+ ResponseXmlAttributes.TC_STATUS)))
+ .setAddrStatus(
+ Integer.parseInt(
+ doc.get(ResponseXmlNode.APPLICATION,
+ ResponseXmlAttributes.ADDR_STATUS)))
+ .setProvStatus(
+ Integer.parseInt(
+ doc.get(ResponseXmlNode.APPLICATION,
+ ResponseXmlAttributes.PROVISION_STATUS)));
+ }
+
+ /** 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);
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ public boolean serverDataMissing() {
+ return entitlementStatus() == EntitlementStatus.DISABLED
+ && (tcStatus() == TcStatus.NOT_AVAILABLE
+ || addrStatus() == AddrStatus.NOT_AVAILABLE);
+ }
+
+ @Override
+ 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));
+ }
+
+ @Override
+ 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/ImsUtils.java b/src/com/android/ImsServiceEntitlement/utils/ImsUtils.java
new file mode 100644
index 0000000..4537ba2
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/utils/ImsUtils.java
@@ -0,0 +1,131 @@
+/*
+ * 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.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 = "WfcActivationActivity";
+ private static final String PACKAGE_NAME = "com.google.android.wfcactivation";
+
+ private final CarrierConfigManager carrierConfigManager;
+ private final ImsMmTelManager imsMmTelManager;
+ private final int subId;
+
+ // Cache subscription id associated {@link ImsUtils} objects for reusing.
+ @GuardedBy("ImsUtils.class")
+ private static SparseArray<ImsUtils> instances = new SparseArray<ImsUtils>();
+
+ private ImsUtils(Context context, int subId) {
+ carrierConfigManager =
+ (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ imsMmTelManager = getImsMmTelManager(context, subId);
+ this.subId = subId;
+ }
+
+ /** Returns {@link ImsUtils} instance. */
+ public static synchronized ImsUtils getInstance(Context context, int subId) {
+ ImsUtils instance = instances.get(subId);
+ if (instance != null) {
+ return instance;
+ }
+
+ instance = new ImsUtils(context, subId);
+ instances.put(subId, instance);
+ return instance;
+ }
+
+ /** Change persistent WFC enabled setting. */
+ public void setWfcSetting(boolean enabled, boolean force) {
+ try {
+ if (force) {
+ imsMmTelManager.setVoWiFiSettingEnabled(enabled);
+ }
+ } 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 (carrierConfigManager != null) {
+ PersistableBundle b = carrierConfigManager.getConfigForSubId(subId);
+ if (b != null) {
+ imsMmTelManager.setVoWiFiModeSetting(
+ b.getInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT));
+ imsMmTelManager.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;
+ }
+
+ 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);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/utils/TelephonyUtils.java b/src/com/android/ImsServiceEntitlement/utils/TelephonyUtils.java
new file mode 100644
index 0000000..b9b178f
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/utils/TelephonyUtils.java
@@ -0,0 +1,137 @@
+/*
+ * 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.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/** This class implements Telephony helper methods. */
+public class TelephonyUtils {
+ public static final String TAG = TelephonyUtils.class.getSimpleName();
+
+ private final ConnectivityManager connectivityManager;
+ private final TelephonyManager telephonyManager;
+
+ public TelephonyUtils(Context context) {
+ this(context, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+ }
+
+ public TelephonyUtils(Context context, int subId) {
+ /* We can also use:
+ *
+ * telephonyManager = context.getSystemService(TelephonyManager.class);
+ *
+ * But Context#getSystemService(Class<T> serviceClass) is a final method, which cannot
+ * be stubbed in Mockito. Hence it's little more dificult to test.
+ */
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ telephonyManager =
+ ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE))
+ .createForSubscriptionId(subId);
+ } else {
+ telephonyManager = (TelephonyManager) context.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ }
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ /** 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 = connectivityManager.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 telephonyManager.getIccAuthentication(
+ TelephonyManager.APPTYPE_USIM, TelephonyManager.AUTHTYPE_EAP_AKA, data);
+ }
+
+ /** Returns carrier ID. */
+ public int getCarrierId() {
+ return telephonyManager.getSimCarrierId();
+ }
+
+ /** Returns fine-grained carrier ID. */
+ public int getSpecificCarrierId() {
+ return telephonyManager.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;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/ImsServiceEntitlement/utils/XmlDoc.java b/src/com/android/ImsServiceEntitlement/utils/XmlDoc.java
new file mode 100644
index 0000000..59c8663
--- /dev/null
+++ b/src/com/android/ImsServiceEntitlement/utils/XmlDoc.java
@@ -0,0 +1,140 @@
+/*
+ * 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.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 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 = "WfcActivationActivity.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>> nodesMap = new ArrayMap<>();
+
+ public XmlDoc(String responseBody) {
+ parseXmlResponse(responseBody);
+ }
+
+ /** Returns node value for given node and key, or {@code null} if not found. */
+ @Nullable
+ public String get(String node, String key) {
+ Map<String, String> paramsMap = nodesMap.get(node);
+ return paramsMap == null ? null : paramsMap.get(key);
+ }
+
+ /**
+ * 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)));
+
+ nodesMap.put(map.item(0).getNodeValue(), 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;
+ }
+} \ No newline at end of file