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