Implement ImsServiceEntitlement app

Implement GSMA TS.43 based IMS service entitlement app in Android.

Bug: 176127289
Test: build pass
Change-Id: Ia4ff4ea9b8284225f946f739c2adc48f3c9df106
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