/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dialer.oem;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.configprovider.ConfigProviderComponent;
import com.google.auto.value.AutoValue;
import java.util.concurrent.ConcurrentHashMap;
/**
* Cequint Caller ID manager to provide caller information.
*
*
This is only enabled on Motorola devices for Sprint.
*
*
If it's enabled, this class will be called by call log and incall to get caller info from
* Cequint Caller ID. It also caches any information fetched in static map, which lives through
* whole application lifecycle.
*/
public class CequintCallerIdManager {
@VisibleForTesting
public static final String CONFIG_CALLER_ID_ENABLED = "config_caller_id_enabled";
private static final int CALLER_ID_LOOKUP_USER_PROVIDED_CID = 0x0001;
private static final int CALLER_ID_LOOKUP_SYSTEM_PROVIDED_CID = 0x0002;
private static final int CALLER_ID_LOOKUP_INCOMING_CALL = 0x0020;
private static final String[] EMPTY_PROJECTION = new String[] {};
/** Column names in Cequint content provider. */
@VisibleForTesting
public static final class CequintColumnNames {
public static final String CITY_NAME = "cid_pCityName";
public static final String STATE_NAME = "cid_pStateName";
public static final String STATE_ABBR = "cid_pStateAbbr";
public static final String COUNTRY_NAME = "cid_pCountryName";
public static final String COMPANY = "cid_pCompany";
public static final String NAME = "cid_pName";
public static final String FIRST_NAME = "cid_pFirstName";
public static final String LAST_NAME = "cid_pLastName";
public static final String PHOTO_URI = "cid_pLogo";
public static final String DISPLAY_NAME = "cid_pDisplayName";
}
private static boolean hasAlreadyCheckedCequintCallerIdPackage;
private static String cequintProviderAuthority;
// TODO(a bug): Revisit it and maybe remove it if it's not necessary.
private final ConcurrentHashMap callLogCache =
new ConcurrentHashMap<>();
/** Cequint caller ID contact information. */
@AutoValue
public abstract static class CequintCallerIdContact {
@Nullable
public abstract String name();
/**
* Description of the geolocation (e.g., "Mountain View, CA"), which is for display purpose
* only.
*/
@Nullable
public abstract String geolocation();
@Nullable
public abstract String photoUri();
static Builder builder() {
return new AutoValue_CequintCallerIdManager_CequintCallerIdContact.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder setName(@Nullable String name);
abstract Builder setGeolocation(@Nullable String geolocation);
abstract Builder setPhotoUri(@Nullable String photoUri);
abstract CequintCallerIdContact build();
}
}
/** Check whether Cequint Caller ID provider package is available and enabled. */
@AnyThread
public static synchronized boolean isCequintCallerIdEnabled(@NonNull Context context) {
if (!ConfigProviderComponent.get(context)
.getConfigProvider()
.getBoolean(CONFIG_CALLER_ID_ENABLED, true)) {
return false;
}
if (!hasAlreadyCheckedCequintCallerIdPackage) {
hasAlreadyCheckedCequintCallerIdPackage = true;
String[] providerNames = context.getResources().getStringArray(R.array.cequint_providers);
PackageManager packageManager = context.getPackageManager();
for (String provider : providerNames) {
if (CequintPackageUtils.isCallerIdInstalled(packageManager, provider)) {
cequintProviderAuthority = provider;
LogUtil.i(
"CequintCallerIdManager.isCequintCallerIdEnabled", "found provider: %s", provider);
return true;
}
}
LogUtil.d("CequintCallerIdManager.isCequintCallerIdEnabled", "no provider found");
}
return cequintProviderAuthority != null;
}
/** Returns a {@link CequintCallerIdContact} for a call. */
@WorkerThread
@Nullable
public static CequintCallerIdContact getCequintCallerIdContactForCall(
Context context, String number, String cnapName, boolean isIncoming) {
Assert.isWorkerThread();
LogUtil.d(
"CequintCallerIdManager.getCequintCallerIdContactForCall",
"number: %s, cnapName: %s, isIncoming: %b",
LogUtil.sanitizePhoneNumber(number),
LogUtil.sanitizePii(cnapName),
isIncoming);
int flag = 0;
if (isIncoming) {
flag |= CALLER_ID_LOOKUP_INCOMING_CALL;
flag |= CALLER_ID_LOOKUP_SYSTEM_PROVIDED_CID;
} else {
flag |= CALLER_ID_LOOKUP_USER_PROVIDED_CID;
}
String[] flags = {cnapName, String.valueOf(flag)};
return lookup(context, getIncallLookupUri(), number, flags);
}
/**
* Returns a cached {@link CequintCallerIdContact} associated with the provided number. If no
* contact can be found in the cache, look up the number using the Cequint content provider.
*
* @deprecated This method is for the old call log only. New code should use {@link
* #getCequintCallerIdContactForNumber(Context, String)}.
*/
@Deprecated
@WorkerThread
@Nullable
public CequintCallerIdContact getCachedCequintCallerIdContact(Context context, String number) {
Assert.isWorkerThread();
LogUtil.d(
"CequintCallerIdManager.getCachedCequintCallerIdContact",
"number: %s",
LogUtil.sanitizePhoneNumber(number));
if (callLogCache.containsKey(number)) {
return callLogCache.get(number);
}
CequintCallerIdContact cequintCallerIdContact =
getCequintCallerIdContactForNumber(context, number);
if (cequintCallerIdContact != null) {
callLogCache.put(number, cequintCallerIdContact);
}
return cequintCallerIdContact;
}
/**
* Returns a {@link CequintCallerIdContact} associated with the provided number by looking it up
* using the Cequint content provider.
*/
@WorkerThread
@Nullable
public static CequintCallerIdContact getCequintCallerIdContactForNumber(
Context context, String number) {
Assert.isWorkerThread();
LogUtil.d(
"CequintCallerIdManager.getCequintCallerIdContactForNumber",
"number: %s",
LogUtil.sanitizePhoneNumber(number));
return lookup(
context, getLookupUri(), PhoneNumberUtils.stripSeparators(number), new String[] {"system"});
}
@WorkerThread
@Nullable
private static CequintCallerIdContact lookup(
Context context, Uri uri, @NonNull String number, String[] flags) {
Assert.isWorkerThread();
Assert.isNotNull(number);
// Cequint is using custom arguments for content provider. See more details in a bug.
try (Cursor cursor =
context.getContentResolver().query(uri, EMPTY_PROJECTION, number, flags, null)) {
if (cursor != null && cursor.moveToFirst()) {
String city = getString(cursor, cursor.getColumnIndex(CequintColumnNames.CITY_NAME));
String state = getString(cursor, cursor.getColumnIndex(CequintColumnNames.STATE_NAME));
String stateAbbr = getString(cursor, cursor.getColumnIndex(CequintColumnNames.STATE_ABBR));
String country = getString(cursor, cursor.getColumnIndex(CequintColumnNames.COUNTRY_NAME));
String company = getString(cursor, cursor.getColumnIndex(CequintColumnNames.COMPANY));
String name = getString(cursor, cursor.getColumnIndex(CequintColumnNames.NAME));
String firstName = getString(cursor, cursor.getColumnIndex(CequintColumnNames.FIRST_NAME));
String lastName = getString(cursor, cursor.getColumnIndex(CequintColumnNames.LAST_NAME));
String photoUri = getString(cursor, cursor.getColumnIndex(CequintColumnNames.PHOTO_URI));
String displayName =
getString(cursor, cursor.getColumnIndex(CequintColumnNames.DISPLAY_NAME));
String contactName =
TextUtils.isEmpty(displayName)
? generateDisplayName(firstName, lastName, company, name)
: displayName;
String geolocation = getGeolocation(city, state, stateAbbr, country);
LogUtil.d(
"CequintCallerIdManager.lookup",
"number: %s, contact name: %s, geo: %s, photo url: %s",
LogUtil.sanitizePhoneNumber(number),
LogUtil.sanitizePii(contactName),
LogUtil.sanitizePii(geolocation),
photoUri);
return CequintCallerIdContact.builder()
.setName(contactName)
.setGeolocation(geolocation)
.setPhotoUri(photoUri)
.build();
} else {
LogUtil.d("CequintCallerIdManager.lookup", "No CequintCallerIdContact found");
return null;
}
} catch (Exception e) {
LogUtil.e("CequintCallerIdManager.lookup", "exception on query", e);
return null;
}
}
private static String getString(Cursor cursor, int columnIndex) {
if (!cursor.isNull(columnIndex)) {
String string = cursor.getString(columnIndex);
if (!TextUtils.isEmpty(string)) {
return string;
}
}
return null;
}
/**
* Returns generated name from other names, e.g. first name, last name etc. Returns null if there
* is no other names.
*/
@Nullable
private static String generateDisplayName(
String firstName, String lastName, String company, String name) {
boolean hasFirstName = !TextUtils.isEmpty(firstName);
boolean hasLastName = !TextUtils.isEmpty(lastName);
boolean hasCompanyName = !TextUtils.isEmpty(company);
boolean hasName = !TextUtils.isEmpty(name);
StringBuilder stringBuilder = new StringBuilder();
if (hasFirstName || hasLastName) {
if (hasFirstName) {
stringBuilder.append(firstName);
if (hasLastName) {
stringBuilder.append(" ");
}
}
if (hasLastName) {
stringBuilder.append(lastName);
}
} else if (hasCompanyName) {
stringBuilder.append(company);
} else if (hasName) {
stringBuilder.append(name);
} else {
return null;
}
if (stringBuilder.length() > 0) {
return stringBuilder.toString();
}
return null;
}
/** Returns geolocation information (e.g., "Mountain View, CA"). */
private static String getGeolocation(
String city, String state, String stateAbbr, String country) {
String geoDescription = null;
if (TextUtils.isEmpty(city) && !TextUtils.isEmpty(state)) {
geoDescription = state;
} else if (!TextUtils.isEmpty(city) && !TextUtils.isEmpty(stateAbbr)) {
geoDescription = city + ", " + stateAbbr;
} else if (!TextUtils.isEmpty(country)) {
geoDescription = country;
}
return geoDescription;
}
private static Uri getLookupUri() {
return Uri.parse("content://" + cequintProviderAuthority + "/lookup");
}
private static Uri getIncallLookupUri() {
return Uri.parse("content://" + cequintProviderAuthority + "/incalllookup");
}
}