1 /* 2 * Copyright (C) 2015 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.dialer.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.BitmapFactory.Options; 26 import android.graphics.Rect; 27 import android.net.Uri; 28 import android.provider.ContactsContract; 29 import android.provider.ContactsContract.CommonDataKinds.Phone; 30 import android.provider.ContactsContract.PhoneLookup; 31 import android.provider.Settings; 32 import android.support.annotation.Nullable; 33 import android.support.annotation.WorkerThread; 34 import android.telecom.Call; 35 import android.telephony.PhoneNumberUtils; 36 import android.telephony.TelephonyManager; 37 import android.text.TextUtils; 38 import android.text.format.DateUtils; 39 import android.util.Log; 40 import android.widget.ImageView; 41 42 import com.android.car.apps.common.LetterTileDrawable; 43 import com.android.car.dialer.R; 44 import com.android.car.dialer.ui.CircleBitmapDrawable; 45 46 import java.io.InputStream; 47 import java.util.Locale; 48 49 public class TelecomUtils { 50 private final static String TAG = "Em.TelecomUtils"; 51 52 private static final String[] CONTACT_ID_PROJECTION = new String[]{ 53 ContactsContract.PhoneLookup.DISPLAY_NAME, 54 ContactsContract.PhoneLookup.TYPE, 55 ContactsContract.PhoneLookup.LABEL, 56 ContactsContract.PhoneLookup._ID 57 }; 58 59 private static String sVoicemailNumber; 60 private static TelephonyManager sTelephonyManager; 61 62 @WorkerThread getContactPhotoFromNumber(ContentResolver contentResolver, String number)63 public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) { 64 if (number == null) { 65 return null; 66 } 67 68 int id = getContactIdFromNumber(contentResolver, number); 69 if (id == 0) { 70 return null; 71 } 72 return getContactPhotoFromId(contentResolver, id); 73 } 74 75 /** 76 * Return the contact id for the given contact id 77 * 78 * @param id the contact id to get the photo for 79 * @return the contact photo if it is found, null otherwise. 80 */ getContactPhotoFromId(ContentResolver contentResolver, long id)81 public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) { 82 Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); 83 InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream( 84 contentResolver, photoUri, true); 85 86 Options options = new Options(); 87 options.inPreferQualityOverSpeed = true; 88 // Scaling will be handled by later. We shouldn't scale multiple times to avoid 89 // quality lost due to multiple potential scaling up and down. 90 options.inScaled = false; 91 92 Rect nullPadding = null; 93 Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options); 94 if (photo != null) { 95 photo.setDensity(Bitmap.DENSITY_NONE); 96 } 97 return photo; 98 } 99 100 /** 101 * Return the contact id for the given phone number. 102 * 103 * @param number Caller phone number 104 * @return the contact id if it is found, 0 otherwise. 105 */ getContactIdFromNumber(ContentResolver cr, String number)106 public static int getContactIdFromNumber(ContentResolver cr, String number) { 107 if (number == null || number.isEmpty()) { 108 return 0; 109 } 110 111 Uri uri = Uri.withAppendedPath( 112 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 113 Uri.encode(number)); 114 Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null); 115 116 try { 117 if (cursor != null && cursor.moveToFirst()) { 118 int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); 119 return id; 120 } 121 } finally { 122 if (cursor != null) { 123 cursor.close(); 124 } 125 } 126 return 0; 127 } 128 129 /** 130 * Return the label for the given phone number. 131 * 132 * @param number Caller phone number 133 * @return the label if it is found, 0 otherwise. 134 */ getTypeFromNumber(Context context, String number)135 public static CharSequence getTypeFromNumber(Context context, String number) { 136 if (Log.isLoggable(TAG, Log.DEBUG)) { 137 Log.d(TAG, "getTypeFromNumber, number: " + number); 138 } 139 String defaultLabel = ""; 140 if (number == null || number.isEmpty()) { 141 return defaultLabel; 142 } 143 144 ContentResolver cr = context.getContentResolver(); 145 Resources res = context.getResources(); 146 Uri uri = Uri.withAppendedPath( 147 PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); 148 Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null); 149 150 try { 151 if (cursor != null && cursor.moveToFirst()) { 152 int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE); 153 int type = cursor.getInt(typeColumn); 154 int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL); 155 String label = cursor.getString(labelColumn); 156 CharSequence typeLabel = 157 Phone.getTypeLabel(res, type, label); 158 return typeLabel; 159 } 160 } finally { 161 if (cursor != null) { 162 cursor.close(); 163 } 164 } 165 return defaultLabel; 166 } 167 getVoicemailNumber(Context context)168 public static String getVoicemailNumber(Context context) { 169 if (sVoicemailNumber == null) { 170 sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber(); 171 } 172 return sVoicemailNumber; 173 } 174 getTelephonyManager(Context context)175 public static TelephonyManager getTelephonyManager(Context context) { 176 if (sTelephonyManager == null) { 177 sTelephonyManager = 178 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 179 } 180 return sTelephonyManager; 181 } 182 getFormattedNumber(Context context, String number)183 public static String getFormattedNumber(Context context, String number) { 184 if (Log.isLoggable(TAG, Log.DEBUG)) { 185 Log.d(TAG, "getFormattedNumber: " + number); 186 } 187 if (number == null) { 188 return ""; 189 } 190 191 String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US); 192 if (countryIso.length() != 2) { 193 countryIso = Locale.getDefault().getCountry(); 194 if (countryIso == null || countryIso.length() != 2) { 195 countryIso = "US"; 196 } 197 } 198 if (Log.isLoggable(TAG, Log.DEBUG)) { 199 Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: " 200 + number + ", country: " + countryIso); 201 } 202 String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso); 203 String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso); 204 formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber; 205 if (Log.isLoggable(TAG, Log.DEBUG)) { 206 Log.d(TAG, "getFormattedNumber, result: " + formattedNumber); 207 } 208 return formattedNumber; 209 } 210 getDisplayName(Context context, UiCall call)211 public static String getDisplayName(Context context, UiCall call) { 212 // A call might get created before its children are added. In that case, the display name 213 // would go from "Unknown" to "Conference call" therefore we don't want to cache it. 214 if (call.hasChildren()) { 215 return context.getString(R.string.conference_call); 216 } 217 218 return getDisplayName(context, call.getNumber(), call.getGatewayInfoOriginalAddress()); 219 } 220 getDisplayName(Context context, String number)221 public static String getDisplayName(Context context, String number) { 222 return getDisplayName(context, number, null); 223 } 224 getDisplayName(Context context, String number, Uri gatewayOriginalAddress)225 private static String getDisplayName(Context context, String number, 226 Uri gatewayOriginalAddress) { 227 if (Log.isLoggable(TAG, Log.DEBUG)) { 228 Log.d(TAG, "getDisplayName: " + number 229 + ", gatewayOriginalAddress: " + gatewayOriginalAddress); 230 } 231 232 if (TextUtils.isEmpty(number)) { 233 return context.getString(R.string.unknown); 234 } 235 ContentResolver cr = context.getContentResolver(); 236 String name; 237 if (number.equals(getVoicemailNumber(context))) { 238 name = context.getResources().getString(R.string.voicemail); 239 } else { 240 name = getContactNameFromNumber(cr, number); 241 } 242 243 if (name == null) { 244 name = getFormattedNumber(context, number); 245 } 246 if (name == null && gatewayOriginalAddress != null) { 247 name = gatewayOriginalAddress.getSchemeSpecificPart(); 248 } 249 if (name == null) { 250 name = context.getString(R.string.unknown); 251 } 252 return name; 253 } 254 getContactNameFromNumber(ContentResolver cr, String number)255 private static String getContactNameFromNumber(ContentResolver cr, String number) { 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 } 267 } finally { 268 if (cursor != null) { 269 cursor.close(); 270 } 271 } 272 return name; 273 } 274 275 /** 276 * @return A formatted string that has information about the phone call 277 * Possible strings: 278 * "Mobile · Dialing" 279 * "Mobile · 1:05" 280 * "Bluetooth disconnected" 281 */ getCallInfoText(Context context, UiCall call, CharSequence label)282 public static String getCallInfoText(Context context, UiCall call, CharSequence label) { 283 String text; 284 if (call.getState() == Call.STATE_ACTIVE) { 285 long duration = System.currentTimeMillis() - call.getConnectTimeMillis(); 286 String durationString = DateUtils.formatElapsedTime(duration / 1000); 287 if (!TextUtils.isEmpty(durationString) && !TextUtils.isEmpty(label)) { 288 text = context.getString(R.string.phone_label_with_info, label, durationString); 289 } else if (!TextUtils.isEmpty(durationString)) { 290 text = durationString; 291 } else if (!TextUtils.isEmpty(label)) { 292 text = (String) label; 293 } else { 294 text = ""; 295 } 296 } else { 297 String state = callStateToUiString(context, call.getState()); 298 if (!TextUtils.isEmpty(label)) { 299 text = context.getString(R.string.phone_label_with_info, label, state); 300 } else { 301 text = state; 302 } 303 } 304 return text; 305 } 306 307 /** 308 * @return A string representation of the call state that can be presented to a user. 309 */ callStateToUiString(Context context, int state)310 public static String callStateToUiString(Context context, int state) { 311 Resources res = context.getResources(); 312 switch (state) { 313 case Call.STATE_ACTIVE: 314 return res.getString(R.string.call_state_call_active); 315 case Call.STATE_HOLDING: 316 return res.getString(R.string.call_state_hold); 317 case Call.STATE_NEW: 318 case Call.STATE_CONNECTING: 319 return res.getString(R.string.call_state_connecting); 320 case Call.STATE_SELECT_PHONE_ACCOUNT: 321 case Call.STATE_DIALING: 322 return res.getString(R.string.call_state_dialing); 323 case Call.STATE_DISCONNECTED: 324 return res.getString(R.string.call_state_call_ended); 325 case Call.STATE_RINGING: 326 return res.getString(R.string.call_state_call_ringing); 327 case Call.STATE_DISCONNECTING: 328 return res.getString(R.string.call_state_call_ending); 329 default: 330 throw new IllegalStateException("Unknown Call State: " + state); 331 } 332 } 333 isNetworkAvailable(Context context)334 public static boolean isNetworkAvailable(Context context) { 335 TelephonyManager tm = 336 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 337 return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN && 338 tm.getSimState() == TelephonyManager.SIM_STATE_READY; 339 } 340 isAirplaneModeOn(Context context)341 public static boolean isAirplaneModeOn(Context context) { 342 return Settings.System.getInt(context.getContentResolver(), 343 Settings.Global.AIRPLANE_MODE_ON, 0) != 0; 344 } 345 346 /** 347 * Sets a Contact bitmap on the provided image taking into account fail cases. 348 * It will attempt to load a Bitmap from the Contacts store, otherwise it will paint 349 * a the first letter of the contact name. 350 * 351 * @param number A key to have a consisten color per phone number. 352 * @return A worker task if a new one was needed to load the bitmap. 353 */ 354 @Nullable setContactBitmapAsync(Context context, final ImageView icon, final @Nullable String name, final String number)355 public static ContactBitmapWorker setContactBitmapAsync(Context context, 356 final ImageView icon, final @Nullable String name, final String number) { 357 return ContactBitmapWorker.loadBitmap(context.getContentResolver(), icon, number, 358 bitmap -> { 359 Resources r = icon.getResources(); 360 if (bitmap != null) { 361 icon.setScaleType(ImageView.ScaleType.CENTER_CROP); 362 icon.setImageDrawable(new CircleBitmapDrawable(r, bitmap)); 363 } else { 364 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 365 LetterTileDrawable letterTileDrawable = new LetterTileDrawable(r); 366 letterTileDrawable.setContactDetails(name, number); 367 letterTileDrawable.setIsCircular(true); 368 icon.setImageDrawable(letterTileDrawable); 369 } 370 }); 371 } 372 373 } 374