• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2016, 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 package com.android.car.stream.telecom;
17 
18 import android.content.ContentResolver;
19 import android.content.ContentUris;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.database.Cursor;
23 import android.graphics.Bitmap;
24 import android.graphics.BitmapFactory;
25 import android.graphics.Rect;
26 import android.net.Uri;
27 import android.provider.ContactsContract;
28 import android.support.annotation.Nullable;
29 import android.support.annotation.WorkerThread;
30 import android.telecom.Call;
31 import android.telecom.GatewayInfo;
32 import android.telephony.PhoneNumberUtils;
33 import android.telephony.TelephonyManager;
34 import android.text.TextUtils;
35 import android.util.LruCache;
36 import com.android.car.apps.common.CircleBitmapDrawable;
37 import com.android.car.apps.common.LetterTileDrawable;
38 import com.android.car.stream.R;
39 
40 import java.io.InputStream;
41 import java.util.HashMap;
42 import java.util.Locale;
43 
44 /**
45  * Telecom related utility methods.
46  */
47 public class TelecomUtils {
48     private static final int LRU_CACHE_SIZE = 4194304; /** 4 mb **/
49 
50     private static final String[] CONTACT_ID_PROJECTION = new String[] {
51             ContactsContract.PhoneLookup.DISPLAY_NAME,
52             ContactsContract.PhoneLookup.TYPE,
53             ContactsContract.PhoneLookup.LABEL,
54             ContactsContract.PhoneLookup._ID
55     };
56 
57     private static String sVoicemailNumber;
58 
59     private static LruCache<String, Bitmap> sContactPhotoNumberCache;
60     private static LruCache<Long, Bitmap> sContactPhotoIdCache;
61     private static HashMap<String, String> sContactNameCache;
62     private static HashMap<String, Integer> sContactIdCache;
63     private static HashMap<String, String> sFormattedNumberCache;
64     private static HashMap<String, String> sDisplayNameCache;
65 
66     /**
67      * Create a round bitmap icon to represent the call. If a contact photo does not exist,
68      * a letter tile will be used instead.
69      */
createStreamCardSecondaryIcon(Context context, String number)70     public static Bitmap createStreamCardSecondaryIcon(Context context, String number) {
71         Resources res = context.getResources();
72         Bitmap largeIcon
73                 = TelecomUtils.getContactPhotoFromNumber(context.getContentResolver(), number);
74         if (largeIcon == null) {
75             LetterTileDrawable ltd = new LetterTileDrawable(res);
76             String name = TelecomUtils.getDisplayName(context, number);
77             ltd.setContactDetails(name, number);
78             ltd.setIsCircular(true);
79             int size = res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen);
80             largeIcon = ltd.toBitmap(size);
81         }
82 
83         return new CircleBitmapDrawable(res, largeIcon)
84                 .toBitmap(res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen));
85     }
86 
87 
88     /**
89      * Fetch contact photo by number from local cache.
90      *
91      * @param number
92      * @return Contact photo if it's in the cache, otherwise null.
93      */
94     @Nullable
getCachedContactPhotoFromNumber(String number)95     public static Bitmap getCachedContactPhotoFromNumber(String number) {
96         if (number == null) {
97             return null;
98         }
99 
100         if (sContactPhotoNumberCache == null) {
101             sContactPhotoNumberCache = new LruCache<String, Bitmap>(LRU_CACHE_SIZE) {
102                 @Override
103                 protected int sizeOf(String key, Bitmap value) {
104                     return value.getByteCount();
105                 }
106             };
107         }
108         return sContactPhotoNumberCache.get(number);
109     }
110 
111     @WorkerThread
getContactPhotoFromNumber(ContentResolver contentResolver, String number)112     public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) {
113         if (number == null) {
114             return null;
115         }
116 
117         Bitmap photo = getCachedContactPhotoFromNumber(number);
118         if (photo != null) {
119             return photo;
120         }
121 
122         int id = getContactIdFromNumber(contentResolver, number);
123         if (id == 0) {
124             return null;
125         }
126         photo = getContactPhotoFromId(contentResolver, id);
127         if (photo != null) {
128             sContactPhotoNumberCache.put(number, photo);
129         }
130         return photo;
131     }
132 
133     /**
134      * Return the contact id for the given contact id
135      * @param id the contact id to get the photo for
136      * @return the contact photo if it is found, null otherwise.
137      */
getContactPhotoFromId(ContentResolver contentResolver, long id)138     public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) {
139         if (sContactPhotoIdCache == null) {
140             sContactPhotoIdCache = new LruCache<Long, Bitmap>(LRU_CACHE_SIZE) {
141                 @Override
142                 protected int sizeOf(Long key, Bitmap value) {
143                     return value.getByteCount();
144                 }
145             };
146         } else if (sContactPhotoIdCache.get(id) != null) {
147             return sContactPhotoIdCache.get(id);
148         }
149 
150         Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
151         InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream(
152                 contentResolver, photoUri, true);
153 
154         BitmapFactory.Options options = new BitmapFactory.Options();
155         options.inPreferQualityOverSpeed = true;
156         // Scaling will be handled by later. We shouldn't scale multiple times to avoid
157         // quality lost due to multiple potential scaling up and down.
158         options.inScaled = false;
159 
160         Rect nullPadding = null;
161         Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options);
162         if (photo != null) {
163             photo.setDensity(Bitmap.DENSITY_NONE);
164             sContactPhotoIdCache.put(id, photo);
165         }
166         return photo;
167     }
168 
169     /**
170      * Return the contact id for the given phone number.
171      * @param number Caller phone number
172      * @return the contact id if it is found, 0 otherwise.
173      */
getContactIdFromNumber(ContentResolver cr, String number)174     public static int getContactIdFromNumber(ContentResolver cr, String number) {
175         if (number == null || number.isEmpty()) {
176             return 0;
177         }
178         if (sContactIdCache == null) {
179             sContactIdCache = new HashMap<>();
180         } else if (sContactIdCache.containsKey(number)) {
181             return sContactIdCache.get(number);
182         }
183 
184         Uri uri = Uri.withAppendedPath(
185                 ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
186                 Uri.encode(number));
187         Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
188 
189         try {
190             if (cursor != null && cursor.moveToFirst()) {
191                 int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID));
192                 sContactIdCache.put(number, id);
193                 return id;
194             }
195         }
196         finally {
197             if (cursor != null) {
198                 cursor.close();
199             }
200         }
201         return 0;
202     }
203 
getDisplayName(Context context, String number)204     public static String getDisplayName(Context context, String number) {
205         return getDisplayName(context, number, (Uri)null);
206     }
207 
getDisplayName(Context context, Call call)208     public static String getDisplayName(Context context, Call call) {
209         // A call might get created before its children are added. In that case, the display name
210         // would go from "Unknown" to "Conference call" therefore we don't want to cache it.
211         if (call.getChildren() != null && call.getChildren().size() > 0) {
212             return context.getString(R.string.conference_call);
213         }
214         return getDisplayName(context, getNumber(call), getGatewayInfoOriginalAddress(call));
215     }
216 
getGatewayInfoOriginalAddress(Call call)217     private static Uri getGatewayInfoOriginalAddress(Call call) {
218         if (call == null || call.getDetails() == null) {
219             return null;
220         }
221         GatewayInfo gatewayInfo = call.getDetails().getGatewayInfo();
222 
223         if (gatewayInfo != null && gatewayInfo.getOriginalAddress() != null) {
224             return gatewayInfo.getGatewayAddress();
225         }
226         return null;
227     }
228 
229     /**
230      * Return the phone number of the call. This CAN return null under certain circumstances such
231      * as if the incoming number is hidden.
232      */
getNumber(Call call)233     public static String getNumber(Call call) {
234         if (call == null || call.getDetails() == null) {
235             return null;
236         }
237 
238         Uri gatewayInfoOriginalAddress = getGatewayInfoOriginalAddress(call);
239         if (gatewayInfoOriginalAddress != null) {
240             return gatewayInfoOriginalAddress.getSchemeSpecificPart();
241         }
242 
243         if (call.getDetails().getHandle() != null) {
244             return call.getDetails().getHandle().getSchemeSpecificPart();
245         }
246         return null;
247     }
248 
getContactNameFromNumber(ContentResolver cr, String number)249     private static String getContactNameFromNumber(ContentResolver cr, String number) {
250         if (sContactNameCache == null) {
251             sContactNameCache = new HashMap<>();
252         } else if (sContactNameCache.containsKey(number)) {
253             return sContactNameCache.get(number);
254         }
255 
256         Uri uri = Uri.withAppendedPath(
257                 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
258 
259         Cursor cursor = null;
260         String name = null;
261         try {
262             cursor = cr.query(uri,
263                     new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null);
264             if (cursor != null && cursor.moveToFirst()) {
265                 name = cursor.getString(0);
266                 sContactNameCache.put(number, name);
267             }
268         } finally {
269             if (cursor != null) {
270                 cursor.close();
271             }
272         }
273         return name;
274     }
275 
getDisplayName( Context context, String number, Uri gatewayOriginalAddress)276     private static String getDisplayName(
277             Context context, String number, Uri gatewayOriginalAddress) {
278         if (sDisplayNameCache == null) {
279             sDisplayNameCache = new HashMap<>();
280         } else {
281             if (sDisplayNameCache.containsKey(number)) {
282                 return sDisplayNameCache.get(number);
283             }
284         }
285 
286         if (TextUtils.isEmpty(number)) {
287             return context.getString(R.string.unknown);
288         }
289         ContentResolver cr = context.getContentResolver();
290         String name;
291         if (number.equals(getVoicemailNumber(context))) {
292             name = context.getString(R.string.voicemail);
293         } else {
294             name = getContactNameFromNumber(cr, number);
295         }
296 
297         if (name == null) {
298             name = getFormattedNumber(context, number);
299         }
300         if (name == null && gatewayOriginalAddress != null) {
301             name = gatewayOriginalAddress.getSchemeSpecificPart();
302         }
303         if (name == null) {
304             name = context.getString(R.string.unknown);
305         }
306         sDisplayNameCache.put(number, name);
307         return name;
308     }
309 
getVoicemailNumber(Context context)310     public static String getVoicemailNumber(Context context) {
311         if (sVoicemailNumber == null) {
312             TelephonyManager tm =
313                     (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
314             sVoicemailNumber = tm.getVoiceMailNumber();
315         }
316         return sVoicemailNumber;
317     }
318 
getFormattedNumber(Context context, @Nullable String number)319     public static String getFormattedNumber(Context context, @Nullable String number) {
320         if (TextUtils.isEmpty(number)) {
321             return "";
322         }
323 
324         if (sFormattedNumberCache == null) {
325             sFormattedNumberCache = new HashMap<>();
326         } else {
327             if (sFormattedNumberCache.containsKey(number)) {
328                 return sFormattedNumberCache.get(number);
329             }
330         }
331 
332         String countryIso = getSimRegionCode(context);
333         String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
334         String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
335         formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
336         sFormattedNumberCache.put(number, formattedNumber);
337         return formattedNumber;
338     }
339 
340     /**
341      * Wrapper around TelephonyManager.getSimCountryIso() that will fallback to locale or USA ISOs
342      * if it finds bogus data.
343      */
getSimRegionCode(Context context)344     private static String getSimRegionCode(Context context) {
345         TelephonyManager telephonyManager =
346                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
347 
348         // This can be null on some phones (and is null on robolectric default TelephonyManager)
349         String countryIso = telephonyManager.getSimCountryIso();
350         if (TextUtils.isEmpty(countryIso) || countryIso.length() != 2) {
351             countryIso = Locale.getDefault().getCountry();
352             if (countryIso == null || countryIso.length() != 2) {
353                 countryIso = "US";
354             }
355         }
356 
357         return countryIso.toUpperCase(Locale.US);
358     }
359 }