• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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