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