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 package com.android.dialer.phonelookup.consolidator;
17 
18 import android.support.annotation.IntDef;
19 import android.support.annotation.Nullable;
20 import com.android.dialer.common.Assert;
21 import com.android.dialer.logging.ContactSource;
22 import com.android.dialer.phonelookup.PhoneLookup;
23 import com.android.dialer.phonelookup.PhoneLookupInfo;
24 import com.android.dialer.phonelookup.PhoneLookupInfo.BlockedState;
25 import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo;
26 import com.android.dialer.phonelookup.PhoneLookupInfo.PeopleApiInfo;
27 import com.android.dialer.phonelookup.PhoneLookupInfo.PeopleApiInfo.InfoType;
28 import com.google.common.collect.ImmutableList;
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 
32 /**
33  * Consolidates information from a {@link PhoneLookupInfo}.
34  *
35  * <p>For example, a single {@link PhoneLookupInfo} may contain different name information from many
36  * different {@link PhoneLookup} implementations. This class defines the rules for deciding which
37  * name should be selected for display to the user, by prioritizing the data from some {@link
38  * PhoneLookup PhoneLookups} over others.
39  */
40 public final class PhoneLookupInfoConsolidator {
41 
42   /** Integers representing {@link PhoneLookup} implementations that can provide a contact's name */
43   @Retention(RetentionPolicy.SOURCE)
44   @IntDef({
45     NameSource.NONE,
46     NameSource.CP2_DEFAULT_DIRECTORY,
47     NameSource.CP2_EXTENDED_DIRECTORY,
48     NameSource.PEOPLE_API,
49     NameSource.CEQUINT,
50     NameSource.CNAP,
51     NameSource.PHONE_NUMBER_CACHE
52   })
53   @interface NameSource {
54     int NONE = 0; // used when none of the other sources can provide the name
55     int CP2_DEFAULT_DIRECTORY = 1;
56     int CP2_EXTENDED_DIRECTORY = 2;
57     int PEOPLE_API = 3;
58     int CEQUINT = 4;
59     int CNAP = 5;
60     int PHONE_NUMBER_CACHE = 6;
61   }
62 
63   /**
64    * Sources that can provide information about a contact's name.
65    *
66    * <p>Each source is one of the values in NameSource, as defined above.
67    *
68    * <p>Sources are sorted in the order of priority. For example, if source CP2_DEFAULT_DIRECTORY
69    * can provide the name, we will use that name in the UI and ignore all the other sources. If
70    * source CP2_DEFAULT_DIRECTORY can't provide the name, source CP2_EXTENDED_DIRECTORY will be
71    * consulted.
72    *
73    * <p>The reason for defining a name source is to avoid mixing info from different sub-messages in
74    * PhoneLookupInfo proto when we are supposed to stick with only one sub-message. For example, if
75    * a PhoneLookupInfo proto has both default_cp2_info and extended_cp2_info but only
76    * extended_cp2_info has a photo URI, PhoneLookupInfoConsolidator should provide an empty photo
77    * URI as CP2_DEFAULT_DIRECTORY has higher priority and we should not use extended_cp2_info's
78    * photo URI to display the contact's photo.
79    */
80   private static final ImmutableList<Integer> NAME_SOURCES_IN_PRIORITY_ORDER =
81       ImmutableList.of(
82           NameSource.CP2_DEFAULT_DIRECTORY,
83           NameSource.CP2_EXTENDED_DIRECTORY,
84           NameSource.PEOPLE_API,
85           NameSource.CEQUINT,
86           NameSource.CNAP,
87           NameSource.PHONE_NUMBER_CACHE);
88 
89   private final @NameSource int nameSource;
90   private final PhoneLookupInfo phoneLookupInfo;
91 
92   @Nullable private final Cp2ContactInfo firstDefaultCp2Contact;
93   @Nullable private final Cp2ContactInfo firstExtendedCp2Contact;
94 
PhoneLookupInfoConsolidator(PhoneLookupInfo phoneLookupInfo)95   public PhoneLookupInfoConsolidator(PhoneLookupInfo phoneLookupInfo) {
96     this.phoneLookupInfo = phoneLookupInfo;
97 
98     this.firstDefaultCp2Contact = getFirstContactInDefaultDirectory();
99     this.firstExtendedCp2Contact = getFirstContactInExtendedDirectories();
100     this.nameSource = selectNameSource();
101   }
102 
103   /**
104    * Returns a {@link com.android.dialer.logging.ContactSource.Type} representing the source from
105    * which info is used to display contact info in the UI.
106    */
getContactSource()107   public ContactSource.Type getContactSource() {
108     switch (nameSource) {
109       case NameSource.CP2_DEFAULT_DIRECTORY:
110         return ContactSource.Type.SOURCE_TYPE_DIRECTORY;
111       case NameSource.CP2_EXTENDED_DIRECTORY:
112         return ContactSource.Type.SOURCE_TYPE_EXTENDED;
113       case NameSource.PEOPLE_API:
114         return getRefinedPeopleApiSource();
115       case NameSource.CEQUINT:
116         return ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID;
117       case NameSource.CNAP:
118         return ContactSource.Type.SOURCE_TYPE_CNAP;
119       case NameSource.PHONE_NUMBER_CACHE:
120         ContactSource.Type sourceType =
121             ContactSource.Type.forNumber(phoneLookupInfo.getMigratedInfo().getSourceType());
122         if (sourceType == null) {
123           sourceType = ContactSource.Type.UNKNOWN_SOURCE_TYPE;
124         }
125         return sourceType;
126       case NameSource.NONE:
127         return ContactSource.Type.UNKNOWN_SOURCE_TYPE;
128       default:
129         throw Assert.createUnsupportedOperationFailException(
130             String.format("Unsupported name source: %s", nameSource));
131     }
132   }
133 
getRefinedPeopleApiSource()134   private ContactSource.Type getRefinedPeopleApiSource() {
135     Assert.checkState(nameSource == NameSource.PEOPLE_API);
136 
137     switch (phoneLookupInfo.getPeopleApiInfo().getInfoType()) {
138       case CONTACT:
139         return ContactSource.Type.SOURCE_TYPE_PROFILE;
140       case NEARBY_BUSINESS:
141         return ContactSource.Type.SOURCE_TYPE_PLACES;
142       default:
143         return ContactSource.Type.SOURCE_TYPE_REMOTE_OTHER;
144     }
145   }
146 
147   /**
148    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
149    * returns the name associated with that number.
150    *
151    * <p>Examples of this are a local contact's name or a business name received from caller ID.
152    *
153    * <p>If no name can be obtained from the {@link PhoneLookupInfo}, an empty string will be
154    * returned.
155    */
getName()156   public String getName() {
157     switch (nameSource) {
158       case NameSource.CP2_DEFAULT_DIRECTORY:
159         return Assert.isNotNull(firstDefaultCp2Contact).getName();
160       case NameSource.CP2_EXTENDED_DIRECTORY:
161         return Assert.isNotNull(firstExtendedCp2Contact).getName();
162       case NameSource.PEOPLE_API:
163         return phoneLookupInfo.getPeopleApiInfo().getDisplayName();
164       case NameSource.CEQUINT:
165         return phoneLookupInfo.getCequintInfo().getName();
166       case NameSource.CNAP:
167         return phoneLookupInfo.getCnapInfo().getName();
168       case NameSource.PHONE_NUMBER_CACHE:
169         return phoneLookupInfo.getMigratedInfo().getName();
170       case NameSource.NONE:
171         return "";
172       default:
173         throw Assert.createUnsupportedOperationFailException(
174             String.format("Unsupported name source: %s", nameSource));
175     }
176   }
177 
178   /**
179    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
180    * returns the photo thumbnail URI associated with that number.
181    *
182    * <p>If no photo thumbnail URI can be obtained from the {@link PhoneLookupInfo}, an empty string
183    * will be returned.
184    */
getPhotoThumbnailUri()185   public String getPhotoThumbnailUri() {
186     switch (nameSource) {
187       case NameSource.CP2_DEFAULT_DIRECTORY:
188         return Assert.isNotNull(firstDefaultCp2Contact).getPhotoThumbnailUri();
189       case NameSource.CP2_EXTENDED_DIRECTORY:
190         return Assert.isNotNull(firstExtendedCp2Contact).getPhotoThumbnailUri();
191       case NameSource.PHONE_NUMBER_CACHE:
192         return phoneLookupInfo.getMigratedInfo().getPhotoUri();
193       case NameSource.PEOPLE_API:
194       case NameSource.CEQUINT:
195       case NameSource.CNAP:
196       case NameSource.NONE:
197         return "";
198       default:
199         throw Assert.createUnsupportedOperationFailException(
200             String.format("Unsupported name source: %s", nameSource));
201     }
202   }
203 
204   /**
205    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
206    * returns the photo URI associated with that number.
207    *
208    * <p>If no photo URI can be obtained from the {@link PhoneLookupInfo}, an empty string will be
209    * returned.
210    */
getPhotoUri()211   public String getPhotoUri() {
212     switch (nameSource) {
213       case NameSource.CP2_DEFAULT_DIRECTORY:
214         return Assert.isNotNull(firstDefaultCp2Contact).getPhotoUri();
215       case NameSource.CP2_EXTENDED_DIRECTORY:
216         return Assert.isNotNull(firstExtendedCp2Contact).getPhotoUri();
217       case NameSource.CEQUINT:
218         return phoneLookupInfo.getCequintInfo().getPhotoUri();
219       case NameSource.PHONE_NUMBER_CACHE:
220         return phoneLookupInfo.getMigratedInfo().getPhotoUri();
221       case NameSource.PEOPLE_API:
222       case NameSource.CNAP:
223       case NameSource.NONE:
224         return "";
225       default:
226         throw Assert.createUnsupportedOperationFailException(
227             String.format("Unsupported name source: %s", nameSource));
228     }
229   }
230 
231   /**
232    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
233    * returns the photo ID associated with that number, or 0 if there is none.
234    */
getPhotoId()235   public long getPhotoId() {
236     switch (nameSource) {
237       case NameSource.CP2_DEFAULT_DIRECTORY:
238         return Math.max(Assert.isNotNull(firstDefaultCp2Contact).getPhotoId(), 0);
239       case NameSource.CP2_EXTENDED_DIRECTORY:
240         return Math.max(Assert.isNotNull(firstExtendedCp2Contact).getPhotoId(), 0);
241       case NameSource.PHONE_NUMBER_CACHE:
242       case NameSource.PEOPLE_API:
243       case NameSource.CEQUINT:
244       case NameSource.CNAP:
245       case NameSource.NONE:
246         return 0;
247       default:
248         throw Assert.createUnsupportedOperationFailException(
249             String.format("Unsupported name source: %s", nameSource));
250     }
251   }
252 
253   /**
254    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
255    * returns the lookup URI associated with that number, or an empty string if no lookup URI can be
256    * obtained.
257    */
getLookupUri()258   public String getLookupUri() {
259     switch (nameSource) {
260       case NameSource.CP2_DEFAULT_DIRECTORY:
261         return Assert.isNotNull(firstDefaultCp2Contact).getLookupUri();
262       case NameSource.CP2_EXTENDED_DIRECTORY:
263         return Assert.isNotNull(firstExtendedCp2Contact).getLookupUri();
264       case NameSource.PEOPLE_API:
265         return Assert.isNotNull(phoneLookupInfo.getPeopleApiInfo().getLookupUri());
266       case NameSource.PHONE_NUMBER_CACHE:
267       case NameSource.CEQUINT:
268       case NameSource.CNAP:
269       case NameSource.NONE:
270         return "";
271       default:
272         throw Assert.createUnsupportedOperationFailException(
273             String.format("Unsupported name source: %s", nameSource));
274     }
275   }
276 
277   /**
278    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
279    * returns a localized string representing the number type such as "Home" or "Mobile", or a custom
280    * value set by the user.
281    *
282    * <p>If no label can be obtained from the {@link PhoneLookupInfo}, an empty string will be
283    * returned.
284    */
getNumberLabel()285   public String getNumberLabel() {
286     switch (nameSource) {
287       case NameSource.CP2_DEFAULT_DIRECTORY:
288         return Assert.isNotNull(firstDefaultCp2Contact).getLabel();
289       case NameSource.CP2_EXTENDED_DIRECTORY:
290         return Assert.isNotNull(firstExtendedCp2Contact).getLabel();
291       case NameSource.PHONE_NUMBER_CACHE:
292         return phoneLookupInfo.getMigratedInfo().getLabel();
293       case NameSource.PEOPLE_API:
294       case NameSource.CEQUINT:
295       case NameSource.CNAP:
296       case NameSource.NONE:
297         return "";
298       default:
299         throw Assert.createUnsupportedOperationFailException(
300             String.format("Unsupported name source: %s", nameSource));
301     }
302   }
303 
304   /**
305    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
306    * returns the number's geolocation (which is for display purpose only).
307    *
308    * <p>If no geolocation can be obtained from the {@link PhoneLookupInfo}, an empty string will be
309    * returned.
310    */
getGeolocation()311   public String getGeolocation() {
312     switch (nameSource) {
313       case NameSource.CEQUINT:
314         return phoneLookupInfo.getCequintInfo().getGeolocation();
315       case NameSource.CP2_DEFAULT_DIRECTORY:
316       case NameSource.CP2_EXTENDED_DIRECTORY:
317       case NameSource.PEOPLE_API:
318       case NameSource.CNAP:
319       case NameSource.PHONE_NUMBER_CACHE:
320       case NameSource.NONE:
321         return "";
322       default:
323         throw Assert.createUnsupportedOperationFailException(
324             String.format("Unsupported name source: %s", nameSource));
325     }
326   }
327 
328   /**
329    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
330    * returns whether the number belongs to a business place.
331    */
isBusiness()332   public boolean isBusiness() {
333     switch (nameSource) {
334       case NameSource.PEOPLE_API:
335         return phoneLookupInfo.getPeopleApiInfo().getInfoType() == InfoType.NEARBY_BUSINESS;
336       case NameSource.PHONE_NUMBER_CACHE:
337         return phoneLookupInfo.getMigratedInfo().getIsBusiness();
338       case NameSource.CP2_DEFAULT_DIRECTORY:
339       case NameSource.CP2_EXTENDED_DIRECTORY:
340       case NameSource.CEQUINT:
341       case NameSource.CNAP:
342       case NameSource.NONE:
343         return false;
344       default:
345         throw Assert.createUnsupportedOperationFailException(
346             String.format("Unsupported name source: %s", nameSource));
347     }
348   }
349 
350   /**
351    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
352    * returns whether the number is blocked.
353    */
isBlocked()354   public boolean isBlocked() {
355     // If system blocking reported blocked state it always takes priority over the dialer blocking.
356     // It will be absent if dialer blocking should be used.
357     if (phoneLookupInfo.getSystemBlockedNumberInfo().hasBlockedState()) {
358       return phoneLookupInfo
359           .getSystemBlockedNumberInfo()
360           .getBlockedState()
361           .equals(BlockedState.BLOCKED);
362     }
363     return false;
364   }
365 
366   /**
367    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
368    * returns whether the number is spam.
369    */
isSpam()370   public boolean isSpam() {
371     return phoneLookupInfo.getSpamInfo().getIsSpam();
372   }
373 
374   /**
375    * Returns true if the {@link PhoneLookupInfo} passed to the constructor has incomplete default
376    * CP2 info (info from the default directory).
377    */
isDefaultCp2InfoIncomplete()378   public boolean isDefaultCp2InfoIncomplete() {
379     return phoneLookupInfo.getDefaultCp2Info().getIsIncomplete();
380   }
381 
382   /**
383    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
384    * returns whether the number is an emergency number (e.g., 911 in the U.S.).
385    */
isEmergencyNumber()386   public boolean isEmergencyNumber() {
387     return phoneLookupInfo.getEmergencyInfo().getIsEmergencyNumber();
388   }
389 
390   /**
391    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
392    * returns whether the number can be reported as invalid.
393    *
394    * <p>As we currently report invalid numbers via the People API, only numbers from the People API
395    * can be reported as invalid.
396    */
canReportAsInvalidNumber()397   public boolean canReportAsInvalidNumber() {
398     switch (nameSource) {
399       case NameSource.CP2_DEFAULT_DIRECTORY:
400       case NameSource.CP2_EXTENDED_DIRECTORY:
401       case NameSource.CEQUINT:
402       case NameSource.CNAP:
403       case NameSource.PHONE_NUMBER_CACHE:
404       case NameSource.NONE:
405         return false;
406       case NameSource.PEOPLE_API:
407         PeopleApiInfo peopleApiInfo = phoneLookupInfo.getPeopleApiInfo();
408         return peopleApiInfo.getInfoType() != InfoType.UNKNOWN
409             && !peopleApiInfo.getPersonId().isEmpty();
410       default:
411         throw Assert.createUnsupportedOperationFailException(
412             String.format("Unsupported name source: %s", nameSource));
413     }
414   }
415 
416   /**
417    * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
418    * returns whether the number can be reached via carrier video calls.
419    */
canSupportCarrierVideoCall()420   public boolean canSupportCarrierVideoCall() {
421     switch (nameSource) {
422       case NameSource.CP2_DEFAULT_DIRECTORY:
423         return Assert.isNotNull(firstDefaultCp2Contact).getCanSupportCarrierVideoCall();
424       case NameSource.CP2_EXTENDED_DIRECTORY:
425       case NameSource.PEOPLE_API:
426       case NameSource.CEQUINT:
427       case NameSource.CNAP:
428       case NameSource.PHONE_NUMBER_CACHE:
429       case NameSource.NONE:
430         return false;
431       default:
432         throw Assert.createUnsupportedOperationFailException(
433             String.format("Unsupported name source: %s", nameSource));
434     }
435   }
436 
437   /**
438    * Arbitrarily select the first CP2 contact in the default directory. In the future, it may make
439    * sense to display contact information from all contacts with the same number (for example show
440    * the name as "Mom, Dad" or show a synthesized photo containing photos of both "Mom" and "Dad").
441    */
442   @Nullable
getFirstContactInDefaultDirectory()443   private Cp2ContactInfo getFirstContactInDefaultDirectory() {
444     return phoneLookupInfo.getDefaultCp2Info().getCp2ContactInfoCount() > 0
445         ? phoneLookupInfo.getDefaultCp2Info().getCp2ContactInfo(0)
446         : null;
447   }
448 
449   /**
450    * Arbitrarily select the first CP2 contact in extended directories. In the future, it may make
451    * sense to display contact information from all contacts with the same number (for example show
452    * the name as "Mom, Dad" or show a synthesized photo containing photos of both "Mom" and "Dad").
453    */
454   @Nullable
getFirstContactInExtendedDirectories()455   private Cp2ContactInfo getFirstContactInExtendedDirectories() {
456     return phoneLookupInfo.getExtendedCp2Info().getCp2ContactInfoCount() > 0
457         ? phoneLookupInfo.getExtendedCp2Info().getCp2ContactInfo(0)
458         : null;
459   }
460 
461   /** Select the {@link PhoneLookup} source providing a contact's name. */
selectNameSource()462   private @NameSource int selectNameSource() {
463     for (int nameSource : NAME_SOURCES_IN_PRIORITY_ORDER) {
464       switch (nameSource) {
465         case NameSource.CP2_DEFAULT_DIRECTORY:
466           if (firstDefaultCp2Contact != null && !firstDefaultCp2Contact.getName().isEmpty()) {
467             return NameSource.CP2_DEFAULT_DIRECTORY;
468           }
469           break;
470         case NameSource.CP2_EXTENDED_DIRECTORY:
471           if (firstExtendedCp2Contact != null && !firstExtendedCp2Contact.getName().isEmpty()) {
472             return NameSource.CP2_EXTENDED_DIRECTORY;
473           }
474           break;
475         case NameSource.PEOPLE_API:
476           if (phoneLookupInfo.hasPeopleApiInfo()
477               && !phoneLookupInfo.getPeopleApiInfo().getDisplayName().isEmpty()) {
478             return NameSource.PEOPLE_API;
479           }
480           break;
481         case NameSource.CEQUINT:
482           if (!phoneLookupInfo.getCequintInfo().getName().isEmpty()) {
483             return NameSource.CEQUINT;
484           }
485           break;
486         case NameSource.CNAP:
487           if (!phoneLookupInfo.getCnapInfo().getName().isEmpty()) {
488             return NameSource.CNAP;
489           }
490           break;
491         case NameSource.PHONE_NUMBER_CACHE:
492           if (!phoneLookupInfo.getMigratedInfo().getName().isEmpty()) {
493             return NameSource.PHONE_NUMBER_CACHE;
494           }
495           break;
496         default:
497           throw Assert.createUnsupportedOperationFailException(
498               String.format("Unsupported name source: %s", nameSource));
499       }
500     }
501 
502     return NameSource.NONE;
503   }
504 }
505