1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.telephony.qns.wfc;
17 
18 import static android.os.AsyncTask.THREAD_POOL_EXECUTOR;
19 
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.net.ConnectivityManager;
24 import android.net.NetworkInfo;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.telephony.AccessNetworkConstants;
29 import android.telephony.SubscriptionManager;
30 import android.telephony.ims.ImsException;
31 import android.telephony.ims.ImsMmTelManager;
32 import android.telephony.ims.ImsReasonInfo;
33 
34 import android.util.Log;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.telephony.qns.R;
38 
39 import java.util.concurrent.Executor;
40 
41 /** A class with helper methods for WfcActivationCanadaActivity */
42 public class WfcActivationHelper {
43     private static final String TAG = WfcActivationActivity.TAG;
44 
45     @VisibleForTesting static final int PRE_EPDG_CONNECTION_DELAY_MS = 1000; // 1 second
46 
47     // Enums for Wi-Fi check result
48     public static final int WIFI_CONNECTION_SUCCESS = 0;
49     public static final int WIFI_CONNECTION_ERROR = 1;
50 
51     // Enums for ePDG connection result
52     public static final int EPDG_CONNECTION_SUCCESS = 0;
53     public static final int EPDG_CONNECTION_ERROR = 1;
54 
55     // Event IDs for ePDG connection
56     @VisibleForTesting static final int EVENT_PRE_START_ATTEMPT = 0;
57     @VisibleForTesting static final int EVENT_START_ATTEMPT = 1;
58     @VisibleForTesting static final int EVENT_FINISH_ATTEMPT = 2;
59     private static final int EVENT_RESULT_SUCCESS = 3;
60     private static final int EVENT_TIMEOUT = 4;
61     private static final int EVENT_RESULT_FAILURE_IKEV2 = 5;
62     private static final int EVENT_RESULT_FAILURE_OTHER = 6;
63 
64     public static final String ACTION_TRY_WFC_CONNECTION =
65             "com.android.qns.wfcactivation.TRY_WFC_CONNECTION";
66     public static final String EXTRA_SUB_ID = "SUB_ID";
67     public static final String EXTRA_TRY_STATUS = "TRY_STATUS";
68     public static final int STATUS_START = 1;
69     public static final int STATUS_END = 2;
70 
71     // Dependencies
72     private final Context mContext;
73     private final ConnectivityManager mConnectivityManager;
74     private final ImsMmTelManager mImsMmTelManager;
75     private final WfcCarrierConfigManager mWfcConfigManager;
76 
77     private final int mSubId;
78     private final Executor mBackgroundExecutor;
79 
WfcActivationHelper(Context context, int subId)80     public WfcActivationHelper(Context context, int subId) {
81         this(
82                 context,
83                 subId,
84                 context.getSystemService(ConnectivityManager.class),
85                 WfcUtils.getImsMmTelManager(subId),
86                 new WfcCarrierConfigManager(context.getApplicationContext(), subId),
87                         THREAD_POOL_EXECUTOR);
88     }
89 
90     @VisibleForTesting
WfcActivationHelper( Context context, int subId, ConnectivityManager cm, @Nullable ImsMmTelManager imsMmTelManager, WfcCarrierConfigManager wfcConfigManager, Executor backgroundExecutor)91     WfcActivationHelper(
92             Context context,
93             int subId,
94             ConnectivityManager cm,
95             @Nullable ImsMmTelManager imsMmTelManager,
96             WfcCarrierConfigManager wfcConfigManager,
97             Executor backgroundExecutor) {
98         mContext = context;
99         mSubId = subId;
100         mConnectivityManager = cm;
101         mImsMmTelManager = imsMmTelManager;
102         mWfcConfigManager = wfcConfigManager;
103         mBackgroundExecutor = backgroundExecutor;
104         mWfcConfigManager.loadConfigurations();
105     }
106 
107     /**
108      * Check WiFi connection
109      *
110      * @param msg The Message to be send with arg1 = result. Result is one of WIFI_CONNECTION_*.
111      */
checkWiFi(Message msg)112     public void checkWiFi(Message msg) {
113         msg.arg1 = checkWiFiAvailability() ? WIFI_CONNECTION_SUCCESS : WIFI_CONNECTION_ERROR;
114         msg.sendToTarget();
115     }
116 
checkWiFiAvailability()117     private boolean checkWiFiAvailability() {
118         NetworkInfo activeNetwork = mConnectivityManager.getActiveNetworkInfo();
119         return activeNetwork != null
120                 && activeNetwork.isConnected()
121                 && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
122     }
123 
notifyQnsServiceToSetWfcMode(int status)124     private void notifyQnsServiceToSetWfcMode(int status) {
125         String qnsPackage = mContext.getResources().getString(R.string.qns_package);
126         Intent intent = new Intent(ACTION_TRY_WFC_CONNECTION);
127         intent.putExtra(EXTRA_SUB_ID, mSubId);
128         intent.putExtra(EXTRA_TRY_STATUS, status);
129         intent.setPackage(qnsPackage);
130         Log.d(TAG, "notify QNS: subId =" + mSubId + ", status =" + status);
131         mContext.sendBroadcast(intent);
132     }
133 
134     // This class is a effectively a one-way state machine that cannot be reset & reused. Each call
135     // of tryEpdgConnectionOverWiFi() creates a new instance of this class.
136     private class EpdgConnectHandler extends Handler {
137         final ImsCallback imsCallback;
138         final Message result;
139         boolean imsCallbackRegistered;
140         boolean waitingForResult; // ImsCallback wil be no-op when this is false
141 
EpdgConnectHandler(Looper looper, Message result)142         EpdgConnectHandler(Looper looper, Message result) {
143             super(looper);
144             imsCallback = new ImsCallback(this);
145             this.result = result;
146         }
147 
148         @Override
handleMessage(Message msg)149         public void handleMessage(Message msg) {
150             switch (msg.what) {
151                 case EVENT_PRE_START_ATTEMPT:
152                     // The callback must be registered before triggering ePDG connection, because
153                     // the very 1st firing of the callback after registering MAY be the last IMS
154                     // state.
155                     // We assume 1 second is enough for that 1st firing.
156                     // This means adding 1s delay to WFC activation flow in all cases, and it should
157                     // be fine, given this can only be triggered by user manually and is not
158                     // expected to be fast.
159                     waitingForResult = false;
160                     registerImsRegistrationCallback();
161                     // Populate arg1 to EVENT_START_ATTEMPT message
162                     sendMessageDelayed(
163                             obtainMessage(EVENT_START_ATTEMPT, msg.arg1, 0),
164                             /* delayMillis= */ msg.arg2);
165                     break;
166 
167                 case EVENT_START_ATTEMPT:
168                     Log.d(TAG, "Try to setup ePDG connection over WiFi");
169                     waitingForResult = true;
170 
171                     mBackgroundExecutor.execute(
172                             () -> {
173                                 // WFC: on; WFC preference: WiFi preferred (2)
174                                 mImsMmTelManager.setVoWiFiNonPersistent(true, 2);
175                                 // notify IMS to program WFC on and WFC mode as Wi-Fi Preferred
176                                 notifyQnsServiceToSetWfcMode(STATUS_START);
177                             });
178 
179                     // Timeout event
180                     Log.d(TAG, "Will timeout after " + msg.arg1 + " ms");
181                     sendEmptyMessageDelayed(EVENT_TIMEOUT, /* delayMillis= */ msg.arg1);
182                     break;
183 
184                 case EVENT_TIMEOUT:
185                     Log.d(TAG, "Timeout: IKEV2 Auth failure not received.");
186                     if (getTimeoutResult() == EPDG_CONNECTION_SUCCESS) {
187                         sendEmptyMessage(EVENT_RESULT_SUCCESS);
188                     } else {
189                         sendEmptyMessage(EVENT_RESULT_FAILURE_IKEV2);
190                     }
191                     break;
192 
193                 case EVENT_RESULT_SUCCESS:
194                     result.arg1 = EPDG_CONNECTION_SUCCESS;
195                     // Clean up and send result
196                     sendEmptyMessage(EVENT_FINISH_ATTEMPT);
197                     break;
198 
199                 case EVENT_RESULT_FAILURE_IKEV2:
200                     Log.d(TAG, "Turn off WFC");
201                     // WFC: off; WFC preference: cellular preferred (1)
202                     mBackgroundExecutor.execute(
203                             () -> mImsMmTelManager.setVoWiFiNonPersistent(false, 1));
204                     // Set result: failure
205                     result.arg1 = EPDG_CONNECTION_ERROR;
206                     // Clean up and send result
207                     sendEmptyMessage(EVENT_FINISH_ATTEMPT);
208                     break;
209 
210                 case EVENT_FINISH_ATTEMPT:
211                     waitingForResult = false;
212                     // Remove timeout event - if we get here via EVENT_TIMEOUT, this do nothing.
213                     removeMessages(EVENT_TIMEOUT);
214                     // Unregister mImsCallback
215                     unregisterImsRegistrationCallback();
216                     mBackgroundExecutor.execute(
217                             () -> {
218                                 // Turn on WFC if success. W/o this, WFC could be turned
219                                 // ON (by STATUS_START) - OFF (by STATUS_END) - ON (by Settings app)
220                                 // which causes unnecessary IMS registration traffic.
221                                 // This must be done before sending STATUS_END so vendor IMS will
222                                 // see DB value ON.
223                                 if (result.arg1 == EPDG_CONNECTION_SUCCESS) {
224                                     Log.d(TAG, "Turn on WFC");
225                                     mImsMmTelManager.setVoWiFiSettingEnabled(true);
226                                 }
227                                 // Notify IMS to revert WFC on/off and mode to follow user settings.
228                                 // Notify here to make sure all cases (success, failure, timeout)
229                                 // reach this line.
230                                 notifyQnsServiceToSetWfcMode(STATUS_END);
231                                 // Send result
232                                 result.sendToTarget();
233                             });
234                     break;
235 
236                 case EVENT_RESULT_FAILURE_OTHER:
237                     break;
238                 default: // Do nothing
239             }
240         }
241 
registerImsRegistrationCallback()242         private void registerImsRegistrationCallback() {
243             try {
244                 Log.d(TAG, "registerImsRegistrationCallback");
245                 mImsMmTelManager.registerImsRegistrationCallback(this::post, imsCallback);
246                 imsCallbackRegistered = true;
247             } catch (ImsException | RuntimeException e) {
248                 Log.e(TAG, "registerImsRegistrationCallback failed", e);
249                 // Fail silently to trigger timeout
250                 imsCallbackRegistered = false;
251             }
252         }
253 
unregisterImsRegistrationCallback()254         private void unregisterImsRegistrationCallback() {
255             if (!imsCallbackRegistered) {
256                 return;
257             }
258 
259             try {
260                 Log.d(TAG, "unregisterImsRegistrationCallback");
261                 mImsMmTelManager.unregisterImsRegistrationCallback(imsCallback);
262                 imsCallbackRegistered = false;
263             } catch (RuntimeException e) {
264                 Log.e(TAG, "unregisterImsRegistrationCallback failed", e);
265             }
266         }
267     }
268 
269     /**
270      * Try to setup ePDG connection over WiFi.
271      *
272      * @param msg The Message to be send with arg1 = result. Result is one of EPDG_CONNECTION_*.
273      * @param timeoutMs Timeout, in milliseconds, then abort waiting for ePDG connection result.
274      */
tryEpdgConnectionOverWiFi(Message msg, int timeoutMs)275     public void tryEpdgConnectionOverWiFi(Message msg, int timeoutMs) {
276         if (mImsMmTelManager == null) {
277             // Send message with EPDG_CONNECTION_ERROR immediately.
278             Log.e(TAG, "ImsMmTelManager is null");
279             msg.arg1 = EPDG_CONNECTION_ERROR;
280             msg.sendToTarget();
281             return;
282         }
283 
284         // NOTE: This private handler is hosted on the same looper as msg.
285         EpdgConnectHandler handler = new EpdgConnectHandler(msg.getTarget().getLooper(), msg);
286         // Start attempt of ePDG connection.
287         handler.obtainMessage(EVENT_PRE_START_ATTEMPT, timeoutMs, PRE_EPDG_CONNECTION_DELAY_MS)
288                 .sendToTarget();
289     }
290 
291     @VisibleForTesting
292     static class ImsCallback extends ImsMmTelManager.RegistrationCallback {
293         private final EpdgConnectHandler handler;
294 
ImsCallback(EpdgConnectHandler handler)295         ImsCallback(EpdgConnectHandler handler) {
296             this.handler = handler;
297         }
298 
299         @Override
onRegistered(int imsTransportType)300         public void onRegistered(int imsTransportType) {
301             if (!handler.waitingForResult) {
302                 return;
303             }
304             if (imsTransportType != AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
305                 return;
306             }
307             Log.d(TAG, "IMS connected on WLAN.");
308             handler.sendEmptyMessage(EVENT_RESULT_SUCCESS);
309         }
310 
311         @Override
onUnregistered(ImsReasonInfo imsReasonInfo)312         public void onUnregistered(ImsReasonInfo imsReasonInfo) {
313             if (!handler.waitingForResult) {
314                 return;
315             }
316             Log.d(TAG, "IMS disconnected: " + imsReasonInfo);
317             if (isIkev2AuthFailure(imsReasonInfo)) {
318                 handler.sendEmptyMessage(EVENT_RESULT_FAILURE_IKEV2);
319             } else {
320                 handler.obtainMessage(
321                                 EVENT_RESULT_FAILURE_OTHER,
322                                 imsReasonInfo.getCode(),
323                                 imsReasonInfo.getExtraCode())
324                         .sendToTarget();
325             }
326         }
327 
328         @Override
onTechnologyChangeFailed(int imsTransportType, ImsReasonInfo imsReasonInfo)329         public void onTechnologyChangeFailed(int imsTransportType, ImsReasonInfo imsReasonInfo) {
330             if (!handler.waitingForResult) {
331                 return;
332             }
333             if (imsTransportType != AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
334                 return;
335             }
336             Log.d(TAG, "IMS registration failed on WLAN: " + imsReasonInfo);
337             if (isIkev2AuthFailure(imsReasonInfo)) {
338                 handler.sendEmptyMessage(EVENT_RESULT_FAILURE_IKEV2);
339             } else {
340                 handler.obtainMessage(
341                                 EVENT_RESULT_FAILURE_OTHER,
342                                 imsReasonInfo.getCode(),
343                                 imsReasonInfo.getExtraCode())
344                         .sendToTarget();
345             }
346         }
347     }
348 
isIkev2AuthFailure(ImsReasonInfo imsReasonInfo)349     static boolean isIkev2AuthFailure(ImsReasonInfo imsReasonInfo) {
350         if (imsReasonInfo.getCode() == ImsReasonInfo.CODE_EPDG_TUNNEL_ESTABLISH_FAILURE) {
351             if (imsReasonInfo.getExtraCode() == ImsReasonInfo.CODE_IKEV2_AUTH_FAILURE) {
352                 return true;
353             }
354         }
355         return false;
356     }
357 
getTimeoutResult()358     private int getTimeoutResult() {
359         return mWfcConfigManager.isShowVowifiPortalAfterTimeout()
360                 ? EPDG_CONNECTION_ERROR
361                 : EPDG_CONNECTION_SUCCESS;
362     }
363 
getWebPortalUrl()364     public String getWebPortalUrl() {
365         return mWfcConfigManager.getVowifiEntitlementServerUrl();
366     }
367 
getVowifiRegistrationTimerForVowifiActivation()368     public int getVowifiRegistrationTimerForVowifiActivation() {
369         return mWfcConfigManager.getVowifiRegistrationTimerForVowifiActivation();
370     }
371 
supportJsCallbackForVowifiPortal()372     public boolean supportJsCallbackForVowifiPortal() {
373         return mWfcConfigManager.supportJsCallbackForVowifiPortal();
374     }
375 }
376