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