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 
17 package com.android.services.telephony.domainselection;
18 
19 import static android.telephony.DomainSelectionService.SELECTOR_TYPE_CALLING;
20 
21 import android.annotation.NonNull;
22 import android.content.Context;
23 import android.os.Looper;
24 import android.os.Message;
25 import android.os.PersistableBundle;
26 import android.telecom.TelecomManager;
27 import android.telephony.Annotation.DisconnectCauses;
28 import android.telephony.CarrierConfigManager;
29 import android.telephony.DisconnectCause;
30 import android.telephony.DomainSelectionService.SelectionAttributes;
31 import android.telephony.NetworkRegistrationInfo;
32 import android.telephony.PhoneNumberUtils;
33 import android.telephony.ServiceState;
34 import android.telephony.SubscriptionManager;
35 import android.telephony.TransportSelectorCallback;
36 import android.telephony.ims.ImsReasonInfo;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.telephony.CallFailCause;
40 
41 /**
42  * Implements domain selector for outgoing non-emergency calls.
43  */
44 public class NormalCallDomainSelector extends DomainSelectorBase implements
45         ImsStateTracker.ImsStateListener, ImsStateTracker.ServiceStateListener {
46 
47     private static final String LOG_TAG = "NCDS";
48 
49     // Wait-time for IMS state change callback.
50     @VisibleForTesting
51     protected static final int WAIT_FOR_IMS_STATE_TIMEOUT_MS = 3000; // 3 seconds
52 
53     @VisibleForTesting
54     protected static final int MSG_WAIT_FOR_IMS_STATE_TIMEOUT = 11;
55 
56     @VisibleForTesting
57     protected enum SelectorState {
58         ACTIVE,
59         INACTIVE,
60         DESTROYED
61     };
62 
63     protected SelectorState mSelectorState = SelectorState.INACTIVE;
64     protected ServiceState mServiceState;
65     private boolean mImsRegStateReceived;
66     private boolean mMmTelCapabilitiesReceived;
67     private boolean mReselectDomain;
68 
NormalCallDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener destroyListener)69     public NormalCallDomainSelector(Context context, int slotId, int subId, @NonNull Looper looper,
70                                     @NonNull ImsStateTracker imsStateTracker,
71                                     @NonNull DestroyListener destroyListener) {
72         super(context, slotId, subId, looper, imsStateTracker, destroyListener, LOG_TAG);
73 
74         if (SubscriptionManager.isValidSubscriptionId(subId)) {
75             logd("Subscribing to state callbacks. Subid:" + subId);
76             mImsStateTracker.addServiceStateListener(this);
77             mImsStateTracker.addImsStateListener(this);
78 
79         } else {
80             loge("Invalid Subscription. Subid:" + subId);
81         }
82     }
83 
84     @Override
handleMessage(Message message)85     public void handleMessage(Message message) {
86         switch (message.what) {
87 
88             case MSG_WAIT_FOR_IMS_STATE_TIMEOUT: {
89                 loge("ImsStateTimeout. ImsState callback not received");
90                 if (mSelectorState != SelectorState.ACTIVE) {
91                     return;
92                 }
93 
94                 if (!mImsRegStateReceived) {
95                     onImsRegistrationStateChanged();
96                 }
97 
98                 if (!mMmTelCapabilitiesReceived) {
99                     onImsMmTelCapabilitiesChanged();
100                 }
101             }
102             break;
103 
104             default: {
105                 super.handleMessage(message);
106             }
107             break;
108         }
109     }
110 
111     @Override
selectDomain(SelectionAttributes attributes, TransportSelectorCallback callback)112     public void selectDomain(SelectionAttributes attributes, TransportSelectorCallback callback) {
113         mSelectionAttributes = attributes;
114         mTransportSelectorCallback = callback;
115         mSelectorState = SelectorState.ACTIVE;
116 
117         if (callback == null) {
118             mSelectorState = SelectorState.INACTIVE;
119             loge("Invalid params: TransportSelectorCallback is null");
120             return;
121         }
122 
123         if (attributes == null) {
124             loge("Invalid params: SelectionAttributes are null");
125             notifySelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
126             return;
127         }
128 
129         int subId = attributes.getSubscriptionId();
130         boolean validSubscriptionId = SubscriptionManager.isValidSubscriptionId(subId);
131         if (attributes.getSelectorType() != SELECTOR_TYPE_CALLING || attributes.isEmergency()
132                 || !validSubscriptionId) {
133             loge("Domain Selection stopped. SelectorType:" + attributes.getSelectorType()
134                     + ", isEmergency:" + attributes.isEmergency()
135                     + ", ValidSubscriptionId:" + validSubscriptionId);
136 
137             notifySelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
138             return;
139         }
140 
141         if (subId == getSubId()) {
142             logd("NormalCallDomainSelection triggered. Sub-id:" + subId);
143             sendEmptyMessageDelayed(MSG_WAIT_FOR_IMS_STATE_TIMEOUT, WAIT_FOR_IMS_STATE_TIMEOUT_MS);
144             post(() -> selectDomain());
145         } else {
146             mSelectorState = SelectorState.INACTIVE;
147             loge("Subscription-ids doesn't match. This instance is associated with sub-id:"
148                     + getSubId() + ", requested sub-id:" + subId);
149             // TODO: Throw anomaly here. This condition should never occur.
150         }
151     }
152 
153     @Override
reselectDomain(SelectionAttributes attributes)154     public void reselectDomain(SelectionAttributes attributes) {
155         logd("reselectDomain called");
156         mReselectDomain = true;
157         selectDomain(attributes, mTransportSelectorCallback);
158     }
159 
160     @Override
finishSelection()161     public synchronized void finishSelection() {
162         logd("finishSelection");
163         if (mSelectorState == SelectorState.ACTIVE) {
164             // This is cancel selection case.
165             cancelSelection();
166             return;
167         }
168 
169         if (mSelectorState != SelectorState.DESTROYED) {
170             mImsStateTracker.removeServiceStateListener(this);
171             mImsStateTracker.removeImsStateListener(this);
172             mSelectionAttributes = null;
173             mTransportSelectorCallback = null;
174             destroy();
175         }
176     }
177 
178     @Override
destroy()179     public void destroy() {
180         logd("destroy");
181         switch (mSelectorState) {
182             case INACTIVE:
183                 mSelectorState = SelectorState.DESTROYED;
184                 super.destroy();
185                 break;
186 
187             case ACTIVE:
188                 loge("destroy is called when selector state is in ACTIVE state");
189                 cancelSelection();
190                 break;
191 
192             case DESTROYED:
193                 super.destroy();
194                 break;
195         }
196     }
197 
cancelSelection()198     public void cancelSelection() {
199         logd("cancelSelection");
200         mSelectorState = SelectorState.INACTIVE;
201         mReselectDomain = false;
202         if (mTransportSelectorCallback != null) {
203             mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.OUTGOING_CANCELED);
204         }
205         finishSelection();
206     }
207 
208     @Override
onImsRegistrationStateChanged()209     public void onImsRegistrationStateChanged() {
210         logd("onImsRegistrationStateChanged. IsImsRegistered: "
211                 + mImsStateTracker.isImsRegistered());
212         mImsRegStateReceived = true;
213         selectDomain();
214     }
215 
216     @Override
onImsMmTelCapabilitiesChanged()217     public void onImsMmTelCapabilitiesChanged() {
218         logd("onImsMmTelCapabilitiesChanged. ImsVoiceCap: " + mImsStateTracker.isImsVoiceCapable()
219                 + " ImsVideoCap: " + mImsStateTracker.isImsVideoCapable());
220         mMmTelCapabilitiesReceived = true;
221         selectDomain();
222     }
223 
224     @Override
onImsMmTelFeatureAvailableChanged()225     public void onImsMmTelFeatureAvailableChanged() {
226         logd("onImsMmTelFeatureAvailableChanged");
227         selectDomain();
228     }
229 
230     @Override
onServiceStateUpdated(ServiceState serviceState)231     public void onServiceStateUpdated(ServiceState serviceState) {
232         logd("onServiceStateUpdated");
233         mServiceState = serviceState;
234         selectDomain();
235     }
236 
notifyPsSelected()237     private void notifyPsSelected() {
238         logd("notifyPsSelected");
239         mSelectorState = SelectorState.INACTIVE;
240         if (mImsStateTracker.isImsRegisteredOverWlan()) {
241             logd("WLAN selected");
242             mTransportSelectorCallback.onWlanSelected(false);
243         } else {
244             if (mWwanSelectorCallback == null) {
245                 mTransportSelectorCallback.onWwanSelected((callback) -> {
246                     mWwanSelectorCallback = callback;
247                     notifyPsSelectedInternal();
248                 });
249             } else {
250                 notifyPsSelectedInternal();
251             }
252         }
253     }
254 
notifyPsSelectedInternal()255     private void notifyPsSelectedInternal() {
256         if (mWwanSelectorCallback != null) {
257             logd("notifyPsSelected - onWwanSelected");
258             mWwanSelectorCallback.onDomainSelected(NetworkRegistrationInfo.DOMAIN_PS, false);
259         } else {
260             loge("wwanSelectorCallback is null");
261             mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
262         }
263     }
264 
notifyCsSelected()265     private void notifyCsSelected() {
266         if (isOutOfService()) {
267             loge("Cannot place call in current ServiceState: " + mServiceState.getState());
268             notifySelectionTerminated(DisconnectCause.OUT_OF_SERVICE);
269             return;
270         }
271 
272         logd("notifyCsSelected");
273         mSelectorState = SelectorState.INACTIVE;
274         if (mWwanSelectorCallback == null) {
275             mTransportSelectorCallback.onWwanSelected((callback) -> {
276                 mWwanSelectorCallback = callback;
277                 notifyCsSelectedInternal();
278             });
279         } else {
280             notifyCsSelectedInternal();
281         }
282     }
283 
notifyCsSelectedInternal()284     private void notifyCsSelectedInternal() {
285         if (mWwanSelectorCallback != null) {
286             logd("wwanSelectorCallback -> onDomainSelected(DOMAIN_CS)");
287             mWwanSelectorCallback.onDomainSelected(NetworkRegistrationInfo.DOMAIN_CS, false);
288         } else {
289             loge("wwanSelectorCallback is null");
290             mTransportSelectorCallback.onSelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
291         }
292     }
293 
notifySelectionTerminated(@isconnectCauses int cause)294     private void notifySelectionTerminated(@DisconnectCauses int cause) {
295         mSelectorState = SelectorState.INACTIVE;
296         if (mTransportSelectorCallback != null) {
297             mTransportSelectorCallback.onSelectionTerminated(cause);
298             finishSelection();
299         }
300     }
301 
isOutOfService()302     private boolean isOutOfService() {
303         return (mServiceState.getState() == ServiceState.STATE_OUT_OF_SERVICE
304                 || mServiceState.getState() == ServiceState.STATE_POWER_OFF
305                 || mServiceState.getState() == ServiceState.STATE_EMERGENCY_ONLY);
306     }
307 
isWpsCallSupportedByIms()308     private boolean isWpsCallSupportedByIms() {
309         CarrierConfigManager configManager = mContext.getSystemService(CarrierConfigManager.class);
310 
311         PersistableBundle config = null;
312         if (configManager != null) {
313             config = configManager.getConfigForSubId(mSelectionAttributes.getSubscriptionId(),
314                     new String[] {CarrierConfigManager.KEY_SUPPORT_WPS_OVER_IMS_BOOL});
315         }
316 
317         return (config != null)
318                 ? config.getBoolean(CarrierConfigManager.KEY_SUPPORT_WPS_OVER_IMS_BOOL) : false;
319     }
320 
handleWpsCall()321     private void handleWpsCall() {
322         if (isWpsCallSupportedByIms()) {
323             logd("WPS call placed over PS");
324             notifyPsSelected();
325         } else {
326             logd("WPS call placed over CS");
327             notifyCsSelected();
328         }
329     }
330 
isTtySupportedByIms()331     private boolean isTtySupportedByIms() {
332         CarrierConfigManager configManager = mContext.getSystemService(CarrierConfigManager.class);
333 
334         PersistableBundle config = null;
335         if (configManager != null) {
336             config = configManager.getConfigForSubId(mSelectionAttributes.getSubscriptionId(),
337                     new String[] {CarrierConfigManager.KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL});
338         }
339 
340         return (config != null)
341                 && config.getBoolean(CarrierConfigManager.KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL);
342     }
343 
isTtyModeEnabled()344     private boolean isTtyModeEnabled() {
345         TelecomManager tm = mContext.getSystemService(TelecomManager.class);
346         if (tm == null) {
347             loge("isTtyModeEnabled: telecom not available");
348             return false;
349         }
350         return tm.getCurrentTtyMode() != TelecomManager.TTY_MODE_OFF;
351     }
352 
selectDomain()353     private synchronized void selectDomain() {
354         if (mSelectorState != SelectorState.ACTIVE || mSelectionAttributes == null
355                 || mTransportSelectorCallback == null) {
356             mSelectorState = SelectorState.INACTIVE;
357             logd("Domain Selection is stopped.");
358             return;
359         }
360 
361         if (mServiceState == null) {
362             logd("Waiting for ServiceState callback.");
363             return;
364         }
365 
366         // Check if this is a re-dial scenario
367         ImsReasonInfo imsReasonInfo = mSelectionAttributes.getPsDisconnectCause();
368         if (mReselectDomain) {
369             mReselectDomain = false;
370 
371             // IMS -> CS
372             if (imsReasonInfo != null) {
373                 logd("PsDisconnectCause:" + imsReasonInfo.getCode());
374                 if (imsReasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED) {
375                     logd("Redialing over CS");
376                     notifyCsSelected();
377                 } else {
378                     // Not a valid redial
379                     logd("Redialing cancelled.");
380                     notifySelectionTerminated(DisconnectCause.NOT_VALID);
381                 }
382                 return;
383             }
384 
385             // CS -> IMS
386             int csDisconnectCause = mSelectionAttributes.getCsDisconnectCause();
387             switch (csDisconnectCause) {
388                 case CallFailCause.EMC_REDIAL_ON_IMS:
389                 case CallFailCause.EMC_REDIAL_ON_VOWIFI:
390                     // Check IMS registration state.
391                     if (mImsStateTracker.isImsRegistered()) {
392                         logd("IMS is registered");
393                         notifyPsSelected();
394                         return;
395                     } else {
396                         logd("IMS is NOT registered");
397                     }
398             }
399 
400             // Not a valid redial
401             logd("Redialing cancelled.");
402             notifySelectionTerminated(DisconnectCause.NOT_VALID);
403             return;
404         }
405 
406         if (!mImsStateTracker.isMmTelFeatureAvailable()) {
407             logd("MmTelFeatureAvailable unavailable");
408             notifyCsSelected();
409             return;
410         }
411 
412         if (!mImsRegStateReceived || !mMmTelCapabilitiesReceived) {
413             loge("Waiting for ImsState and MmTelCapabilities callbacks");
414             return;
415         }
416 
417         if (hasMessages(MSG_WAIT_FOR_IMS_STATE_TIMEOUT)) {
418             removeMessages(MSG_WAIT_FOR_IMS_STATE_TIMEOUT);
419         }
420 
421         // Check IMS registration state.
422         if (!mImsStateTracker.isImsRegistered()) {
423             logd("IMS is NOT registered");
424             notifyCsSelected();
425             return;
426         }
427 
428         // Check TTY
429         if (isTtyModeEnabled() && !isTtySupportedByIms()) {
430             notifyCsSelected();
431             return;
432         }
433 
434         // Handle video call.
435         if (mSelectionAttributes.isVideoCall()) {
436             logd("It's a video call");
437             if (mImsStateTracker.isImsVideoCapable()) {
438                 logd("IMS is video capable");
439                 notifyPsSelected();
440             } else {
441                 logd("IMS is not video capable. Ending the call");
442                 notifySelectionTerminated(DisconnectCause.OUTGOING_FAILURE);
443             }
444             return;
445         }
446 
447         // Handle voice call.
448         if (mImsStateTracker.isImsVoiceCapable()) {
449             logd("IMS is voice capable");
450             String number = mSelectionAttributes.getAddress().getSchemeSpecificPart();
451             if (PhoneNumberUtils.isWpsCallNumber(number)) {
452                 handleWpsCall();
453             } else {
454                 notifyPsSelected();
455             }
456         } else {
457             logd("IMS is not voice capable");
458             // Voice call CS fallback
459             notifyCsSelected();
460         }
461     }
462 
463     @VisibleForTesting
getSelectorState()464     protected SelectorState getSelectorState() {
465         return mSelectorState;
466     }
467 }
468