1 /* 2 * Copyright (C) 2019 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.phone; 18 19 import android.content.Context; 20 import android.os.PersistableBundle; 21 import android.provider.Settings; 22 import android.telecom.PhoneAccount; 23 import android.telecom.PhoneAccountHandle; 24 import android.telecom.TelecomManager; 25 import android.telephony.CarrierConfigManager; 26 import android.telephony.SubscriptionManager; 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 androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 36 import com.android.internal.telephony.util.ArrayUtils; 37 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Map; 41 42 class ShortcutViewUtils { 43 private static final String LOG_TAG = "ShortcutViewUtils"; 44 45 // Emergency services which will be promoted on the shortcut view. 46 static final int[] PROMOTED_CATEGORIES = { 47 EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE, 48 EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_AMBULANCE, 49 EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_FIRE_BRIGADE, 50 }; 51 52 static final int PROMOTED_CATEGORIES_BITMASK; 53 54 static { 55 int bitmask = 0; 56 for (int category : PROMOTED_CATEGORIES) { 57 bitmask |= category; 58 } 59 PROMOTED_CATEGORIES_BITMASK = bitmask; 60 } 61 62 static class Config { 63 private final boolean mCanEnableShortcutView; 64 private PhoneInfo mPhoneInfo = null; 65 Config(@onNull Context context, PersistableBundle carrierConfig, int entryType)66 Config(@NonNull Context context, PersistableBundle carrierConfig, int entryType) { 67 mCanEnableShortcutView = canEnableShortcutView(carrierConfig, entryType); 68 refresh(context); 69 } 70 refresh(@onNull Context context)71 void refresh(@NonNull Context context) { 72 if (mCanEnableShortcutView && !isAirplaneModeOn(context)) { 73 mPhoneInfo = ShortcutViewUtils.pickPreferredPhone(context); 74 } else { 75 mPhoneInfo = null; 76 } 77 } 78 isEnabled()79 boolean isEnabled() { 80 return mPhoneInfo != null; 81 } 82 getPhoneInfo()83 PhoneInfo getPhoneInfo() { 84 return mPhoneInfo; 85 } 86 getCountryIso()87 String getCountryIso() { 88 if (mPhoneInfo == null) { 89 return null; 90 } 91 return mPhoneInfo.getCountryIso(); 92 } 93 hasPromotedEmergencyNumber(String number)94 boolean hasPromotedEmergencyNumber(String number) { 95 if (mPhoneInfo == null) { 96 return false; 97 } 98 return mPhoneInfo.hasPromotedEmergencyNumber(number); 99 } 100 canEnableShortcutView(PersistableBundle carrierConfig, int entryType)101 private boolean canEnableShortcutView(PersistableBundle carrierConfig, int entryType) { 102 if (entryType != EmergencyDialer.ENTRY_TYPE_POWER_MENU) { 103 Log.d(LOG_TAG, "Disables shortcut view since it's not launched from power menu"); 104 return false; 105 } 106 if (carrierConfig == null || !carrierConfig.getBoolean( 107 CarrierConfigManager.KEY_SUPPORT_EMERGENCY_DIALER_SHORTCUT_BOOL)) { 108 Log.d(LOG_TAG, "Disables shortcut view by carrier requirement"); 109 return false; 110 } 111 return true; 112 } 113 isAirplaneModeOn(@onNull Context context)114 private boolean isAirplaneModeOn(@NonNull Context context) { 115 return Settings.Global.getInt(context.getContentResolver(), 116 Settings.Global.AIRPLANE_MODE_ON, 0) != 0; 117 } 118 } 119 120 // Info and emergency call capability of every phone. 121 static class PhoneInfo { 122 private final PhoneAccountHandle mHandle; 123 private final boolean mCanPlaceEmergencyCall; 124 private final int mSubId; 125 private final String mCountryIso; 126 private final List<EmergencyNumber> mPromotedEmergencyNumbers; 127 PhoneInfo(int subId, String countryIso, List<EmergencyNumber> promotedEmergencyNumbers)128 private PhoneInfo(int subId, String countryIso, 129 List<EmergencyNumber> promotedEmergencyNumbers) { 130 this(null, true, subId, countryIso, promotedEmergencyNumbers); 131 } 132 PhoneInfo(PhoneAccountHandle handle, boolean canPlaceEmergencyCall, int subId, String countryIso, List<EmergencyNumber> promotedEmergencyNumbers)133 private PhoneInfo(PhoneAccountHandle handle, boolean canPlaceEmergencyCall, int subId, 134 String countryIso, List<EmergencyNumber> promotedEmergencyNumbers) { 135 mHandle = handle; 136 mCanPlaceEmergencyCall = canPlaceEmergencyCall; 137 mSubId = subId; 138 mCountryIso = countryIso; 139 mPromotedEmergencyNumbers = promotedEmergencyNumbers; 140 } 141 getPhoneAccountHandle()142 public PhoneAccountHandle getPhoneAccountHandle() { 143 return mHandle; 144 } 145 canPlaceEmergencyCall()146 public boolean canPlaceEmergencyCall() { 147 return mCanPlaceEmergencyCall; 148 } 149 getSubId()150 public int getSubId() { 151 return mSubId; 152 } 153 getCountryIso()154 public String getCountryIso() { 155 return mCountryIso; 156 } 157 getPromotedEmergencyNumbers()158 public List<EmergencyNumber> getPromotedEmergencyNumbers() { 159 return mPromotedEmergencyNumbers; 160 } 161 isSufficientForEmergencyCall(@onNull Context context)162 public boolean isSufficientForEmergencyCall(@NonNull Context context) { 163 // Checking mCountryIso because the emergency number list is not reliable to be 164 // suggested to users if the device didn't camp to any network. In this case, users 165 // can still try to dial emergency numbers with dial pad. 166 return mCanPlaceEmergencyCall && mPromotedEmergencyNumbers != null 167 && isSupportedCountry(context, mCountryIso); 168 } 169 hasPromotedEmergencyNumber(String number)170 public boolean hasPromotedEmergencyNumber(String number) { 171 for (EmergencyNumber emergencyNumber : mPromotedEmergencyNumbers) { 172 if (emergencyNumber.getNumber().equalsIgnoreCase(number)) { 173 return true; 174 } 175 } 176 return false; 177 } 178 179 @Override toString()180 public String toString() { 181 StringBuilder sb = new StringBuilder(); 182 sb.append("{"); 183 if (mHandle != null) { 184 sb.append("handle=").append(mHandle.getId()).append(", "); 185 } 186 sb.append("subId=").append(mSubId) 187 .append(", canPlaceEmergencyCall=").append(mCanPlaceEmergencyCall) 188 .append(", networkCountryIso=").append(mCountryIso); 189 if (mPromotedEmergencyNumbers != null) { 190 sb.append(", emergencyNumbers="); 191 for (EmergencyNumber emergencyNumber : mPromotedEmergencyNumbers) { 192 sb.append(emergencyNumber.getNumber()).append(":") 193 .append(emergencyNumber).append(","); 194 } 195 } 196 sb.append("}"); 197 return sb.toString(); 198 } 199 } 200 201 /** 202 * Picks a preferred phone (SIM slot) which is sufficient for emergency call and can provide 203 * promoted emergency numbers. 204 * 205 * A promoted emergency number should be dialed out over the preferred phone. Other emergency 206 * numbers should be still dialable over the system default phone. 207 * 208 * @return A preferred phone and its promoted emergency number, or null if no phone/promoted 209 * emergency numbers available. 210 */ 211 @Nullable pickPreferredPhone(@onNull Context context)212 static PhoneInfo pickPreferredPhone(@NonNull Context context) { 213 TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); 214 if (telephonyManager.getPhoneCount() <= 0) { 215 Log.w(LOG_TAG, "No phone available!"); 216 return null; 217 } 218 219 Map<Integer, List<EmergencyNumber>> promotedLists = 220 getPromotedEmergencyNumberLists(telephonyManager); 221 if (promotedLists == null || promotedLists.isEmpty()) { 222 return null; 223 } 224 225 // For a multi-phone device, tries the default phone account. 226 TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 227 PhoneAccountHandle defaultHandle = telecomManager.getDefaultOutgoingPhoneAccount( 228 PhoneAccount.SCHEME_TEL); 229 if (defaultHandle != null) { 230 PhoneInfo phone = loadPhoneInfo(context, defaultHandle, telephonyManager, 231 telecomManager, promotedLists); 232 if (phone.isSufficientForEmergencyCall(context)) { 233 return phone; 234 } 235 Log.w(LOG_TAG, "Default PhoneAccount is insufficient for emergency call: " 236 + phone.toString()); 237 } else { 238 Log.w(LOG_TAG, "Missing default PhoneAccount! Is this really a phone device?"); 239 } 240 241 // Looks for any one phone which supports emergency call. 242 List<PhoneAccountHandle> allHandles = telecomManager.getCallCapablePhoneAccounts(); 243 if (allHandles != null && !allHandles.isEmpty()) { 244 for (PhoneAccountHandle handle : allHandles) { 245 PhoneInfo phone = loadPhoneInfo(context, handle, telephonyManager, telecomManager, 246 promotedLists); 247 if (phone.isSufficientForEmergencyCall(context)) { 248 return phone; 249 } else { 250 if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { 251 Log.d(LOG_TAG, "PhoneAccount " + phone.toString() 252 + " is insufficient for emergency call."); 253 } 254 } 255 } 256 } 257 258 Log.w(LOG_TAG, "No PhoneAccount available for emergency call!"); 259 return null; 260 } 261 isSupportedCountry(@onNull Context context, String countryIso)262 private static boolean isSupportedCountry(@NonNull Context context, String countryIso) { 263 if (TextUtils.isEmpty(countryIso)) { 264 return false; 265 } 266 267 String[] countrysToEnableShortcutView = context.getResources().getStringArray( 268 R.array.config_countries_to_enable_shortcut_view); 269 for (String supportedCountry : countrysToEnableShortcutView) { 270 if (countryIso.equalsIgnoreCase(supportedCountry)) { 271 return true; 272 } 273 } 274 return false; 275 } 276 loadPhoneInfo( @onNull Context context, @NonNull PhoneAccountHandle handle, @NonNull TelephonyManager telephonyManager, @NonNull TelecomManager telecomManager, Map<Integer, List<EmergencyNumber>> promotedLists)277 private static PhoneInfo loadPhoneInfo( 278 @NonNull Context context, 279 @NonNull PhoneAccountHandle handle, 280 @NonNull TelephonyManager telephonyManager, 281 @NonNull TelecomManager telecomManager, 282 Map<Integer, List<EmergencyNumber>> promotedLists) { 283 boolean canPlaceEmergencyCall = false; 284 int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 285 String countryIso = null; 286 List<EmergencyNumber> emergencyNumberList = null; 287 288 PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle); 289 if (phoneAccount != null) { 290 canPlaceEmergencyCall = phoneAccount.hasCapabilities( 291 PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS); 292 subId = telephonyManager.getSubIdForPhoneAccount(phoneAccount); 293 } 294 295 TelephonyManager subTelephonyManager = telephonyManager.createForSubscriptionId(subId); 296 if (subTelephonyManager != null) { 297 countryIso = subTelephonyManager.getNetworkCountryIso(); 298 } 299 300 if (promotedLists != null) { 301 emergencyNumberList = removeCarrierSpecificPrefixes(context, subId, 302 promotedLists.get(subId)); 303 } 304 305 return new PhoneInfo(handle, canPlaceEmergencyCall, subId, countryIso, emergencyNumberList); 306 } 307 308 @Nullable getCarrierSpecificPrefixes(@onNull Context context, int subId)309 private static String[] getCarrierSpecificPrefixes(@NonNull Context context, int subId) { 310 CarrierConfigManager configMgr = context.getSystemService(CarrierConfigManager.class); 311 if (configMgr == null) { 312 return null; 313 } 314 PersistableBundle b = configMgr.getConfigForSubId(subId); 315 return b == null ? null : b.getStringArray( 316 CarrierConfigManager.KEY_EMERGENCY_NUMBER_PREFIX_STRING_ARRAY); 317 } 318 319 // Removes carrier specific emergency number prefixes (if there is any) from every emergency 320 // number and create a new list without duplications. Returns the original list if there is no 321 // prefixes. 322 @NonNull removeCarrierSpecificPrefixes( @onNull Context context, int subId, @NonNull List<EmergencyNumber> emergencyNumberList)323 private static List<EmergencyNumber> removeCarrierSpecificPrefixes( 324 @NonNull Context context, 325 int subId, 326 @NonNull List<EmergencyNumber> emergencyNumberList) { 327 String[] prefixes = getCarrierSpecificPrefixes(context, subId); 328 if (ArrayUtils.isEmpty(prefixes)) { 329 return emergencyNumberList; 330 } 331 332 List<EmergencyNumber> newList = new ArrayList<>(emergencyNumberList.size()); 333 for (EmergencyNumber emergencyNumber : emergencyNumberList) { 334 // If no prefix was removed from emergencyNumber, add it to the newList directly. 335 EmergencyNumber newNumber = emergencyNumber; 336 String number = emergencyNumber.getNumber(); 337 for (String prefix : prefixes) { 338 // If emergencyNumber starts with this prefix, remove this prefix to retrieve the 339 // actual emergency number. 340 // However, if emergencyNumber is exactly the same with this prefix, it could be 341 // either a real emergency number, or composed with another prefix. It shouldn't be 342 // processed with this prefix whatever. 343 if (!TextUtils.isEmpty(prefix) && number.startsWith(prefix) 344 && !number.equals(prefix)) { 345 newNumber = new EmergencyNumber( 346 number.substring(prefix.length()), 347 emergencyNumber.getCountryIso(), 348 emergencyNumber.getMnc(), 349 emergencyNumber.getEmergencyServiceCategoryBitmask(), 350 emergencyNumber.getEmergencyUrns(), 351 emergencyNumber.getEmergencyNumberSourceBitmask(), 352 emergencyNumber.getEmergencyCallRouting()); 353 // There should not be more than one prefix attached to a number. 354 break; 355 } 356 } 357 if (!newList.contains(newNumber)) { 358 newList.add(newNumber); 359 } 360 } 361 return newList; 362 } 363 364 @NonNull getPromotedEmergencyNumberLists( @onNull TelephonyManager telephonyManager)365 private static Map<Integer, List<EmergencyNumber>> getPromotedEmergencyNumberLists( 366 @NonNull TelephonyManager telephonyManager) { 367 Map<Integer, List<EmergencyNumber>> allLists = 368 telephonyManager.getEmergencyNumberList(); 369 if (allLists == null || allLists.isEmpty()) { 370 Log.w(LOG_TAG, "Unable to retrieve emergency number lists!"); 371 return new ArrayMap<>(); 372 } 373 374 boolean isDebugLoggable = Log.isLoggable(LOG_TAG, Log.DEBUG); 375 Map<Integer, List<EmergencyNumber>> promotedEmergencyNumberLists = new ArrayMap<>(); 376 for (Map.Entry<Integer, List<EmergencyNumber>> entry : allLists.entrySet()) { 377 if (entry.getKey() == null || entry.getValue() == null) { 378 continue; 379 } 380 List<EmergencyNumber> emergencyNumberList = entry.getValue(); 381 if (isDebugLoggable) { 382 Log.d(LOG_TAG, "Emergency numbers of " + entry.getKey()); 383 } 384 385 // The list of promoted emergency numbers which will be visible on shortcut view. 386 List<EmergencyNumber> promotedList = new ArrayList<>(); 387 // A temporary list for non-prioritized emergency numbers. 388 List<EmergencyNumber> tempList = new ArrayList<>(); 389 390 for (EmergencyNumber emergencyNumber : emergencyNumberList) { 391 boolean isPromotedCategory = (emergencyNumber.getEmergencyServiceCategoryBitmask() 392 & PROMOTED_CATEGORIES_BITMASK) != 0; 393 394 // Emergency numbers in DATABASE are prioritized for shortcut view since they were 395 // well-categorized. 396 boolean isFromPrioritizedSource = 397 (emergencyNumber.getEmergencyNumberSourceBitmask() 398 & EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE) != 0; 399 if (isDebugLoggable) { 400 Log.d(LOG_TAG, " " + emergencyNumber 401 + (isPromotedCategory ? "M" : "") 402 + (isFromPrioritizedSource ? "P" : "")); 403 } 404 405 if (isPromotedCategory) { 406 if (isFromPrioritizedSource) { 407 promotedList.add(emergencyNumber); 408 } else { 409 tempList.add(emergencyNumber); 410 } 411 } 412 } 413 // Puts numbers in temp list after prioritized numbers. 414 promotedList.addAll(tempList); 415 416 if (!promotedList.isEmpty()) { 417 promotedEmergencyNumberLists.put(entry.getKey(), promotedList); 418 } 419 } 420 421 if (promotedEmergencyNumberLists.isEmpty()) { 422 Log.w(LOG_TAG, "No promoted emergency number found!"); 423 } 424 return promotedEmergencyNumberLists; 425 } 426 } 427