diff options
author | Sean.JS Tsai <seanjstsai@google.com> | 2022-12-06 08:27:34 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2022-12-06 08:27:34 +0000 |
commit | 365757bd0d701e8c8e64ca27f0a6ee882c252e71 (patch) | |
tree | d4b4be04b5a1d5a5912148a5a9afe5348f87e984 /services/QualifiedNetworksService/src/com/android | |
parent | 4c6995266b5f7bc738f3117792e486c6fbdb75f0 (diff) | |
parent | 5e440eb9a6a06ffb495a5a4f6acd11c344143ba3 (diff) | |
download | Telephony-365757bd0d701e8c8e64ca27f0a6ee882c252e71.tar.gz |
Merge "[WFC] move ePDG-based WFC activation from WFC APP to Google QNS"
Diffstat (limited to 'services/QualifiedNetworksService/src/com/android')
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; + } +} |