summaryrefslogtreecommitdiff
path: root/services/QualifiedNetworksService/src/com/android
diff options
context:
space:
mode:
authorSean.JS Tsai <seanjstsai@google.com>2022-12-06 08:27:34 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2022-12-06 08:27:34 +0000
commit365757bd0d701e8c8e64ca27f0a6ee882c252e71 (patch)
treed4b4be04b5a1d5a5912148a5a9afe5348f87e984 /services/QualifiedNetworksService/src/com/android
parent4c6995266b5f7bc738f3117792e486c6fbdb75f0 (diff)
parent5e440eb9a6a06ffb495a5a4f6acd11c344143ba3 (diff)
downloadTelephony-365757bd0d701e8c8e64ca27f0a6ee882c252e71.tar.gz
Merge "[WFC] move ePDG-based WFC activation from WFC APP to Google QNS"
Diffstat (limited to 'services/QualifiedNetworksService/src/com/android')
-rw-r--r--services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationActivity.java349
-rw-r--r--services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationHelper.java375
-rw-r--r--services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcCarrierConfigManager.java224
-rw-r--r--services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcUtils.java117
4 files changed, 1065 insertions, 0 deletions
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationActivity.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationActivity.java
new file mode 100644
index 0000000..b53fed0
--- /dev/null
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationActivity.java
@@ -0,0 +1,349 @@
+/*
+ * 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.telephony.qns.wfc;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.WindowManager;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsServiceConnection;
+import androidx.browser.customtabs.CustomTabsSession;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.telephony.qns.R;
+
+/** Main activity to handle VoWiFi activation */
+public class WfcActivationActivity extends FragmentActivity {
+
+ public static final String TAG = "QNS-WfcActivationActivity";
+
+ private static final String EXTRA_URL = "EXTRA_URL";
+
+ // Message IDs
+ private static final int MESSAGE_CHECK_WIFI = 1;
+ private static final int MESSAGE_CHECK_WIFI_DONE = 2;
+ private static final int MESSAGE_TRY_EPDG_CONNECTION = 3;
+ private static final int MESSAGE_TRY_EPDG_CONNECTION_DONE = 4;
+ private static final int MESSAGE_SHOW_WEB_PORTAL = 5;
+
+ private WfcActivationHelper mWfcActivationHelper;
+
+ private Handler mUiHandler;
+ @VisibleForTesting ProgressDialog mProgressDialog;
+
+ private CustomTabsSession mCustomTabsSession;
+ @VisibleForTesting CustomTabsServiceConnection mServiceConnection;
+ private ActivityResultLauncher<Intent> mWebviewResultsLauncher =
+ registerForActivityResult(
+ new StartActivityForResult(),
+ activityResult -> {
+ if (activityResult.getResultCode() == Activity.RESULT_CANCELED) {
+ Log.d(TAG, "Webview Activity Result CANCEL");
+ finish();
+ } else {
+ Log.d(TAG, "Webview Activity Result OK");
+ finish();
+ }
+ });
+
+ // Whether it's safe now to update UI, based on activity visibility.
+ // It should be true between onResume() and onPause().
+ private boolean mSafeToUpdateUi = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ // Initialization
+ super.onCreate(savedInstanceState);
+ createDependencies();
+ createUiHandler();
+
+ // Set layout
+ setContentView(R.layout.activity_wfc_activation);
+
+ if (WfcUtils.isActivationFlow(getIntent())) {
+ // WFC activation flow
+ mUiHandler.sendEmptyMessage(MESSAGE_CHECK_WIFI);
+ } else {
+ // Emergency address update flow
+ mUiHandler.sendEmptyMessage(MESSAGE_SHOW_WEB_PORTAL);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mSafeToUpdateUi = true;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mSafeToUpdateUi = false;
+ }
+
+ private void createUiHandler() {
+ Handler.Callback handlerCallback =
+ (Message msg) -> {
+ Log.d(TAG, "UiHandler received: " + msg);
+ switch (msg.what) {
+ case MESSAGE_CHECK_WIFI:
+ mWfcActivationHelper.checkWiFi(
+ mUiHandler.obtainMessage(MESSAGE_CHECK_WIFI_DONE));
+ break;
+ case MESSAGE_CHECK_WIFI_DONE:
+ if (msg.arg1 == WfcActivationHelper.WIFI_CONNECTION_SUCCESS) {
+ mUiHandler.sendEmptyMessage(MESSAGE_TRY_EPDG_CONNECTION);
+ } else { // msg.arg1 == WfcActivationHelper.WIFI_CONNECTION_ERROR
+ showWiFiUnavailableDialog();
+ }
+ break;
+ case MESSAGE_TRY_EPDG_CONNECTION:
+ showProgressDialog();
+ Log.d(TAG, "Show progress dialog - tryEpdgConnectionOverWiFi");
+ mWfcActivationHelper.tryEpdgConnectionOverWiFi(
+ mUiHandler.obtainMessage(MESSAGE_TRY_EPDG_CONNECTION_DONE),
+ mWfcActivationHelper
+ .getVowifiRegistrationTimerForVowifiActivation());
+ break;
+ case MESSAGE_TRY_EPDG_CONNECTION_DONE:
+ dismissProgressDialog();
+ Log.d(TAG, "Dismiss progress dialog - tryEpdgConnectionOverWiFi");
+ if (msg.arg1 == WfcActivationHelper.EPDG_CONNECTION_SUCCESS) {
+ Log.d(TAG, "VoWiFi activated");
+ setResultAndFinish(RESULT_OK);
+ } else { // msg.arg1 == WfcActivationHelper.EPDG_CONNECTION_ERROR
+ mUiHandler.sendEmptyMessage(MESSAGE_SHOW_WEB_PORTAL);
+ }
+ break;
+ case MESSAGE_SHOW_WEB_PORTAL:
+ startWebPortal();
+ break;
+ default:
+ Log.e(TAG, "UiHandler received unknown message: " + msg);
+ return false;
+ }
+ return true;
+ };
+ mUiHandler = new Handler(handlerCallback);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mServiceConnection != null) {
+ unbindService(mServiceConnection);
+ }
+ super.onDestroy();
+ }
+
+ private void startWebPortal() {
+ Log.d(TAG, "starting web portal ..");
+ if (!mSafeToUpdateUi) {
+ Log.d(TAG, "Not safe to update UI. Stopping.");
+ return;
+ }
+ String url = mWfcActivationHelper.getWebPortalUrl();
+ if (TextUtils.isEmpty(url)) {
+ Log.d(TAG, "No web portal url!");
+ return;
+ }
+ if (!mWfcActivationHelper.supportJsCallbackForVowifiPortal()) {
+ // For carriers not requiring JS callback in their WFC activation webpage, using a
+ // ChromeCustomTab provides richer web functionality while avoiding jumping to the browser
+ // app and introducing a discontinuity in UX.
+ startCustomTab(url);
+ } else {
+ // Because QNS uses system UID now, webview cannot be started here. Instead, webview is
+ // started in a different activity, {@code R.string.webview_component}.
+ startWebPortalActivity();
+ }
+ }
+
+ private void startCustomTab(String url) {
+ mServiceConnection =
+ new CustomTabsServiceConnection() {
+ @Override
+ public void onCustomTabsServiceConnected(
+ ComponentName name, CustomTabsClient client) {
+ client.warmup(0L);
+ mCustomTabsSession = client.newSession(null);
+ mCustomTabsSession.mayLaunchUrl(Uri.parse(url), null, null);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mCustomTabsSession = null;
+ }
+ };
+
+ String ServicePackageName =
+ getResources().getString(R.string.custom_tabs_service_package_name);
+ CustomTabsClient.bindCustomTabsService(this, ServicePackageName, mServiceConnection);
+ new CustomTabsIntent.Builder(mCustomTabsSession).build().launchUrl(this, Uri.parse(url));
+
+ if (WfcUtils.isActivationFlow(getIntent())) {
+ setResultAndFinish(RESULT_CANCELED);
+ } else {
+ setResultAndFinish(RESULT_OK);
+ }
+ }
+ private void startWebPortalActivity() {
+ String webviewComponent = getResources().getString(R.string.webview_component);
+ ComponentName componentName = ComponentName.unflattenFromString(webviewComponent);
+ String url = mWfcActivationHelper.getWebPortalUrl();
+
+ Log.d(TAG, "startWebPortalActivity componentName: " + componentName);
+ Intent intent = new Intent();
+ intent.setComponent(componentName);
+ intent.putExtra(EXTRA_URL, url);
+ mWebviewResultsLauncher.launch(intent);
+ }
+
+ private void showProgressDialog() {
+ if (!mSafeToUpdateUi) {
+ return;
+ }
+ if (mProgressDialog != null && mProgressDialog.isShowing()) {
+ return;
+ }
+ mProgressDialog =
+ new ProgressDialog(
+ new ContextThemeWrapper(
+ this, android.R.style.Theme_DeviceDefault_Light_Dialog));
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setCanceledOnTouchOutside(false);
+ mProgressDialog.setMessage(getText(R.string.progress_text));
+ mProgressDialog.show();
+ // Keep screen on
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ private void dismissProgressDialog() {
+ if (!mSafeToUpdateUi) {
+ return;
+ }
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ // Allow screen off
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ private void showWiFiUnavailableDialog() {
+ if (!mSafeToUpdateUi) {
+ return;
+ }
+ DialogFragment dialog =
+ AlertDialogFragment.newInstance(
+ R.string.connect_to_wifi_or_web_portal_title,
+ R.string.connect_to_wifi_or_web_portal_message);
+ dialog.show(getSupportFragmentManager(), "Wifi_unavailable_dialog");
+ }
+
+ /** Dialog fragment to show error messages */
+ public static class AlertDialogFragment extends DialogFragment {
+
+ private static final String TITLE_KEY = "TITLE_KEY";
+ private static final String MESSAGE_KEY = "MESSAGE_KEY";
+
+ /** Static constructor */
+ public static AlertDialogFragment newInstance(int titleId, int messageId) {
+ AlertDialogFragment frag = new AlertDialogFragment();
+ frag.setCancelable(false);
+
+ Bundle args = new Bundle();
+ args.putInt(TITLE_KEY, titleId);
+ args.putInt(MESSAGE_KEY, messageId);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ int titleId = args.getInt(TITLE_KEY);
+ int messageId = args.getInt(MESSAGE_KEY);
+ final WfcActivationActivity activity =
+ (WfcActivationActivity) getActivity();
+ return new AlertDialog.Builder(
+ new ContextThemeWrapper(
+ getActivity(),
+ android.R.style.Theme_DeviceDefault_Light_Dialog))
+ .setTitle(titleId)
+ .setMessage(messageId)
+ .setPositiveButton(
+ R.string.button_setup_web_portal,
+ (OnClickListener)
+ (dialog, which) ->
+ activity.mUiHandler.sendEmptyMessage(
+ MESSAGE_SHOW_WEB_PORTAL))
+ .setNegativeButton(
+ R.string.button_turn_on_wifi,
+ (OnClickListener)
+ (dialog, which) -> {
+ // Redirect to WiFi settings UI
+ Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS);
+ activity.startActivity(intent);
+ // And finish self
+ activity.setResultAndFinish(RESULT_CANCELED);
+ })
+ .create();
+ }
+ }
+
+ private void setResultAndFinish(int resultCode) {
+ setResult(resultCode);
+ finish();
+ }
+
+ private void createDependencies() {
+ // Default initialization for production
+ int subId = WfcUtils.getSubId(getIntent());
+
+ if (WfcUtils.getWfcActivationHelper() != null) {
+ mWfcActivationHelper = WfcUtils.getWfcActivationHelper();
+ Log.v(TAG, "WfcActivationHelper injected: " + mWfcActivationHelper);
+ } else {
+ mWfcActivationHelper = new WfcActivationHelper(this, subId);
+ }
+
+ if (WfcUtils.getWebviewResultLauncher() != null) {
+ mWebviewResultsLauncher = WfcUtils.getWebviewResultLauncher();
+ Log.v(TAG, "getWebviewResultLauncher injected: " + mWebviewResultsLauncher);
+ }
+ }
+}
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationHelper.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationHelper.java
new file mode 100644
index 0000000..eeb403f
--- /dev/null
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcActivationHelper.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2022 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.telephony.qns.wfc;
+
+import static android.os.AsyncTask.THREAD_POOL_EXECUTOR;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.telephony.qns.R;
+
+import java.util.concurrent.Executor;
+
+/** A class with helper methods for WfcActivationCanadaActivity */
+public class WfcActivationHelper {
+ private static final String TAG = WfcActivationActivity.TAG;
+
+ @VisibleForTesting static final int PRE_EPDG_CONNECTION_DELAY_MS = 1000; // 1 second
+
+ // Enums for Wi-Fi check result
+ public static final int WIFI_CONNECTION_SUCCESS = 0;
+ public static final int WIFI_CONNECTION_ERROR = 1;
+
+ // Enums for ePDG connection result
+ public static final int EPDG_CONNECTION_SUCCESS = 0;
+ public static final int EPDG_CONNECTION_ERROR = 1;
+
+ // Event IDs for ePDG connection
+ @VisibleForTesting static final int EVENT_PRE_START_ATTEMPT = 0;
+ @VisibleForTesting static final int EVENT_START_ATTEMPT = 1;
+ @VisibleForTesting static final int EVENT_FINISH_ATTEMPT = 2;
+ private static final int EVENT_RESULT_SUCCESS = 3;
+ private static final int EVENT_TIMEOUT = 4;
+ private static final int EVENT_RESULT_FAILURE_IKEV2 = 5;
+ private static final int EVENT_RESULT_FAILURE_OTHER = 6;
+
+ public static final String ACTION_TRY_WFC_CONNECTION =
+ "com.android.qns.wfcactivation.TRY_WFC_CONNECTION";
+ public static final String EXTRA_SUB_ID = "SUB_ID";
+ public static final String EXTRA_TRY_STATUS = "TRY_STATUS";
+ public static final int STATUS_START = 1;
+ public static final int STATUS_END = 2;
+
+ // Dependencies
+ private final Context mContext;
+ private final ConnectivityManager mConnectivityManager;
+ private final ImsMmTelManager mImsMmTelManager;
+ private final WfcCarrierConfigManager mWfcConfigManager;
+
+ private final int mSubId;
+ private final Executor mBackgroundExecutor;
+
+ public WfcActivationHelper(Context context, int subId) {
+ this(
+ context,
+ subId,
+ context.getSystemService(ConnectivityManager.class),
+ WfcUtils.getImsMmTelManager(subId),
+ new WfcCarrierConfigManager(context.getApplicationContext(), subId),
+ THREAD_POOL_EXECUTOR);
+ }
+
+ @VisibleForTesting
+ WfcActivationHelper(
+ Context context,
+ int subId,
+ ConnectivityManager cm,
+ @Nullable ImsMmTelManager imsMmTelManager,
+ WfcCarrierConfigManager wfcConfigManager,
+ Executor backgroundExecutor) {
+ mContext = context;
+ mSubId = subId;
+ mConnectivityManager = cm;
+ mImsMmTelManager = imsMmTelManager;
+ mWfcConfigManager = wfcConfigManager;
+ mBackgroundExecutor = backgroundExecutor;
+ mWfcConfigManager.loadConfigurations();
+ }
+
+ /**
+ * Check WiFi connection
+ *
+ * @param msg The Message to be send with arg1 = result. Result is one of WIFI_CONNECTION_*.
+ */
+ public void checkWiFi(Message msg) {
+ msg.arg1 = checkWiFiAvailability() ? WIFI_CONNECTION_SUCCESS : WIFI_CONNECTION_ERROR;
+ msg.sendToTarget();
+ }
+
+ private boolean checkWiFiAvailability() {
+ NetworkInfo activeNetwork = mConnectivityManager.getActiveNetworkInfo();
+ return activeNetwork != null
+ && activeNetwork.isConnected()
+ && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
+ }
+
+ private void notifyQnsServiceToSetWfcMode(int status) {
+ String qnsPackage = mContext.getResources().getString(R.string.qns_package);
+ Intent intent = new Intent(ACTION_TRY_WFC_CONNECTION);
+ intent.putExtra(EXTRA_SUB_ID, mSubId);
+ intent.putExtra(EXTRA_TRY_STATUS, status);
+ intent.setPackage(qnsPackage);
+ Log.d(TAG, "notify QNS: subId =" + mSubId + ", status =" + status);
+ mContext.sendBroadcast(intent);
+ }
+
+ // This class is a effectively a one-way state machine that cannot be reset & reused. Each call
+ // of tryEpdgConnectionOverWiFi() creates a new instance of this class.
+ private class EpdgConnectHandler extends Handler {
+ final ImsCallback imsCallback;
+ final Message result;
+ boolean imsCallbackRegistered;
+ boolean waitingForResult; // ImsCallback wil be no-op when this is false
+
+ EpdgConnectHandler(Looper looper, Message result) {
+ super(looper);
+ imsCallback = new ImsCallback(this);
+ this.result = result;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_PRE_START_ATTEMPT:
+ // The callback must be registered before triggering ePDG connection, because
+ // the very 1st firing of the callback after registering MAY be the last IMS
+ // state.
+ // We assume 1 second is enough for that 1st firing.
+ // This means adding 1s delay to WFC activation flow in all cases, and it should
+ // be fine, given this can only be triggered by user manually and is not
+ // expected to be fast.
+ waitingForResult = false;
+ registerImsRegistrationCallback();
+ // Populate arg1 to EVENT_START_ATTEMPT message
+ sendMessageDelayed(
+ obtainMessage(EVENT_START_ATTEMPT, msg.arg1, 0),
+ /* delayMillis= */ msg.arg2);
+ break;
+
+ case EVENT_START_ATTEMPT:
+ Log.d(TAG, "Try to setup ePDG connection over WiFi");
+ waitingForResult = true;
+
+ mBackgroundExecutor.execute(
+ () -> {
+ // WFC: on; WFC preference: WiFi preferred (2)
+ mImsMmTelManager.setVoWiFiNonPersistent(true, 2);
+ // notify IMS to program WFC on and WFC mode as Wi-Fi Preferred
+ notifyQnsServiceToSetWfcMode(STATUS_START);
+ });
+
+ // Timeout event
+ Log.d(TAG, "Will timeout after " + msg.arg1 + " ms");
+ sendEmptyMessageDelayed(EVENT_TIMEOUT, /* delayMillis= */ msg.arg1);
+ break;
+
+ case EVENT_TIMEOUT:
+ Log.d(TAG, "Timeout: IKEV2 Auth failure not received.");
+ if (getTimeoutResult() == EPDG_CONNECTION_SUCCESS) {
+ sendEmptyMessage(EVENT_RESULT_SUCCESS);
+ } else {
+ sendEmptyMessage(EVENT_RESULT_FAILURE_IKEV2);
+ }
+ break;
+
+ case EVENT_RESULT_SUCCESS:
+ result.arg1 = EPDG_CONNECTION_SUCCESS;
+ // Clean up and send result
+ sendEmptyMessage(EVENT_FINISH_ATTEMPT);
+ break;
+
+ case EVENT_RESULT_FAILURE_IKEV2:
+ Log.d(TAG, "Turn off WFC");
+ // WFC: off; WFC preference: cellular preferred (1)
+ mBackgroundExecutor.execute(
+ () -> mImsMmTelManager.setVoWiFiNonPersistent(false, 1));
+ // Set result: failure
+ result.arg1 = EPDG_CONNECTION_ERROR;
+ // Clean up and send result
+ sendEmptyMessage(EVENT_FINISH_ATTEMPT);
+ break;
+
+ case EVENT_FINISH_ATTEMPT:
+ waitingForResult = false;
+ // Remove timeout event - if we get here via EVENT_TIMEOUT, this do nothing.
+ removeMessages(EVENT_TIMEOUT);
+ // Unregister mImsCallback
+ unregisterImsRegistrationCallback();
+ mBackgroundExecutor.execute(
+ () -> {
+ // Turn on WFC if success. W/o this, WFC could be turned
+ // ON (by STATUS_START) - OFF (by STATUS_END) - ON (by Settings app)
+ // which causes unnecessary IMS registration traffic.
+ // This must be done before sending STATUS_END so vendor IMS will
+ // see DB value ON.
+ if (result.arg1 == EPDG_CONNECTION_SUCCESS) {
+ Log.d(TAG, "Turn on WFC");
+ mImsMmTelManager.setVoWiFiSettingEnabled(true);
+ }
+ // Notify IMS to revert WFC on/off and mode to follow user settings.
+ // Notify here to make sure all cases (success, failure, timeout)
+ // reach this line.
+ notifyQnsServiceToSetWfcMode(STATUS_END);
+ // Send result
+ result.sendToTarget();
+ });
+ break;
+
+ case EVENT_RESULT_FAILURE_OTHER:
+ break;
+ default: // Do nothing
+ }
+ }
+
+ private void registerImsRegistrationCallback() {
+ try {
+ Log.d(TAG, "registerImsRegistrationCallback");
+ mImsMmTelManager.registerImsRegistrationCallback(this::post, imsCallback);
+ imsCallbackRegistered = true;
+ } catch (ImsException | RuntimeException e) {
+ Log.e(TAG, "registerImsRegistrationCallback failed", e);
+ // Fail silently to trigger timeout
+ imsCallbackRegistered = false;
+ }
+ }
+
+ private void unregisterImsRegistrationCallback() {
+ if (!imsCallbackRegistered) {
+ return;
+ }
+
+ try {
+ Log.d(TAG, "unregisterImsRegistrationCallback");
+ mImsMmTelManager.unregisterImsRegistrationCallback(imsCallback);
+ imsCallbackRegistered = false;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "unregisterImsRegistrationCallback failed", e);
+ }
+ }
+ }
+
+ /**
+ * Try to setup ePDG connection over WiFi.
+ *
+ * @param msg The Message to be send with arg1 = result. Result is one of EPDG_CONNECTION_*.
+ * @param timeoutMs Timeout, in milliseconds, then abort waiting for ePDG connection result.
+ */
+ public void tryEpdgConnectionOverWiFi(Message msg, int timeoutMs) {
+ if (mImsMmTelManager == null) {
+ // Send message with EPDG_CONNECTION_ERROR immediately.
+ Log.e(TAG, "ImsMmTelManager is null");
+ msg.arg1 = EPDG_CONNECTION_ERROR;
+ msg.sendToTarget();
+ return;
+ }
+
+ // NOTE: This private handler is hosted on the same looper as msg.
+ EpdgConnectHandler handler = new EpdgConnectHandler(msg.getTarget().getLooper(), msg);
+ // Start attempt of ePDG connection.
+ handler.obtainMessage(EVENT_PRE_START_ATTEMPT, timeoutMs, PRE_EPDG_CONNECTION_DELAY_MS)
+ .sendToTarget();
+ }
+
+ @VisibleForTesting
+ static class ImsCallback extends ImsMmTelManager.RegistrationCallback {
+ private final EpdgConnectHandler handler;
+
+ ImsCallback(EpdgConnectHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void onRegistered(int imsTransportType) {
+ if (!handler.waitingForResult) {
+ return;
+ }
+ if (imsTransportType != AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
+ return;
+ }
+ Log.d(TAG, "IMS connected on WLAN.");
+ handler.sendEmptyMessage(EVENT_RESULT_SUCCESS);
+ }
+
+ @Override
+ public void onUnregistered(ImsReasonInfo imsReasonInfo) {
+ if (!handler.waitingForResult) {
+ return;
+ }
+ Log.d(TAG, "IMS disconnected: " + imsReasonInfo);
+ if (isIkev2AuthFailure(imsReasonInfo)) {
+ handler.sendEmptyMessage(EVENT_RESULT_FAILURE_IKEV2);
+ } else {
+ handler.obtainMessage(
+ EVENT_RESULT_FAILURE_OTHER,
+ imsReasonInfo.getCode(),
+ imsReasonInfo.getExtraCode())
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void onTechnologyChangeFailed(int imsTransportType, ImsReasonInfo imsReasonInfo) {
+ if (!handler.waitingForResult) {
+ return;
+ }
+ if (imsTransportType != AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
+ return;
+ }
+ Log.d(TAG, "IMS registration failed on WLAN: " + imsReasonInfo);
+ if (isIkev2AuthFailure(imsReasonInfo)) {
+ handler.sendEmptyMessage(EVENT_RESULT_FAILURE_IKEV2);
+ } else {
+ handler.obtainMessage(
+ EVENT_RESULT_FAILURE_OTHER,
+ imsReasonInfo.getCode(),
+ imsReasonInfo.getExtraCode())
+ .sendToTarget();
+ }
+ }
+ }
+
+ static boolean isIkev2AuthFailure(ImsReasonInfo imsReasonInfo) {
+ if (imsReasonInfo.getCode() == ImsReasonInfo.CODE_EPDG_TUNNEL_ESTABLISH_FAILURE) {
+ if (imsReasonInfo.getExtraCode() == ImsReasonInfo.CODE_IKEV2_AUTH_FAILURE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int getTimeoutResult() {
+ return mWfcConfigManager.isShowVowifiPortalAfterTimeout()
+ ? EPDG_CONNECTION_ERROR
+ : EPDG_CONNECTION_SUCCESS;
+ }
+
+ public String getWebPortalUrl() {
+ return mWfcConfigManager.getVowifiEntitlementServerUrl();
+ }
+
+ public int getVowifiRegistrationTimerForVowifiActivation() {
+ return mWfcConfigManager.getVowifiRegistrationTimerForVowifiActivation();
+ }
+
+ public boolean supportJsCallbackForVowifiPortal() {
+ return mWfcConfigManager.supportJsCallbackForVowifiPortal();
+ }
+}
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcCarrierConfigManager.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcCarrierConfigManager.java
new file mode 100644
index 0000000..c6555b1
--- /dev/null
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcCarrierConfigManager.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telephony.qns.wfc;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.content.Context;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/** This class supports loading WFC config */
+public class WfcCarrierConfigManager {
+ private static final String TAG = WfcActivationActivity.TAG;
+ private final int mSubId;
+ private final Context mContext;
+ protected int mCurrCarrierId;
+
+ private boolean mIsShowVowifiPortalAfterTimeout;
+ private boolean mIsJsCallbackForVowifiPortal;
+
+ private int mVowifiRegistrationTimerForVowifiActivation;
+
+ private String mVowifiEntitlementServerUrl;
+
+ /**
+ * The address of the VoWiFi entitlement server for Emergency Address Registration.
+ *
+ * <p>Note: this is effective only if the {@link #KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING}
+ * is set to the QNS app.
+ */
+ public static final String KEY_QNS_VOWIFI_ENTITLEMENT_SERVER_URL_STRING =
+ "qns.vowifi_entitlement_server_url_string";
+
+ /**
+ * Specifies the wait time in milliseconds that VoWiFi registration in VoWiFi activation
+ * process.
+ *
+ * <p>Note: this is effective only if the {@link #KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING}
+ * is set to the QNS app.
+ */
+ public static final String KEY_QNS_VOWIFI_REGISTATION_TIMER_FOR_VOWIFI_ACTIVATION_INT =
+ "qns.vowifi_registation_timer_for_vowifi_activation_int";
+
+ /**
+ * Indicates whether to pop up a web portal of the carrier or to turn on WFC directly when
+ * {@link #KEY_QNS_VOWIFI_REGISTATION_TIMER_FOR_VOWIFI_ACTIVATION_INT} is expired in VoWiFi
+ * activation process
+ *
+ * <p>{@code true} - show the VoWiFi portal after the timer expires. {@code false}
+ * - turn on WFC UI after the timer expires.
+ *
+ * <p>Note: this is effective only if the {@link #KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING}
+ * is set to the QNS app.
+ */
+ public static final String KEY_QNS_SHOW_VOWIFI_PORTAL_AFTER_TIMEOUT_BOOL =
+ "qns.show_vowifi_portal_after_timeout_bool";
+
+ /**
+ * Indicates whether web portal {@link #KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING} of the
+ * carrier supports JavaScript callback interfaces
+ *
+ * <p>{@code true} - use webview with JavaScript callback interfaces to display web content.
+ * {@code false} - use chrome with custom tabs to display web content.
+ *
+ * <p>Note: this is effective only if the {@link #KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING}
+ * is set to the QNS app.
+ */
+ public static final String KEY_QNS_JS_CALLBACK_FOR_VOWIFI_PORTAL_BOOL =
+ "qns.js_callback_for_vowifi_portal_bool";
+
+ public static final int CONFIG_DEFAULT_VOWIFI_REGISTATION_TIMER = 120000;
+
+ WfcCarrierConfigManager(Context context, int subId) {
+ mSubId = subId;
+ mContext = context;
+ }
+
+ private static boolean getDefaultBooleanValueForKey(String key) {
+ Log.d(TAG, "Use default value for key: " + key);
+ switch (key) {
+ case KEY_QNS_SHOW_VOWIFI_PORTAL_AFTER_TIMEOUT_BOOL:
+ return true;
+ case KEY_QNS_JS_CALLBACK_FOR_VOWIFI_PORTAL_BOOL:
+ return false;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ private static String getDefaultStringValueForKey(String key) {
+ Log.d(TAG, "Use default value for key: " + key);
+ switch (key) {
+ case KEY_QNS_VOWIFI_ENTITLEMENT_SERVER_URL_STRING:
+ return "";
+ default:
+ break;
+ }
+ return "";
+ }
+
+ private static int getDefaultIntValueForKey(String key) {
+ Log.d(TAG, "Use default value for key: " + key);
+ switch (key) {
+ case KEY_QNS_VOWIFI_REGISTATION_TIMER_FOR_VOWIFI_ACTIVATION_INT:
+ return CONFIG_DEFAULT_VOWIFI_REGISTATION_TIMER;
+ default:
+ break;
+ }
+ return 0;
+ }
+
+ private PersistableBundle readFromCarrierConfigManager(Context context) {
+ PersistableBundle carrierConfigBundle;
+ CarrierConfigManager carrierConfigManager =
+ context.getSystemService(CarrierConfigManager.class);
+
+ if (carrierConfigManager == null) {
+ throw new IllegalStateException("Carrier config manager is null.");
+ }
+ carrierConfigBundle = carrierConfigManager.getConfigForSubId(mSubId);
+
+ return carrierConfigBundle;
+ }
+
+ @VisibleForTesting
+ void loadConfigurations() {
+ PersistableBundle carrierConfigBundle = readFromCarrierConfigManager(mContext);
+ Log.d(TAG, "CarrierConfig Bundle for subId: " + mSubId + carrierConfigBundle);
+ loadConfigurationsFromCarrierConfig(carrierConfigBundle);
+ }
+
+ @VisibleForTesting
+ void loadConfigurationsFromCarrierConfig(PersistableBundle carrierConfigBundle) {
+ mVowifiEntitlementServerUrl =
+ getStringConfig(carrierConfigBundle,
+ KEY_QNS_VOWIFI_ENTITLEMENT_SERVER_URL_STRING);
+ mVowifiRegistrationTimerForVowifiActivation =
+ getIntConfig(
+ carrierConfigBundle,
+ KEY_QNS_VOWIFI_REGISTATION_TIMER_FOR_VOWIFI_ACTIVATION_INT);
+ mIsShowVowifiPortalAfterTimeout =
+ getBooleanConfig(carrierConfigBundle,
+ KEY_QNS_SHOW_VOWIFI_PORTAL_AFTER_TIMEOUT_BOOL);
+ mIsJsCallbackForVowifiPortal =
+ getBooleanConfig(carrierConfigBundle,
+ KEY_QNS_JS_CALLBACK_FOR_VOWIFI_PORTAL_BOOL);
+ }
+
+ private boolean getBooleanConfig(PersistableBundle bundleCarrier, String key) {
+ if (bundleCarrier == null || bundleCarrier.get(key) == null) {
+ return getDefaultBooleanValueForKey(key);
+ }
+ return bundleCarrier.getBoolean(key);
+ }
+
+ private int getIntConfig(PersistableBundle bundleCarrier, String key) {
+ if (bundleCarrier == null || bundleCarrier.get(key) == null) {
+ return getDefaultIntValueForKey(key);
+ }
+ return bundleCarrier.getInt(key);
+ }
+
+ private String getStringConfig(PersistableBundle bundleCarrier, String key) {
+ if (bundleCarrier == null || bundleCarrier.get(key) == null) {
+ return getDefaultStringValueForKey(key);
+ }
+ return bundleCarrier.getString(key);
+ }
+
+ /**
+ * This method returns the URL of the VoWiFi entitlement server for an emergency address
+ * registration
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ String getVowifiEntitlementServerUrl() {
+ return mVowifiEntitlementServerUrl;
+ }
+
+ /**
+ * This method returns the wait timer in milliseconds that VoWiFi registration in VoWiFi
+ * activation process
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ int getVowifiRegistrationTimerForVowifiActivation() {
+ return mVowifiRegistrationTimerForVowifiActivation;
+ }
+
+ /**
+ * This method returns true if a web portal of the carrier is poped up when
+ * {@link #KEY_QNS_VOWIFI_REGISTATION_TIMER_FOR_VOWIFI_ACTIVATION_INT} is expired in VoWiFi
+ * activation process; Otherwise, WFC is tuned on directly.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ boolean isShowVowifiPortalAfterTimeout() {
+ return mIsShowVowifiPortalAfterTimeout;
+ }
+
+ /**
+ * This method returns true if JavaScript callback interface is not support for web portal
+ * {@link #KEY_WFC_EMERGENCY_ADDRESS_CARRIER_APP_STRING} of the carrier
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ boolean supportJsCallbackForVowifiPortal() {
+ return mIsJsCallbackForVowifiPortal;
+ }
+}
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcUtils.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcUtils.java
new file mode 100644
index 0000000..16f4a61
--- /dev/null
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/wfc/WfcUtils.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 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.telephony.qns.wfc;
+
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.VisibleForTesting;
+
+public final class WfcUtils {
+ private static final String TAG = WfcActivationActivity.TAG;
+
+ // Constants shared by WifiCallingSettings
+ static final String EXTRA_LAUNCH_CARRIER_APP = "EXTRA_LAUNCH_CARRIER_APP";
+ static final int LAUNCH_APP_ACTIVATE = 0;
+ static final int LAUNCH_APP_UPDATE = 1;
+
+ // OK to suppress warnings here because it's used only for unit tests
+ @SuppressLint("StaticFieldLeak")
+ private static WfcActivationHelper mWfcActivationHelper;
+ private static ActivityResultLauncher mWebViewResultsLauncher;
+
+ private WfcUtils() {}
+
+ /**
+ * 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.getDefaultDataSubscriptionId();
+ }
+ int subId =
+ intent.getIntExtra(
+ SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
+ SubscriptionManager.getDefaultDataSubscriptionId());
+ Log.d(TAG, "Start Activity with subId : " + subId);
+ return subId;
+ }
+
+ /**
+ * Returns {@link ImsMmTelManager} with specific subscription id. Returns {@code null} if
+ * provided subscription id invalid.
+ */
+ @Nullable
+ public static ImsMmTelManager getImsMmTelManager(int subId) {
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ try {
+ return ImsMmTelManager.createForSubscriptionId(subId);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Can't get ImsMmTelManager, IllegalArgumentException: subId = " + subId);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Dependency providers.
+ *
+ * <p>In normal case, setters are not invoked, hence getters return null. The component is
+ * supposed to do null check and initialize dependencies by itself. In tests, setters can be
+ * invoked to provide mock dependencies.
+ */
+ @VisibleForTesting
+ public static void setWfcActivationHelper(WfcActivationHelper obj) {
+ mWfcActivationHelper = obj;
+ }
+
+ @VisibleForTesting
+ public static void setWebviewResultLauncher(ActivityResultLauncher obj) {
+ mWebViewResultsLauncher = obj;
+ }
+
+ @Nullable
+ public static WfcActivationHelper getWfcActivationHelper() {
+ return mWfcActivationHelper;
+ }
+
+ @Nullable
+ public static ActivityResultLauncher getWebviewResultLauncher() {
+ return mWebViewResultsLauncher;
+ }
+}