1 /* 2 * Copyright (C) 2018 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 17 package com.android.dialer.phonelookup.cp2; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.net.Uri; 22 import android.os.Build.VERSION; 23 import android.os.Build.VERSION_CODES; 24 import android.provider.ContactsContract; 25 import android.support.annotation.VisibleForTesting; 26 import com.android.dialer.DialerPhoneNumber; 27 import com.android.dialer.common.LogUtil; 28 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; 29 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; 30 import com.android.dialer.common.cp2.DirectoryCompat; 31 import com.android.dialer.inject.ApplicationContext; 32 import com.android.dialer.phonelookup.PhoneLookup; 33 import com.android.dialer.phonelookup.PhoneLookupInfo; 34 import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; 35 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 36 import com.google.common.collect.ImmutableMap; 37 import com.google.common.collect.ImmutableSet; 38 import com.google.common.util.concurrent.Futures; 39 import com.google.common.util.concurrent.ListenableFuture; 40 import com.google.common.util.concurrent.ListeningExecutorService; 41 import java.util.ArrayList; 42 import java.util.List; 43 import javax.inject.Inject; 44 45 /** 46 * PhoneLookup implementation for contacts in both local and remote directories other than the 47 * default directory. 48 * 49 * <p>Contacts in these directories are accessible only by specifying a directory ID. 50 */ 51 public final class Cp2ExtendedDirectoryPhoneLookup implements PhoneLookup<Cp2Info> { 52 53 private final Context appContext; 54 private final ListeningExecutorService backgroundExecutorService; 55 private final ListeningExecutorService lightweightExecutorService; 56 57 @Inject Cp2ExtendedDirectoryPhoneLookup( @pplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutorService, @LightweightExecutor ListeningExecutorService lightweightExecutorService)58 Cp2ExtendedDirectoryPhoneLookup( 59 @ApplicationContext Context appContext, 60 @BackgroundExecutor ListeningExecutorService backgroundExecutorService, 61 @LightweightExecutor ListeningExecutorService lightweightExecutorService) { 62 this.appContext = appContext; 63 this.backgroundExecutorService = backgroundExecutorService; 64 this.lightweightExecutorService = lightweightExecutorService; 65 } 66 67 @Override lookup(DialerPhoneNumber dialerPhoneNumber)68 public ListenableFuture<Cp2Info> lookup(DialerPhoneNumber dialerPhoneNumber) { 69 return Futures.transformAsync( 70 queryCp2ForExtendedDirectoryIds(), 71 directoryIds -> queryCp2ForDirectoryContact(dialerPhoneNumber, directoryIds), 72 lightweightExecutorService); 73 } 74 queryCp2ForExtendedDirectoryIds()75 private ListenableFuture<List<Long>> queryCp2ForExtendedDirectoryIds() { 76 return backgroundExecutorService.submit( 77 () -> { 78 List<Long> directoryIds = new ArrayList<>(); 79 try (Cursor cursor = 80 appContext 81 .getContentResolver() 82 .query( 83 DirectoryCompat.getContentUri(), 84 /* projection = */ new String[] {ContactsContract.Directory._ID}, 85 /* selection = */ null, 86 /* selectionArgs = */ null, 87 /* sortOrder = */ ContactsContract.Directory._ID)) { 88 if (cursor == null) { 89 LogUtil.e( 90 "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForExtendedDirectoryIds", "null cursor"); 91 return directoryIds; 92 } 93 94 if (!cursor.moveToFirst()) { 95 LogUtil.i( 96 "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForExtendedDirectoryIds", 97 "empty cursor"); 98 return directoryIds; 99 } 100 101 int idColumnIndex = cursor.getColumnIndexOrThrow(ContactsContract.Directory._ID); 102 do { 103 long directoryId = cursor.getLong(idColumnIndex); 104 105 if (isExtendedDirectory(directoryId)) { 106 directoryIds.add(cursor.getLong(idColumnIndex)); 107 } 108 } while (cursor.moveToNext()); 109 return directoryIds; 110 } 111 }); 112 } 113 queryCp2ForDirectoryContact( DialerPhoneNumber dialerPhoneNumber, List<Long> directoryIds)114 private ListenableFuture<Cp2Info> queryCp2ForDirectoryContact( 115 DialerPhoneNumber dialerPhoneNumber, List<Long> directoryIds) { 116 if (directoryIds.isEmpty()) { 117 return Futures.immediateFuture(Cp2Info.getDefaultInstance()); 118 } 119 120 // Note: This loses country info when number is not valid. 121 String number = dialerPhoneNumber.getNormalizedNumber(); 122 123 List<ListenableFuture<Cp2Info>> cp2InfoFutures = new ArrayList<>(); 124 for (long directoryId : directoryIds) { 125 cp2InfoFutures.add(queryCp2ForDirectoryContact(number, directoryId)); 126 } 127 128 return Futures.transform( 129 Futures.allAsList(cp2InfoFutures), 130 cp2InfoList -> { 131 Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder(); 132 for (Cp2Info cp2Info : cp2InfoList) { 133 cp2InfoBuilder.addAllCp2ContactInfo(cp2Info.getCp2ContactInfoList()); 134 } 135 return cp2InfoBuilder.build(); 136 }, 137 lightweightExecutorService); 138 } 139 140 private ListenableFuture<Cp2Info> queryCp2ForDirectoryContact(String number, long directoryId) { 141 return backgroundExecutorService.submit( 142 () -> { 143 Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder(); 144 try (Cursor cursor = 145 appContext 146 .getContentResolver() 147 .query( 148 getContentUriForContacts(number, directoryId), 149 Cp2Projections.getProjectionForPhoneLookupTable(), 150 /* selection = */ null, 151 /* selectionArgs = */ null, 152 /* sortOrder = */ null)) { 153 if (cursor == null) { 154 LogUtil.e( 155 "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForDirectoryContact", 156 "null cursor returned when querying directory %d", 157 directoryId); 158 return cp2InfoBuilder.build(); 159 } 160 161 if (!cursor.moveToFirst()) { 162 LogUtil.i( 163 "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForDirectoryContact", 164 "empty cursor returned when querying directory %d", 165 directoryId); 166 return cp2InfoBuilder.build(); 167 } 168 169 do { 170 cp2InfoBuilder.addCp2ContactInfo( 171 Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor)); 172 } while (cursor.moveToNext()); 173 } 174 175 return cp2InfoBuilder.build(); 176 }); 177 } 178 179 @VisibleForTesting 180 static Uri getContentUriForContacts(String number, long directoryId) { 181 Uri baseUri = 182 VERSION.SDK_INT >= VERSION_CODES.N 183 ? ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI 184 : ContactsContract.PhoneLookup.CONTENT_FILTER_URI; 185 186 Uri.Builder builder = 187 baseUri 188 .buildUpon() 189 .appendPath(number) 190 .appendQueryParameter( 191 ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, 192 String.valueOf(PhoneNumberHelper.isUriNumber(number))) 193 .appendQueryParameter( 194 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); 195 196 return builder.build(); 197 } 198 199 private static boolean isExtendedDirectory(long directoryId) { 200 return DirectoryCompat.isRemoteDirectoryId(directoryId) 201 || DirectoryCompat.isEnterpriseDirectoryId(directoryId); 202 } 203 204 @Override 205 public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { 206 return Futures.immediateFuture(false); 207 } 208 209 @Override 210 public ListenableFuture<ImmutableMap<DialerPhoneNumber, Cp2Info>> getMostRecentInfo( 211 ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) { 212 return Futures.immediateFuture(existingInfoMap); 213 } 214 215 @Override 216 public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) { 217 destination.setExtendedCp2Info(subMessage); 218 } 219 220 @Override 221 public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) { 222 return phoneLookupInfo.getExtendedCp2Info(); 223 } 224 225 @Override 226 public ListenableFuture<Void> onSuccessfulBulkUpdate() { 227 return Futures.immediateFuture(null); 228 } 229 230 @Override 231 public void registerContentObservers(Context appContext) { 232 // For contacts in remote directories, no content observer can be registered. 233 // For contacts in local (but not default) directories (e.g., the local work directory), we 234 // don't register a content observer for now. 235 } 236 } 237