• 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.provider.ContactsContract;
23 import android.provider.ContactsContract.Directory;
24 import android.support.annotation.VisibleForTesting;
25 import com.android.dialer.DialerPhoneNumber;
26 import com.android.dialer.common.LogUtil;
27 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
28 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
29 import com.android.dialer.common.concurrent.Annotations.NonUiSerial;
30 import com.android.dialer.configprovider.ConfigProvider;
31 import com.android.dialer.inject.ApplicationContext;
32 import com.android.dialer.logging.DialerImpression;
33 import com.android.dialer.logging.Logger;
34 import com.android.dialer.phonelookup.PhoneLookup;
35 import com.android.dialer.phonelookup.PhoneLookupInfo;
36 import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
37 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
38 import com.android.dialer.util.PermissionsUtil;
39 import com.google.common.collect.ImmutableMap;
40 import com.google.common.collect.ImmutableSet;
41 import com.google.common.util.concurrent.Futures;
42 import com.google.common.util.concurrent.ListenableFuture;
43 import com.google.common.util.concurrent.ListeningExecutorService;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.concurrent.ScheduledExecutorService;
47 import java.util.concurrent.TimeUnit;
48 import java.util.concurrent.TimeoutException;
49 import java.util.function.Predicate;
50 import javax.inject.Inject;
51 
52 /**
53  * PhoneLookup implementation for contacts in both local and remote directories other than the
54  * default directory.
55  *
56  * <p>Contacts in these directories are accessible only by specifying a directory ID.
57  */
58 public final class Cp2ExtendedDirectoryPhoneLookup implements PhoneLookup<Cp2Info> {
59 
60   /** Config flag for timeout (in ms). */
61   @VisibleForTesting
62   static final String CP2_EXTENDED_DIRECTORY_PHONE_LOOKUP_TIMEOUT_MILLIS =
63       "cp2_extended_directory_phone_lookup_timout_millis";
64 
65   private final Context appContext;
66   private final ConfigProvider configProvider;
67   private final ListeningExecutorService backgroundExecutorService;
68   private final ListeningExecutorService lightweightExecutorService;
69   private final MissingPermissionsOperations missingPermissionsOperations;
70   private final ScheduledExecutorService scheduledExecutorService;
71 
72   @Inject
Cp2ExtendedDirectoryPhoneLookup( @pplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutorService, @LightweightExecutor ListeningExecutorService lightweightExecutorService, @NonUiSerial ScheduledExecutorService scheduledExecutorService, ConfigProvider configProvider, MissingPermissionsOperations missingPermissionsOperations)73   Cp2ExtendedDirectoryPhoneLookup(
74       @ApplicationContext Context appContext,
75       @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
76       @LightweightExecutor ListeningExecutorService lightweightExecutorService,
77       @NonUiSerial ScheduledExecutorService scheduledExecutorService,
78       ConfigProvider configProvider,
79       MissingPermissionsOperations missingPermissionsOperations) {
80     this.appContext = appContext;
81     this.backgroundExecutorService = backgroundExecutorService;
82     this.lightweightExecutorService = lightweightExecutorService;
83     this.scheduledExecutorService = scheduledExecutorService;
84     this.configProvider = configProvider;
85     this.missingPermissionsOperations = missingPermissionsOperations;
86   }
87 
88   @Override
lookup(DialerPhoneNumber dialerPhoneNumber)89   public ListenableFuture<Cp2Info> lookup(DialerPhoneNumber dialerPhoneNumber) {
90     if (!PermissionsUtil.hasContactsReadPermissions(appContext)) {
91       return Futures.immediateFuture(Cp2Info.getDefaultInstance());
92     }
93 
94     ListenableFuture<Cp2Info> cp2InfoFuture =
95         Futures.transformAsync(
96             queryCp2ForExtendedDirectoryIds(),
97             directoryIds -> queryCp2ForDirectoryContact(dialerPhoneNumber, directoryIds),
98             lightweightExecutorService);
99 
100     long timeoutMillis =
101         configProvider.getLong(CP2_EXTENDED_DIRECTORY_PHONE_LOOKUP_TIMEOUT_MILLIS, Long.MAX_VALUE);
102 
103     // Do not pass Long.MAX_VALUE to Futures.withTimeout as it will cause the internal
104     // ScheduledExecutorService for timing to keep waiting even after "cp2InfoFuture" is done.
105     // Do not pass 0 or a negative value to Futures.withTimeout either as it will cause the timeout
106     // event to be triggered immediately.
107     return timeoutMillis == Long.MAX_VALUE
108         ? cp2InfoFuture
109         : Futures.catching(
110             Futures.withTimeout(
111                 cp2InfoFuture, timeoutMillis, TimeUnit.MILLISECONDS, scheduledExecutorService),
112             TimeoutException.class,
113             unused -> {
114               LogUtil.w("Cp2ExtendedDirectoryPhoneLookup.lookup", "Time out!");
115               Logger.get(appContext)
116                   .logImpression(DialerImpression.Type.CP2_EXTENDED_DIRECTORY_PHONE_LOOKUP_TIMEOUT);
117               return Cp2Info.getDefaultInstance();
118             },
119             lightweightExecutorService);
120   }
121 
queryCp2ForExtendedDirectoryIds()122   private ListenableFuture<List<Long>> queryCp2ForExtendedDirectoryIds() {
123     return backgroundExecutorService.submit(
124         () -> {
125           List<Long> directoryIds = new ArrayList<>();
126           try (Cursor cursor =
127               appContext
128                   .getContentResolver()
129                   .query(
130                       Directory.ENTERPRISE_CONTENT_URI,
131                       /* projection = */ new String[] {ContactsContract.Directory._ID},
132                       /* selection = */ null,
133                       /* selectionArgs = */ null,
134                       /* sortOrder = */ ContactsContract.Directory._ID)) {
135             if (cursor == null) {
136               LogUtil.e(
137                   "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForExtendedDirectoryIds", "null cursor");
138               return directoryIds;
139             }
140 
141             if (!cursor.moveToFirst()) {
142               LogUtil.i(
143                   "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForExtendedDirectoryIds",
144                   "empty cursor");
145               return directoryIds;
146             }
147 
148             int idColumnIndex = cursor.getColumnIndexOrThrow(ContactsContract.Directory._ID);
149             do {
150               long directoryId = cursor.getLong(idColumnIndex);
151 
152               if (isExtendedDirectory(directoryId)) {
153                 directoryIds.add(cursor.getLong(idColumnIndex));
154               }
155             } while (cursor.moveToNext());
156             return directoryIds;
157           }
158         });
159   }
160 
161   private ListenableFuture<Cp2Info> queryCp2ForDirectoryContact(
162       DialerPhoneNumber dialerPhoneNumber, List<Long> directoryIds) {
163     if (directoryIds.isEmpty()) {
164       return Futures.immediateFuture(Cp2Info.getDefaultInstance());
165     }
166 
167     // Note: This loses country info when number is not valid.
168     String number = dialerPhoneNumber.getNormalizedNumber();
169 
170     List<ListenableFuture<Cp2Info>> cp2InfoFutures = new ArrayList<>();
171     for (long directoryId : directoryIds) {
172       cp2InfoFutures.add(queryCp2ForDirectoryContact(number, directoryId));
173     }
174 
175     return Futures.transform(
176         Futures.allAsList(cp2InfoFutures),
177         cp2InfoList -> {
178           Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder();
179           for (Cp2Info cp2Info : cp2InfoList) {
180             cp2InfoBuilder.addAllCp2ContactInfo(cp2Info.getCp2ContactInfoList());
181           }
182           return cp2InfoBuilder.build();
183         },
184         lightweightExecutorService);
185   }
186 
187   private ListenableFuture<Cp2Info> queryCp2ForDirectoryContact(String number, long directoryId) {
188     return backgroundExecutorService.submit(
189         () -> {
190           Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder();
191           try (Cursor cursor =
192               appContext
193                   .getContentResolver()
194                   .query(
195                       getContentUriForContacts(number, directoryId),
196                       Cp2Projections.getProjectionForPhoneLookupTable(),
197                       /* selection = */ null,
198                       /* selectionArgs = */ null,
199                       /* sortOrder = */ null)) {
200             if (cursor == null) {
201               LogUtil.e(
202                   "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForDirectoryContact",
203                   "null cursor returned when querying directory %d",
204                   directoryId);
205               return cp2InfoBuilder.build();
206             }
207 
208             if (!cursor.moveToFirst()) {
209               LogUtil.i(
210                   "Cp2ExtendedDirectoryPhoneLookup.queryCp2ForDirectoryContact",
211                   "empty cursor returned when querying directory %d",
212                   directoryId);
213               return cp2InfoBuilder.build();
214             }
215 
216             do {
217               cp2InfoBuilder.addCp2ContactInfo(
218                   Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor, directoryId));
219             } while (cursor.moveToNext());
220           }
221 
222           return cp2InfoBuilder.build();
223         });
224   }
225 
226   @VisibleForTesting
227   static Uri getContentUriForContacts(String number, long directoryId) {
228     Uri.Builder builder =
229         ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI
230             .buildUpon()
231             .appendPath(number)
232             .appendQueryParameter(
233                 ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
234                 String.valueOf(PhoneNumberHelper.isUriNumber(number)))
235             .appendQueryParameter(
236                 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
237 
238     return builder.build();
239   }
240 
241   private static boolean isExtendedDirectory(long directoryId) {
242     return Directory.isRemoteDirectoryId(directoryId)
243         || Directory.isEnterpriseDirectoryId(directoryId);
244   }
245 
246   @Override
247   public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
248     if (!PermissionsUtil.hasContactsReadPermissions(appContext)) {
249       Predicate<PhoneLookupInfo> phoneLookupInfoIsDirtyFn =
250           phoneLookupInfo ->
251               !phoneLookupInfo.getExtendedCp2Info().equals(Cp2Info.getDefaultInstance());
252       return missingPermissionsOperations.isDirtyForMissingPermissions(
253           phoneNumbers, phoneLookupInfoIsDirtyFn);
254     }
255     return Futures.immediateFuture(false);
256   }
257 
258   @Override
259   public ListenableFuture<ImmutableMap<DialerPhoneNumber, Cp2Info>> getMostRecentInfo(
260       ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) {
261     if (!PermissionsUtil.hasContactsReadPermissions(appContext)) {
262       LogUtil.w("Cp2ExtendedDirectoryPhoneLookup.getMostRecentInfo", "missing permissions");
263       return missingPermissionsOperations.getMostRecentInfoForMissingPermissions(existingInfoMap);
264     }
265     return Futures.immediateFuture(existingInfoMap);
266   }
267 
268   @Override
269   public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) {
270     destination.setExtendedCp2Info(subMessage);
271   }
272 
273   @Override
274   public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) {
275     return phoneLookupInfo.getExtendedCp2Info();
276   }
277 
278   @Override
279   public ListenableFuture<Void> onSuccessfulBulkUpdate() {
280     return Futures.immediateFuture(null);
281   }
282 
283   @Override
284   public void registerContentObservers() {
285     // For contacts in remote directories, no content observer can be registered.
286     // For contacts in local (but not default) directories (e.g., the local work directory), we
287     // don't register a content observer for now.
288   }
289 
290   @Override
291   public void unregisterContentObservers() {}
292 
293   @Override
294   public ListenableFuture<Void> clearData() {
295     return Futures.immediateFuture(null);
296   }
297 
298   @Override
299   public String getLoggingName() {
300     return "Cp2ExtendedDirectoryPhoneLookup";
301   }
302 }
303