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