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