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