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