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