1 /*
2  * Copyright (C) 2024 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 android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.res.Resources;
24 import android.os.SystemProperties;
25 import android.telephony.PhoneNumberUtils;
26 import android.telephony.ServiceState;
27 import android.telephony.TelephonyManager;
28 import android.telephony.emergency.EmergencyNumber;
29 import android.text.TextUtils;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.telephony.LocaleTracker;
35 import com.android.internal.telephony.Phone;
36 import com.android.internal.telephony.PhoneConstants;
37 import com.android.internal.telephony.PhoneFactory;
38 import com.android.internal.telephony.ServiceStateTracker;
39 import com.android.phone.R;
40 
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.List;
44 
45 /**
46  * Manages dynamic routing of emergency numbers.
47  *
48  * Normal routing shall be tried if noraml service is available.
49  * Otherwise, emergency routing shall be tried.
50  */
51 public class DynamicRoutingController {
52     private static final String TAG = "DynamicRoutingController";
53     private static final boolean DBG = (SystemProperties.getInt("ro.debuggable", 0) == 1);
54 
55     private static final DynamicRoutingController sInstance =
56             new DynamicRoutingController();
57 
58     /** PhoneFactory Dependencies for testing. */
59     @VisibleForTesting
60     public interface PhoneFactoryProxy {
getPhone(int phoneId)61         Phone getPhone(int phoneId);
62     }
63 
64     private static class PhoneFactoryProxyImpl implements PhoneFactoryProxy {
65         @Override
getPhone(int phoneId)66         public Phone getPhone(int phoneId) {
67             return PhoneFactory.getPhone(phoneId);
68         }
69     }
70 
71     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
72         @Override
73         public void onReceive(Context context, Intent intent) {
74             if (intent.getAction().equals(
75                     TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)) {
76                 int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, -1);
77                 String countryIso = intent.getStringExtra(
78                         TelephonyManager.EXTRA_NETWORK_COUNTRY);
79                 Log.i(TAG, "ACTION_NETWORK_COUNTRY_CHANGED phoneId: " + phoneId
80                         + " countryIso: " + countryIso);
81                 if (TextUtils.isEmpty(countryIso)) {
82                     countryIso = getLastKnownCountryIso(phoneId);
83                     if (TextUtils.isEmpty(countryIso)) {
84                         return;
85                     }
86                 }
87                 String prevIso = mNetworkCountries.get(Integer.valueOf(phoneId));
88                 if (!TextUtils.equals(prevIso, countryIso)) {
89                     mNetworkCountries.put(Integer.valueOf(phoneId), countryIso);
90                     updateDynamicEmergencyNumbers(phoneId);
91                 }
92                 mLastCountryIso = countryIso;
93             }
94         }
95     };
96 
getLastKnownCountryIso(int phoneId)97     private String getLastKnownCountryIso(int phoneId) {
98         try {
99             Phone phone = mPhoneFactoryProxy.getPhone(phoneId);
100             if (phone == null) return "";
101 
102             ServiceStateTracker sst = phone.getServiceStateTracker();
103             if (sst == null) return "";
104 
105             LocaleTracker lt = sst.getLocaleTracker();
106             if (lt != null) {
107                 String iso = lt.getLastKnownCountryIso();
108                 Log.e(TAG, "getLastKnownCountryIso iso=" + iso);
109                 return iso;
110             }
111         } catch (Exception e) {
112             Log.e(TAG, "getLastKnownCountryIso e=" + e);
113         }
114         return "";
115     }
116 
117     private final PhoneFactoryProxy mPhoneFactoryProxy;
118     private final ArrayMap<Integer, String> mNetworkCountries = new ArrayMap<>();
119     private final ArrayMap<Integer, List<EmergencyNumber>> mEmergencyNumbers = new ArrayMap<>();
120 
121     private String mLastCountryIso;
122     private boolean mEnabled;
123     private List<String> mCountriesEnabled = null;
124     private List<String> mDynamicNumbers = null;
125 
126 
127     /**
128      * Returns the singleton instance of DynamicRoutingController.
129      *
130      * @return A {@link DynamicRoutingController} instance.
131      */
getInstance()132     public static DynamicRoutingController getInstance() {
133         return sInstance;
134     }
135 
DynamicRoutingController()136     private DynamicRoutingController() {
137           this(new PhoneFactoryProxyImpl());
138     }
139 
140     @VisibleForTesting
DynamicRoutingController(PhoneFactoryProxy phoneFactoryProxy)141     public DynamicRoutingController(PhoneFactoryProxy phoneFactoryProxy) {
142         mPhoneFactoryProxy = phoneFactoryProxy;
143     }
144 
145     /**
146      * Initializes the instance.
147      *
148      * @param context The context of the application.
149      */
initialize(Context context)150     public void initialize(Context context) {
151         try {
152             mEnabled = context.getResources().getBoolean(R.bool.dynamic_routing_emergency_enabled);
153         } catch (Resources.NotFoundException nfe) {
154             Log.e(TAG, "init exception=" + nfe);
155         } catch (NullPointerException npe) {
156             Log.e(TAG, "init exception=" + npe);
157         }
158 
159         mCountriesEnabled = readResourceConfiguration(context,
160                 R.array.config_countries_dynamic_routing_emergency_enabled);
161 
162         mDynamicNumbers = readResourceConfiguration(context,
163                 R.array.config_dynamic_routing_emergency_numbers);
164 
165         Log.i(TAG, "init enabled=" + mEnabled + ", countriesEnabled=" + mCountriesEnabled);
166 
167         if (mEnabled) {
168             //register country change listener
169             IntentFilter filter = new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
170             context.registerReceiver(mIntentReceiver, filter);
171         }
172     }
173 
readResourceConfiguration(Context context, int id)174     private List<String> readResourceConfiguration(Context context, int id) {
175         Log.i(TAG, "readResourceConfiguration id=" + id);
176 
177         List<String> resource = null;
178         try {
179             resource = Arrays.asList(context.getResources().getStringArray(id));
180         } catch (Resources.NotFoundException nfe) {
181             Log.e(TAG, "readResourceConfiguration exception=" + nfe);
182         } catch (NullPointerException npe) {
183             Log.e(TAG, "readResourceConfiguration exception=" + npe);
184         } finally {
185             if (resource == null) {
186                 resource = new ArrayList<String>();
187             }
188         }
189         return resource;
190     }
191 
192     /**
193      * Returns whether the dynamic routing feature is enabled.
194      */
isDynamicRoutingEnabled()195     public boolean isDynamicRoutingEnabled() {
196         Log.i(TAG, "isDynamicRoutingEnabled " + mEnabled);
197         return mEnabled;
198     }
199 
200     /**
201      * Returns whether the dynamic routing is enabled with the given {@link Phone}.
202      * @param phone A {@link Phone} instance.
203      */
isDynamicRoutingEnabled(Phone phone)204     public boolean isDynamicRoutingEnabled(Phone phone) {
205         Log.i(TAG, "isDynamicRoutingEnabled");
206         if (phone == null) return false;
207         String iso = mNetworkCountries.get(Integer.valueOf(phone.getPhoneId()));
208         Log.i(TAG, "isDynamicRoutingEnabled phoneId=" + phone.getPhoneId() + ", iso=" + iso
209                 + ", lastIso=" + mLastCountryIso);
210         if (TextUtils.isEmpty(iso)) {
211             iso = mLastCountryIso;
212         }
213         boolean ret = mEnabled && mCountriesEnabled.contains(iso);
214         Log.i(TAG, "isDynamicRoutingEnabled returns " + ret);
215         return ret;
216     }
217 
218     /**
219      * Returns emergency call routing that to be used for the given number.
220      * @param phone A {@link Phone} instance.
221      * @param number The dialed number.
222      * @param isNormal Indicates whether it is normal routing number.
223      * @param isAllowed Indicates whether it is allowed emergency number.
224      * @param needToTurnOnRadio Indicates whether it needs to turn on radio power.
225      */
getEmergencyCallRouting(Phone phone, String number, boolean isNormal, boolean isAllowed, boolean needToTurnOnRadio)226     public int getEmergencyCallRouting(Phone phone, String number,
227             boolean isNormal, boolean isAllowed, boolean needToTurnOnRadio) {
228         Log.i(TAG, "getEmergencyCallRouting isNormal=" + isNormal + ", isAllowed=" + isAllowed
229                 + ", needToTurnOnRadio=" + needToTurnOnRadio);
230         number = PhoneNumberUtils.stripSeparators(number);
231         boolean isDynamic = isDynamicNumber(phone, number);
232         if ((!isNormal && !isDynamic && isAllowed) || isFromNetworkOrSim(phone, number)) {
233             return EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN;
234         }
235         if (isDynamicRoutingEnabled(phone)) {
236             // If airplane mode is enabled, check the service state
237             // after turning on the radio power.
238             return (phone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE
239                     || needToTurnOnRadio)
240                     ? EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL
241                     : EmergencyNumber.EMERGENCY_CALL_ROUTING_EMERGENCY;
242         }
243         return EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL;
244     }
245 
isFromNetworkOrSim(Phone phone, String number)246     private boolean isFromNetworkOrSim(Phone phone, String number) {
247         if (phone == null) return false;
248         Log.i(TAG, "isFromNetworkOrSim phoneId=" + phone.getPhoneId());
249         if (phone.getEmergencyNumberTracker() == null) return false;
250         for (EmergencyNumber num : phone.getEmergencyNumberTracker().getEmergencyNumbers(
251                 number)) {
252             if (num.getNumber().equals(number)) {
253                 if (num.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_NETWORK_SIGNALING)
254                         || num.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_SIM)) {
255                     Log.i(TAG, "isFromNetworkOrSim SIM or NETWORK emergency number");
256                     return true;
257                 }
258             }
259         }
260         return false;
261     }
262 
getNetworkCountryIso(int phoneId)263     private String getNetworkCountryIso(int phoneId) {
264         String iso = mNetworkCountries.get(Integer.valueOf(phoneId));
265         if (TextUtils.isEmpty(iso)) {
266             iso = mLastCountryIso;
267         }
268         return iso;
269     }
270 
271     @VisibleForTesting
isDynamicNumber(Phone phone, String number)272     public boolean isDynamicNumber(Phone phone, String number) {
273         if (phone == null || phone.getEmergencyNumberTracker() == null
274                 || TextUtils.isEmpty(number)
275                 || mDynamicNumbers == null || mDynamicNumbers.isEmpty()) {
276             return false;
277         }
278 
279         List<EmergencyNumber> emergencyNumbers =
280                 mEmergencyNumbers.get(Integer.valueOf(phone.getPhoneId()));
281         if (emergencyNumbers == null) {
282             updateDynamicEmergencyNumbers(phone.getPhoneId());
283             emergencyNumbers =
284                     mEmergencyNumbers.get(Integer.valueOf(phone.getPhoneId()));
285         }
286         String iso = getNetworkCountryIso(phone.getPhoneId());
287         if (TextUtils.isEmpty(iso)
288                 || emergencyNumbers == null || emergencyNumbers.isEmpty()) {
289             return false;
290         }
291 
292         // Filter the list with the number.
293         List<EmergencyNumber> dynamicNumbers =
294                 getDynamicEmergencyNumbers(emergencyNumbers, number);
295 
296         // Compare the dynamicNumbers with the list of EmergencyNumber from EmergencyNumberTracker.
297         emergencyNumbers = phone.getEmergencyNumberTracker().getEmergencyNumbers(number);
298 
299         if (dynamicNumbers == null || emergencyNumbers == null
300                 || dynamicNumbers.isEmpty() || emergencyNumbers.isEmpty()) {
301             return false;
302         }
303 
304         if (DBG) {
305             Log.i(TAG, "isDynamicNumber " + emergencyNumbers);
306         }
307 
308         // Compare coutry ISO and MNC. MNC is optional.
309         for (EmergencyNumber dynamicNumber: dynamicNumbers) {
310             if (emergencyNumbers.stream().anyMatch(n ->
311                     TextUtils.equals(n.getCountryIso(), dynamicNumber.getCountryIso())
312                     && (TextUtils.equals(n.getMnc(), dynamicNumber.getMnc())
313                     || TextUtils.isEmpty(dynamicNumber.getMnc())))) {
314                 Log.i(TAG, "isDynamicNumber found");
315                 return true;
316             }
317         }
318         return false;
319     }
320 
321     /** Filter the list of {@link EmergencyNumber} with given number. */
getDynamicEmergencyNumbers( List<EmergencyNumber> emergencyNumbers, String number)322     private static List<EmergencyNumber> getDynamicEmergencyNumbers(
323             List<EmergencyNumber> emergencyNumbers, String number) {
324         List<EmergencyNumber> filteredNumbers = emergencyNumbers.stream()
325                 .filter(num -> num.getNumber().equals(number))
326                 .toList();
327 
328         if (DBG) {
329             Log.i(TAG, "getDynamicEmergencyNumbers " + filteredNumbers);
330         }
331         return filteredNumbers;
332     }
333 
334     /**
335      * Generates the lis of {@link EmergencyNumber} for the given phoneId
336      * based on the detected country from the resource configuration.
337      */
updateDynamicEmergencyNumbers(int phoneId)338     private void updateDynamicEmergencyNumbers(int phoneId) {
339         if (mDynamicNumbers == null || mDynamicNumbers.isEmpty()) {
340             // No resource configuration.
341             mEmergencyNumbers.put(Integer.valueOf(phoneId),
342                     new ArrayList<EmergencyNumber>());
343             return;
344         }
345 
346         String iso = getNetworkCountryIso(phoneId);
347         if (TextUtils.isEmpty(iso)) {
348             // Update again later.
349             return;
350         }
351         List<EmergencyNumber> emergencyNumbers = new ArrayList<EmergencyNumber>();
352         for (String numberInfo : mDynamicNumbers) {
353             if (!TextUtils.isEmpty(numberInfo) && numberInfo.startsWith(iso)) {
354                 emergencyNumbers.addAll(getEmergencyNumbers(numberInfo));
355             }
356         }
357         mEmergencyNumbers.put(Integer.valueOf(phoneId), emergencyNumbers);
358     }
359 
360     /** Returns an {@link EmergencyNumber} instance from the resource configuration. */
getEmergencyNumbers(String numberInfo)361     private List<EmergencyNumber> getEmergencyNumbers(String numberInfo) {
362         ArrayList<EmergencyNumber> emergencyNumbers = new ArrayList<EmergencyNumber>();
363         if (TextUtils.isEmpty(numberInfo)) {
364             return emergencyNumbers;
365         }
366 
367         String[] fields = numberInfo.split(",");
368         // Format: "iso,mnc,number1,number2,..."
369         if (fields == null || fields.length < 3
370                 || TextUtils.isEmpty(fields[0])
371                 || TextUtils.isEmpty(fields[2])) {
372             return emergencyNumbers;
373         }
374 
375         for (int i = 2; i < fields.length; i++) {
376             if (TextUtils.isEmpty(fields[i])) {
377                 continue;
378             }
379             emergencyNumbers.add(new EmergencyNumber(fields[i] /* number */,
380                 fields[0] /* iso */, fields[1] /* mnc */,
381                 EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED,
382                 new ArrayList<String>(),
383                 EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE,
384                 EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN));
385         }
386 
387         return emergencyNumbers;
388     }
389 }
390