• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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