• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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