1 /* 2 * Copyright (C) 2017 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.dialer.oem; 17 18 import android.annotation.TargetApi; 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.database.ContentObserver; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.os.Build.VERSION_CODES; 25 import android.support.annotation.AnyThread; 26 import android.support.annotation.NonNull; 27 import android.support.annotation.Nullable; 28 import android.support.annotation.WorkerThread; 29 import android.telephony.PhoneNumberUtils; 30 import android.text.TextUtils; 31 import com.android.dialer.common.Assert; 32 import com.android.dialer.common.ConfigProviderBindings; 33 import com.android.dialer.common.LogUtil; 34 import com.android.dialer.util.PermissionsUtil; 35 import java.util.concurrent.ConcurrentHashMap; 36 37 /** 38 * Cequint Caller ID manager to provide caller information. 39 * 40 * <p>This is only enabled on Motorola devices for Sprint. 41 * 42 * <p>If it's enabled, this class will be called by call log and incall to get caller info from 43 * Cequint Caller ID. It also caches any information fetched in static map, which lives through 44 * whole application lifecycle. 45 */ 46 @TargetApi(VERSION_CODES.M) 47 public class CequintCallerIdManager { 48 49 private static final String CONFIG_CALLER_ID_ENABLED = "config_caller_id_enabled"; 50 51 private static final String PROVIDER_NAME = "com.cequint.ecid"; 52 53 private static final Uri CONTENT_URI = Uri.parse("content://" + PROVIDER_NAME + "/lookup"); 54 55 private static final int CALLER_ID_LOOKUP_USER_PROVIDED_CID = 0x0001; 56 private static final int CALLER_ID_LOOKUP_SYSTEM_PROVIDED_CID = 0x0002; 57 private static final int CALLER_ID_LOOKUP_INCOMING_CALL = 0x0020; 58 59 private static final Uri CONTENT_URI_FOR_INCALL = 60 Uri.parse("content://" + PROVIDER_NAME + "/incalllookup"); 61 62 private static final String[] EMPTY_PROJECTION = new String[] {}; 63 64 // Column names in Cequint provider. 65 private static final String CITY_NAME = "cid_pCityName"; 66 private static final String STATE_NAME = "cid_pStateName"; 67 private static final String STATE_ABBR = "cid_pStateAbbr"; 68 private static final String COUNTRY_NAME = "cid_pCountryName"; 69 private static final String COMPANY = "cid_pCompany"; 70 private static final String NAME = "cid_pName"; 71 private static final String FIRST_NAME = "cid_pFirstName"; 72 private static final String LAST_NAME = "cid_pLastName"; 73 private static final String IMAGE = "cid_pLogo"; 74 private static final String DISPLAY_NAME = "cid_pDisplayName"; 75 76 // TODO: Revisit it and maybe remove it if it's not necessary. 77 private static final ConcurrentHashMap<String, CequintCallerIdContact> callLogCache = 78 new ConcurrentHashMap<>(); 79 private static final ConcurrentHashMap<String, CequintCallerIdContact> incallIncomingCallCache = 80 new ConcurrentHashMap<>(); 81 private static final ConcurrentHashMap<String, CequintCallerIdContact> incallOutgoingCallCache = 82 new ConcurrentHashMap<>(); 83 private static boolean hasRegisteredContentObserver; 84 private static boolean hasAlreadyCheckedCequintCallerIdPackage; 85 private static boolean isCequintCallerIdEnabled; 86 87 /** Cequint caller id contact information. */ 88 public static class CequintCallerIdContact { 89 public final String name; 90 public final String geoDescription; 91 public final String imageUrl; 92 CequintCallerIdContact(String name, String geoDescription, String imageUrl)93 private CequintCallerIdContact(String name, String geoDescription, String imageUrl) { 94 this.name = name; 95 this.geoDescription = geoDescription; 96 this.imageUrl = imageUrl; 97 } 98 } 99 100 /** Check whether Cequint Caller Id provider package is available and enabled. */ 101 @AnyThread isCequintCallerIdEnabled(@onNull Context context)102 public static synchronized boolean isCequintCallerIdEnabled(@NonNull Context context) { 103 if (!ConfigProviderBindings.get(context).getBoolean(CONFIG_CALLER_ID_ENABLED, true)) { 104 return false; 105 } 106 if (!hasAlreadyCheckedCequintCallerIdPackage) { 107 hasAlreadyCheckedCequintCallerIdPackage = true; 108 isCequintCallerIdEnabled = false; 109 110 try { 111 context.getPackageManager().getPackageInfo(PROVIDER_NAME, 0); 112 isCequintCallerIdEnabled = true; 113 } catch (PackageManager.NameNotFoundException e) { 114 isCequintCallerIdEnabled = false; 115 } 116 } 117 return isCequintCallerIdEnabled; 118 } 119 120 @WorkerThread 121 @Nullable getCequintCallerIdContact(Context context, String number)122 public static CequintCallerIdContact getCequintCallerIdContact(Context context, String number) { 123 Assert.isWorkerThread(); 124 LogUtil.d( 125 "CequintCallerIdManager.getCequintCallerIdContact", 126 "number: %s", 127 LogUtil.sanitizePhoneNumber(number)); 128 if (callLogCache.containsKey(number)) { 129 return callLogCache.get(number); 130 } 131 CequintCallerIdContact cequintCallerIdContact = 132 lookup( 133 context, 134 CONTENT_URI, 135 PhoneNumberUtils.stripSeparators(number), 136 new String[] {"system"}); 137 if (cequintCallerIdContact != null) { 138 callLogCache.put(number, cequintCallerIdContact); 139 } 140 return cequintCallerIdContact; 141 } 142 143 @WorkerThread 144 @Nullable getCequintCallerIdContactForInCall( Context context, String number, String cnapName, boolean isIncoming)145 public static CequintCallerIdContact getCequintCallerIdContactForInCall( 146 Context context, String number, String cnapName, boolean isIncoming) { 147 Assert.isWorkerThread(); 148 LogUtil.d( 149 "CequintCallerIdManager.getCequintCallerIdContactForInCall", 150 "number: %s, cnapName: %s, isIncoming: %b", 151 LogUtil.sanitizePhoneNumber(number), 152 LogUtil.sanitizePii(cnapName), 153 isIncoming); 154 registerContentObserver(context); 155 if (isIncoming && incallIncomingCallCache.containsKey(number)) { 156 return incallIncomingCallCache.get(number); 157 } else if (!isIncoming && incallOutgoingCallCache.containsKey(number)) { 158 return incallOutgoingCallCache.get(number); 159 } 160 int flag = 0; 161 if (isIncoming) { 162 flag |= CALLER_ID_LOOKUP_INCOMING_CALL; 163 flag |= CALLER_ID_LOOKUP_SYSTEM_PROVIDED_CID; 164 } else { 165 flag |= CALLER_ID_LOOKUP_USER_PROVIDED_CID; 166 } 167 String[] flags = {cnapName, String.valueOf(flag)}; 168 CequintCallerIdContact cequintCallerIdContact = 169 lookup(context, CONTENT_URI_FOR_INCALL, number, flags); 170 if (cequintCallerIdContact != null) { 171 if (isIncoming) { 172 incallIncomingCallCache.put(number, cequintCallerIdContact); 173 } else { 174 incallOutgoingCallCache.put(number, cequintCallerIdContact); 175 } 176 } 177 return cequintCallerIdContact; 178 } 179 180 @WorkerThread 181 @Nullable lookup( Context context, Uri uri, @NonNull String number, String[] flags)182 private static CequintCallerIdContact lookup( 183 Context context, Uri uri, @NonNull String number, String[] flags) { 184 Assert.isWorkerThread(); 185 Assert.isNotNull(number); 186 187 // Cequint is using custom arguments for content provider. See more details in b/35766080. 188 try (Cursor cursor = 189 context.getContentResolver().query(uri, EMPTY_PROJECTION, number, flags, null)) { 190 if (cursor != null && cursor.moveToFirst()) { 191 String city = getString(cursor, cursor.getColumnIndex(CITY_NAME)); 192 String state = getString(cursor, cursor.getColumnIndex(STATE_NAME)); 193 String stateAbbr = getString(cursor, cursor.getColumnIndex(STATE_ABBR)); 194 String country = getString(cursor, cursor.getColumnIndex(COUNTRY_NAME)); 195 String company = getString(cursor, cursor.getColumnIndex(COMPANY)); 196 String name = getString(cursor, cursor.getColumnIndex(NAME)); 197 String firstName = getString(cursor, cursor.getColumnIndex(FIRST_NAME)); 198 String lastName = getString(cursor, cursor.getColumnIndex(LAST_NAME)); 199 String imageUrl = getString(cursor, cursor.getColumnIndex(IMAGE)); 200 String displayName = getString(cursor, cursor.getColumnIndex(DISPLAY_NAME)); 201 202 String contactName = 203 TextUtils.isEmpty(displayName) 204 ? generateDisplayName(firstName, lastName, company, name) 205 : displayName; 206 String geoDescription = getGeoDescription(city, state, stateAbbr, country); 207 LogUtil.d( 208 "CequintCallerIdManager.lookup", 209 "number: %s, contact name: %s, geo: %s, photo url: %s", 210 LogUtil.sanitizePhoneNumber(number), 211 LogUtil.sanitizePii(contactName), 212 LogUtil.sanitizePii(geoDescription), 213 imageUrl); 214 return new CequintCallerIdContact(contactName, geoDescription, imageUrl); 215 } else { 216 LogUtil.d("CequintCallerIdManager.lookup", "No CequintCallerIdContact found"); 217 return null; 218 } 219 } catch (Exception e) { 220 LogUtil.e("CequintCallerIdManager.lookup", "exception on query", e); 221 return null; 222 } 223 } 224 getString(Cursor cursor, int columnIndex)225 private static String getString(Cursor cursor, int columnIndex) { 226 if (!cursor.isNull(columnIndex)) { 227 String string = cursor.getString(columnIndex); 228 if (!TextUtils.isEmpty(string)) { 229 return string; 230 } 231 } 232 return null; 233 } 234 235 /** 236 * Returns generated name from other names, e.g. first name, last name etc. Returns null if there 237 * is no other names. 238 */ 239 @Nullable generateDisplayName( String firstName, String lastName, String company, String name)240 private static String generateDisplayName( 241 String firstName, String lastName, String company, String name) { 242 boolean hasFirstName = !TextUtils.isEmpty(firstName); 243 boolean hasLastName = !TextUtils.isEmpty(lastName); 244 boolean hasCompanyName = !TextUtils.isEmpty(company); 245 boolean hasName = !TextUtils.isEmpty(name); 246 247 StringBuilder stringBuilder = new StringBuilder(); 248 249 if (hasFirstName || hasLastName) { 250 if (hasFirstName) { 251 stringBuilder.append(firstName); 252 if (hasLastName) { 253 stringBuilder.append(" "); 254 } 255 } 256 if (hasLastName) { 257 stringBuilder.append(lastName); 258 } 259 } else if (hasCompanyName) { 260 stringBuilder.append(company); 261 } else if (hasName) { 262 stringBuilder.append(name); 263 } else { 264 return null; 265 } 266 267 if (stringBuilder.length() > 0) { 268 return stringBuilder.toString(); 269 } 270 return null; 271 } 272 273 /** Returns geo location information. e.g. Mountain View, CA. */ getGeoDescription( String city, String state, String stateAbbr, String country)274 private static String getGeoDescription( 275 String city, String state, String stateAbbr, String country) { 276 String geoDescription = null; 277 278 if (TextUtils.isEmpty(city) && !TextUtils.isEmpty(state)) { 279 geoDescription = state; 280 } else if (!TextUtils.isEmpty(city) && !TextUtils.isEmpty(stateAbbr)) { 281 geoDescription = city + ", " + stateAbbr; 282 } else if (!TextUtils.isEmpty(country)) { 283 geoDescription = country; 284 } 285 return geoDescription; 286 } 287 registerContentObserver(Context context)288 private static synchronized void registerContentObserver(Context context) { 289 if (!PermissionsUtil.hasCequintPermissions(context)) { 290 LogUtil.i("CequintCallerIdManager.registerContentObserver", "no cequint permissions"); 291 return; 292 } 293 294 if (hasRegisteredContentObserver) { 295 return; 296 } 297 ContentObserver contentObserver = 298 new ContentObserver(null) { 299 @Override 300 public void onChange(boolean selfChange) { 301 invalidateCache(); 302 } 303 }; 304 305 context 306 .getContentResolver() 307 .registerContentObserver(CONTENT_URI_FOR_INCALL, true, contentObserver); 308 hasRegisteredContentObserver = true; 309 } 310 invalidateCache()311 private static void invalidateCache() { 312 incallIncomingCallCache.clear(); 313 incallOutgoingCallCache.clear(); 314 } 315 CequintCallerIdManager()316 private CequintCallerIdManager() {} 317 } 318