/*
* Copyright (C) 2018 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.phonelookup.cp2;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.provider.ContactsContract;
import android.support.annotation.VisibleForTesting;
import com.android.dialer.DialerPhoneNumber;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
import com.android.dialer.common.cp2.DirectoryCompat;
import com.android.dialer.inject.ApplicationContext;
import com.android.dialer.phonelookup.PhoneLookup;
import com.android.dialer.phonelookup.PhoneLookupInfo;
import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* PhoneLookup implementation for contacts in both local and remote directories other than the
* default directory.
*
*
Contacts in these directories are accessible only by specifying a directory ID.
*/
public final class Cp2ExtendedDirectoryPhoneLookup implements PhoneLookup {
private final Context appContext;
private final ListeningExecutorService backgroundExecutorService;
private final ListeningExecutorService lightweightExecutorService;
@Inject
Cp2ExtendedDirectoryPhoneLookup(
@ApplicationContext Context appContext,
@BackgroundExecutor ListeningExecutorService backgroundExecutorService,
@LightweightExecutor ListeningExecutorService lightweightExecutorService) {
this.appContext = appContext;
this.backgroundExecutorService = backgroundExecutorService;
this.lightweightExecutorService = lightweightExecutorService;
}
@Override
public ListenableFuture lookup(DialerPhoneNumber dialerPhoneNumber) {
return Futures.transformAsync(
queryCp2ForExtendedDirectoryIds(),
directoryIds -> queryCp2ForDirectoryContact(dialerPhoneNumber, directoryIds),
lightweightExecutorService);
}
private ListenableFuture> queryCp2ForExtendedDirectoryIds() {
return backgroundExecutorService.submit(
() -> {
List directoryIds = new ArrayList<>();
try (Cursor cursor =
appContext
.getContentResolver()
.query(
DirectoryCompat.getContentUri(),
/* projection = */ new String[] {ContactsContract.Directory._ID},
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ ContactsContract.Directory._ID)) {
if (cursor == null) {
LogUtil.e(
"Cp2ExtendedDirectoryPhoneLookup.queryCp2ForExtendedDirectoryIds", "null cursor");
return directoryIds;
}
if (!cursor.moveToFirst()) {
LogUtil.i(
"Cp2ExtendedDirectoryPhoneLookup.queryCp2ForExtendedDirectoryIds",
"empty cursor");
return directoryIds;
}
int idColumnIndex = cursor.getColumnIndexOrThrow(ContactsContract.Directory._ID);
do {
long directoryId = cursor.getLong(idColumnIndex);
if (isExtendedDirectory(directoryId)) {
directoryIds.add(cursor.getLong(idColumnIndex));
}
} while (cursor.moveToNext());
return directoryIds;
}
});
}
private ListenableFuture queryCp2ForDirectoryContact(
DialerPhoneNumber dialerPhoneNumber, List directoryIds) {
if (directoryIds.isEmpty()) {
return Futures.immediateFuture(Cp2Info.getDefaultInstance());
}
// Note: This loses country info when number is not valid.
String number = dialerPhoneNumber.getNormalizedNumber();
List> cp2InfoFutures = new ArrayList<>();
for (long directoryId : directoryIds) {
cp2InfoFutures.add(queryCp2ForDirectoryContact(number, directoryId));
}
return Futures.transform(
Futures.allAsList(cp2InfoFutures),
cp2InfoList -> {
Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder();
for (Cp2Info cp2Info : cp2InfoList) {
cp2InfoBuilder.addAllCp2ContactInfo(cp2Info.getCp2ContactInfoList());
}
return cp2InfoBuilder.build();
},
lightweightExecutorService);
}
private ListenableFuture queryCp2ForDirectoryContact(String number, long directoryId) {
return backgroundExecutorService.submit(
() -> {
Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder();
try (Cursor cursor =
appContext
.getContentResolver()
.query(
getContentUriForContacts(number, directoryId),
Cp2Projections.getProjectionForPhoneLookupTable(),
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ null)) {
if (cursor == null) {
LogUtil.e(
"Cp2ExtendedDirectoryPhoneLookup.queryCp2ForDirectoryContact",
"null cursor returned when querying directory %d",
directoryId);
return cp2InfoBuilder.build();
}
if (!cursor.moveToFirst()) {
LogUtil.i(
"Cp2ExtendedDirectoryPhoneLookup.queryCp2ForDirectoryContact",
"empty cursor returned when querying directory %d",
directoryId);
return cp2InfoBuilder.build();
}
do {
cp2InfoBuilder.addCp2ContactInfo(
Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor));
} while (cursor.moveToNext());
}
return cp2InfoBuilder.build();
});
}
@VisibleForTesting
static Uri getContentUriForContacts(String number, long directoryId) {
Uri baseUri =
VERSION.SDK_INT >= VERSION_CODES.N
? ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI
: ContactsContract.PhoneLookup.CONTENT_FILTER_URI;
Uri.Builder builder =
baseUri
.buildUpon()
.appendPath(number)
.appendQueryParameter(
ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
String.valueOf(PhoneNumberHelper.isUriNumber(number)))
.appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
return builder.build();
}
private static boolean isExtendedDirectory(long directoryId) {
return DirectoryCompat.isRemoteDirectoryId(directoryId)
|| DirectoryCompat.isEnterpriseDirectoryId(directoryId);
}
@Override
public ListenableFuture isDirty(ImmutableSet phoneNumbers) {
return Futures.immediateFuture(false);
}
@Override
public ListenableFuture> getMostRecentInfo(
ImmutableMap existingInfoMap) {
return Futures.immediateFuture(existingInfoMap);
}
@Override
public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) {
destination.setExtendedCp2Info(subMessage);
}
@Override
public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) {
return phoneLookupInfo.getExtendedCp2Info();
}
@Override
public ListenableFuture onSuccessfulBulkUpdate() {
return Futures.immediateFuture(null);
}
@Override
public void registerContentObservers() {
// For contacts in remote directories, no content observer can be registered.
// For contacts in local (but not default) directories (e.g., the local work directory), we
// don't register a content observer for now.
}
@Override
public void unregisterContentObservers() {}
@Override
public ListenableFuture clearData() {
return Futures.immediateFuture(null);
}
}