diff options
author | danielwbhuang <danielwbhuang@google.com> | 2021-01-08 15:04:15 +0800 |
---|---|---|
committer | Meng Wang <mewan@google.com> | 2021-01-15 17:11:03 -0800 |
commit | a1ecd874379f178a57946d01ccdfbaf88df6e5dc (patch) | |
tree | 559840eb5502cf186e8412ac8bc57bcdbfeaab9f | |
parent | 53875cf2b9ea5d638c81d0c2f7b9af09f70da1d6 (diff) | |
download | ImsServiceEntitlement-a1ecd874379f178a57946d01ccdfbaf88df6e5dc.tar.gz |
Implement ImsServiceEntitlement app
Implement GSMA TS.43 based IMS service entitlement app in Android.
Bug: 176127289
Test: build pass
Change-Id: Ia4ff4ea9b8284225f946f739c2adc48f3c9df106
29 files changed, 2488 insertions, 3 deletions
@@ -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("&", "&").replace("&amp;", "&"); + + try { + InputSource inputSource = new InputSource(new StringReader(responseBody)); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = builderFactory.newDocumentBuilder(); + Document doc = docBuilder.parse(inputSource); + doc.getDocumentElement().normalize(); + + if (DebugUtils.isPiiLoggable()) { + Log.d( + TAG, + "parseXmlResponseForNode() Root element: " + + doc.getDocumentElement().getNodeName()); + } + + NodeList nodeList = doc.getElementsByTagName(NODE_CHARACTERISTIC); + for (int i = 0; i < nodeList.getLength(); i++) { + NamedNodeMap map = nodeList.item(i).getAttributes(); + if (DebugUtils.isPiiLoggable()) { + Log.d( + TAG, + "parseAuthenticateResponse() node name=" + + nodeList.item(i).getNodeName() + + " node value=" + + map.item(0).getNodeValue()); + } + Map<String, String> paramsMap = new ArrayMap<>(); + Element element = (Element) nodeList.item(i); + paramsMap.putAll(parseParams(element.getElementsByTagName(NODE_PARM))); + + 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 |