Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 31e83f3e authored by linyuh's avatar linyuh Committed by Copybara-Service
Browse files

Replace PhoneLookupSelector with PhoneLookupInfoConsolidator.

PhoneLookupInfoConsolidator is designed for the following two purposes.

(1) Different sub-messages in a PhoneLookupInfo proto can contain information for the same purpose. For example, all of cp2_local_info, cp2_remote_info, and people_api_info have the information for a contact's name. PhoneLookupInfoConsolidator defines the rules that determine which sub-message should be used to display the name in the UI. This is the same as PhoneLookupSelector.

(2) Avoid mixing info from different sub-messages when we are supposed to stick with only one sub-message. For example, if a PhoneLookupInfo proto has both cp2_local_info and cp2_remote_info but only cp2_remote_info has a photo URI, PhoneLookupInfoConsolidator should return an *empty* photo URI as cp2_local_info has higher priority and we should not use cp2_remote_info's photo URI to display the contact's photo. This is what PhoneLookupSelector is unable to do.

Bug: 71763594
Test: PhoneLookupInfoConsolidatorTest
PiperOrigin-RevId: 182236013
Change-Id: If19cdc1a9e076f3ebc8f9e2901f050b519e273f2
parent bb41b26b
Loading
Loading
Loading
Loading
+16 −14
Original line number Diff line number Diff line
@@ -37,11 +37,12 @@ import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
import com.android.dialer.inject.ApplicationContext;
import com.android.dialer.phonelookup.PhoneLookup;
import com.android.dialer.phonelookup.PhoneLookupInfo;
import com.android.dialer.phonelookup.consolidator.PhoneLookupInfoConsolidator;
import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract;
import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
import com.android.dialer.phonelookup.selector.PhoneLookupSelector;
import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -67,8 +68,8 @@ import javax.inject.Inject;
public final class PhoneLookupDataSource
    implements CallLogDataSource, PhoneLookup.ContentObserverCallbacks {

  private final Context appContext;
  private final PhoneLookup<PhoneLookupInfo> phoneLookup;
  private final PhoneLookupSelector phoneLookupSelector;
  private final ListeningExecutorService backgroundExecutorService;
  private final ListeningExecutorService lightweightExecutorService;

@@ -94,11 +95,11 @@ public final class PhoneLookupDataSource
  @Inject
  PhoneLookupDataSource(
      PhoneLookup<PhoneLookupInfo> phoneLookup,
      PhoneLookupSelector phoneLookupSelector,
      @ApplicationContext Context appContext,
      @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
      @LightweightExecutor ListeningExecutorService lightweightExecutorService) {
    this.phoneLookup = phoneLookup;
    this.phoneLookupSelector = phoneLookupSelector;
    this.appContext = appContext;
    this.backgroundExecutorService = backgroundExecutorService;
    this.lightweightExecutorService = lightweightExecutorService;
  }
@@ -579,19 +580,20 @@ public final class PhoneLookupDataSource
  }

  private void updateContentValues(ContentValues contentValues, PhoneLookupInfo phoneLookupInfo) {
    PhoneLookupInfoConsolidator phoneLookupInfoConsolidator =
        new PhoneLookupInfoConsolidator(appContext, phoneLookupInfo);
    contentValues.put(
        AnnotatedCallLog.NUMBER_ATTRIBUTES,
        NumberAttributes.newBuilder()
            .setName(phoneLookupSelector.selectName(phoneLookupInfo))
            .setPhotoUri(phoneLookupSelector.selectPhotoUri(phoneLookupInfo))
            .setPhotoId(phoneLookupSelector.selectPhotoId(phoneLookupInfo))
            .setLookupUri(phoneLookupSelector.selectLookupUri(phoneLookupInfo))
            .setNumberTypeLabel(phoneLookupSelector.selectNumberLabel(phoneLookupInfo))
            .setIsBusiness(phoneLookupSelector.selectIsBusiness(phoneLookupInfo))
            .setIsVoicemail(phoneLookupSelector.selectIsVoicemail(phoneLookupInfo))
            .setCanReportAsInvalidNumber(
                phoneLookupSelector.canReportAsInvalidNumber(phoneLookupInfo))
            .setIsCp2InfoIncomplete(phoneLookupSelector.selectIsCp2InfoIncomplete(phoneLookupInfo))
            .setName(phoneLookupInfoConsolidator.getName())
            .setPhotoUri(phoneLookupInfoConsolidator.getPhotoUri())
            .setPhotoId(phoneLookupInfoConsolidator.getPhotoId())
            .setLookupUri(phoneLookupInfoConsolidator.getLookupUri())
            .setNumberTypeLabel(phoneLookupInfoConsolidator.getNumberLabel())
            .setIsBusiness(phoneLookupInfoConsolidator.isBusiness())
            .setIsVoicemail(phoneLookupInfoConsolidator.isVoicemail())
            .setCanReportAsInvalidNumber(phoneLookupInfoConsolidator.canReportAsInvalidNumber())
            .setIsCp2InfoIncomplete(phoneLookupInfoConsolidator.isCp2LocalInfoIncomplete())
            .build()
            .toByteArray());
  }
+18 −15
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.dialer.calllog.ui;

import android.content.Context;
import android.support.annotation.MainThread;
import android.util.ArrayMap;
import com.android.dialer.DialerPhoneNumber;
@@ -23,10 +24,11 @@ import com.android.dialer.NumberAttributes;
import com.android.dialer.calllog.model.CoalescedRow;
import com.android.dialer.common.Assert;
import com.android.dialer.common.concurrent.Annotations.Ui;
import com.android.dialer.inject.ApplicationContext;
import com.android.dialer.phonelookup.PhoneLookupInfo;
import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
import com.android.dialer.phonelookup.consolidator.PhoneLookupInfoConsolidator;
import com.android.dialer.phonelookup.cp2.Cp2LocalPhoneLookup;
import com.android.dialer.phonelookup.selector.PhoneLookupSelector;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -47,20 +49,20 @@ import javax.inject.Inject;
 */
public final class RealtimeRowProcessor {

  private final ListeningExecutorService uiExecutor;
  private final Context appContext;
  private final Cp2LocalPhoneLookup cp2LocalPhoneLookup;
  private final PhoneLookupSelector phoneLookupSelector;
  private final ListeningExecutorService uiExecutor;

  private final Map<DialerPhoneNumber, Cp2Info> cache = new ArrayMap<>();

  @Inject
  RealtimeRowProcessor(
      @ApplicationContext Context appContext,
      @Ui ListeningExecutorService uiExecutor,
      Cp2LocalPhoneLookup cp2LocalPhoneLookup,
      PhoneLookupSelector phoneLookupSelector) {
      Cp2LocalPhoneLookup cp2LocalPhoneLookup) {
    this.appContext = appContext;
    this.uiExecutor = uiExecutor;
    this.cp2LocalPhoneLookup = cp2LocalPhoneLookup;
    this.phoneLookupSelector = phoneLookupSelector;
  }

  /**
@@ -104,19 +106,20 @@ public final class RealtimeRowProcessor {

  private CoalescedRow applyCp2LocalInfoToRow(Cp2Info cp2Info, CoalescedRow row) {
    PhoneLookupInfo phoneLookupInfo = PhoneLookupInfo.newBuilder().setCp2LocalInfo(cp2Info).build();
    PhoneLookupInfoConsolidator phoneLookupInfoConsolidator =
        new PhoneLookupInfoConsolidator(appContext, phoneLookupInfo);
    // It is safe to overwrite any existing data because CP2 always has highest priority.
    return row.toBuilder()
        .setNumberAttributes(
            NumberAttributes.newBuilder()
                .setName(phoneLookupSelector.selectName(phoneLookupInfo))
                .setPhotoUri(phoneLookupSelector.selectPhotoUri(phoneLookupInfo))
                .setPhotoId(phoneLookupSelector.selectPhotoId(phoneLookupInfo))
                .setLookupUri(phoneLookupSelector.selectLookupUri(phoneLookupInfo))
                .setNumberTypeLabel(phoneLookupSelector.selectNumberLabel(phoneLookupInfo))
                .setIsBusiness(phoneLookupSelector.selectIsBusiness(phoneLookupInfo))
                .setIsVoicemail(phoneLookupSelector.selectIsVoicemail(phoneLookupInfo))
                .setCanReportAsInvalidNumber(
                    phoneLookupSelector.canReportAsInvalidNumber(phoneLookupInfo))
                .setName(phoneLookupInfoConsolidator.getName())
                .setPhotoUri(phoneLookupInfoConsolidator.getPhotoUri())
                .setPhotoId(phoneLookupInfoConsolidator.getPhotoId())
                .setLookupUri(phoneLookupInfoConsolidator.getLookupUri())
                .setNumberTypeLabel(phoneLookupInfoConsolidator.getNumberLabel())
                .setIsBusiness(phoneLookupInfoConsolidator.isBusiness())
                .setIsVoicemail(phoneLookupInfoConsolidator.isVoicemail())
                .setCanReportAsInvalidNumber(phoneLookupInfoConsolidator.canReportAsInvalidNumber())
                .build())
        .build();
  }
+1 −1
Original line number Diff line number Diff line
@@ -14,5 +14,5 @@
 ~ limitations under the License
 -->
<manifest
    package="com.android.dialer.phonelookup.selector">
    package="com.android.dialer.phonelookup.consolidator">
</manifest>
+306 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */
package com.android.dialer.phonelookup.consolidator;

import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.android.dialer.common.Assert;
import com.android.dialer.phonelookup.PhoneLookup;
import com.android.dialer.phonelookup.PhoneLookupInfo;
import com.android.dialer.phonelookup.PhoneLookupInfo.BlockedState;
import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo;
import com.android.dialer.phonelookup.PhoneLookupInfo.PeopleApiInfo;
import com.android.dialer.phonelookup.PhoneLookupInfo.PeopleApiInfo.InfoType;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Consolidates information from a {@link PhoneLookupInfo}.
 *
 * <p>For example, a single {@link PhoneLookupInfo} may contain different name information from many
 * different {@link PhoneLookup} implementations. This class defines the rules for deciding which
 * name should be selected for display to the user, by prioritizing the data from some {@link
 * PhoneLookup PhoneLookups} over others.
 */
public final class PhoneLookupInfoConsolidator {

  /** Integers representing {@link PhoneLookup} implementations that can provide a contact's name */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({NameSource.NONE, NameSource.CP2_LOCAL, NameSource.CP2_REMOTE, NameSource.PEOPLE_API})
  @interface NameSource {
    int NONE = 0; // used when none of the other sources can provide the name
    int CP2_LOCAL = 1;
    int CP2_REMOTE = 2;
    int PEOPLE_API = 3;
  }

  /**
   * Sources that can provide information about a contact's name.
   *
   * <p>Each source is one of the values in NameSource, as defined above.
   *
   * <p>Sources are sorted in the order of priority. For example, if source CP2_LOCAL can provide
   * the name, we will use that name in the UI and ignore all the other sources. If source CP2_LOCAL
   * can't provide the name, source CP2_REMOTE will be consulted.
   *
   * <p>The reason for defining a name source is to avoid mixing info from different sub-messages in
   * PhoneLookupInfo proto when we are supposed to stick with only one sub-message. For example, if
   * a PhoneLookupInfo proto has both cp2_local_info and cp2_remote_info but only cp2_remote_info
   * has a photo URI, PhoneLookupInfoConsolidator should provide an empty photo URI as CP2_LOCAL has
   * higher priority and we should not use cp2_remote_info's photo URI to display the contact's
   * photo.
   */
  private static final ImmutableList<Integer> NAME_SOURCES_IN_PRIORITY_ORDER =
      ImmutableList.of(NameSource.CP2_LOCAL, NameSource.CP2_REMOTE, NameSource.PEOPLE_API);

  private final Context appContext;
  private final @NameSource int nameSource;
  private final PhoneLookupInfo phoneLookupInfo;

  @Nullable private final Cp2ContactInfo firstCp2LocalContact;
  @Nullable private final Cp2ContactInfo firstCp2RemoteContact;

  public PhoneLookupInfoConsolidator(Context appContext, PhoneLookupInfo phoneLookupInfo) {
    this.appContext = appContext;
    this.phoneLookupInfo = phoneLookupInfo;

    this.firstCp2LocalContact = getFirstLocalContact();
    this.firstCp2RemoteContact = getFirstRemoteContact();
    this.nameSource = selectNameSource();
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns the name associated with that number.
   *
   * <p>Examples of this are a local contact's name or a business name received from caller ID.
   *
   * <p>If no name can be obtained from the {@link PhoneLookupInfo}, an empty string will be
   * returned.
   */
  public String getName() {
    switch (nameSource) {
      case NameSource.CP2_LOCAL:
        return Assert.isNotNull(firstCp2LocalContact).getName();
      case NameSource.CP2_REMOTE:
        return Assert.isNotNull(firstCp2RemoteContact).getName();
      case NameSource.PEOPLE_API:
        return phoneLookupInfo.getPeopleApiInfo().getDisplayName();
      case NameSource.NONE:
        return "";
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported name source: %s", nameSource));
    }
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns the photo URI associated with that number.
   *
   * <p>If no photo URI can be obtained from the {@link PhoneLookupInfo}, an empty string will be
   * returned.
   */
  public String getPhotoUri() {
    switch (nameSource) {
      case NameSource.CP2_LOCAL:
        return Assert.isNotNull(firstCp2LocalContact).getPhotoUri();
      case NameSource.CP2_REMOTE:
        return Assert.isNotNull(firstCp2RemoteContact).getPhotoUri();
      case NameSource.PEOPLE_API:
      case NameSource.NONE:
        return "";
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported name source: %s", nameSource));
    }
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns the photo ID associated with that number, or 0 if there is none.
   */
  public long getPhotoId() {
    switch (nameSource) {
      case NameSource.CP2_LOCAL:
        return Math.max(Assert.isNotNull(firstCp2LocalContact).getPhotoId(), 0);
      case NameSource.CP2_REMOTE:
        return Math.max(Assert.isNotNull(firstCp2RemoteContact).getPhotoId(), 0);
      case NameSource.PEOPLE_API:
      case NameSource.NONE:
        return 0;
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported name source: %s", nameSource));
    }
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns the lookup URI associated with that number, or an empty string if no lookup URI can be
   * obtained.
   */
  public String getLookupUri() {
    switch (nameSource) {
      case NameSource.CP2_LOCAL:
        return Assert.isNotNull(firstCp2LocalContact).getLookupUri();
      case NameSource.CP2_REMOTE:
        return Assert.isNotNull(firstCp2RemoteContact).getLookupUri();
      case NameSource.PEOPLE_API:
      case NameSource.NONE:
        return "";
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported name source: %s", nameSource));
    }
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns a localized string representing the number type such as "Home" or "Mobile", or a custom
   * value set by the user.
   *
   * <p>If no label can be obtained from the {@link PhoneLookupInfo}, an empty string will be
   * returned.
   */
  public String getNumberLabel() {
    if (phoneLookupInfo.hasDialerBlockedNumberInfo()
        && phoneLookupInfo
            .getDialerBlockedNumberInfo()
            .getBlockedState()
            .equals(BlockedState.BLOCKED)) {
      return appContext.getString(R.string.blocked_number_new_call_log_label);
    }

    switch (nameSource) {
      case NameSource.CP2_LOCAL:
        return Assert.isNotNull(firstCp2LocalContact).getLabel();
      case NameSource.CP2_REMOTE:
        return Assert.isNotNull(firstCp2RemoteContact).getLabel();
      case NameSource.PEOPLE_API:
      case NameSource.NONE:
        return "";
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported name source: %s", nameSource));
    }
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns whether the number belongs to a business place.
   */
  public boolean isBusiness() {
    return phoneLookupInfo.hasPeopleApiInfo()
        && phoneLookupInfo.getPeopleApiInfo().getInfoType() == InfoType.NEARBY_BUSINESS;
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns whether the number is a voicemail number.
   */
  public boolean isVoicemail() {
    // TODO(twyen): implement
    return false;
  }

  /**
   * Returns true if the {@link PhoneLookupInfo} passed to the constructor has incomplete CP2 local
   * info.
   */
  public boolean isCp2LocalInfoIncomplete() {
    return phoneLookupInfo.getCp2LocalInfo().getIsIncomplete();
  }

  /**
   * The {@link PhoneLookupInfo} passed to the constructor is associated with a number. This method
   * returns whether the number can be reported as invalid.
   *
   * <p>As we currently report invalid numbers via the People API, only numbers from the People API
   * can be reported as invalid.
   */
  public boolean canReportAsInvalidNumber() {
    switch (nameSource) {
      case NameSource.CP2_LOCAL:
      case NameSource.CP2_REMOTE:
        return false;
      case NameSource.PEOPLE_API:
        PeopleApiInfo peopleApiInfo = phoneLookupInfo.getPeopleApiInfo();
        return peopleApiInfo.getInfoType() != InfoType.UNKNOWN
            && !peopleApiInfo.getPersonId().isEmpty();
      case NameSource.NONE:
        return false;
      default:
        throw Assert.createUnsupportedOperationFailException(
            String.format("Unsupported name source: %s", nameSource));
    }
  }

  /**
   * Arbitrarily select the first local CP2 contact. In the future, it may make sense to display
   * contact information from all contacts with the same number (for example show the name as "Mom,
   * Dad" or show a synthesized photo containing photos of both "Mom" and "Dad").
   */
  @Nullable
  private Cp2ContactInfo getFirstLocalContact() {
    return phoneLookupInfo.getCp2LocalInfo().getCp2ContactInfoCount() > 0
        ? phoneLookupInfo.getCp2LocalInfo().getCp2ContactInfo(0)
        : null;
  }

  /**
   * Arbitrarily select the first remote CP2 contact. In the future, it may make sense to display
   * contact information from all contacts with the same number (for example show the name as "Mom,
   * Dad" or show a synthesized photo containing photos of both "Mom" and "Dad").
   */
  @Nullable
  private Cp2ContactInfo getFirstRemoteContact() {
    return phoneLookupInfo.getCp2RemoteInfo().getCp2ContactInfoCount() > 0
        ? phoneLookupInfo.getCp2RemoteInfo().getCp2ContactInfo(0)
        : null;
  }

  /** Select the {@link PhoneLookup} source providing a contact's name. */
  private @NameSource int selectNameSource() {
    for (int nameSource : NAME_SOURCES_IN_PRIORITY_ORDER) {
      switch (nameSource) {
        case NameSource.CP2_LOCAL:
          if (firstCp2LocalContact != null && !firstCp2LocalContact.getName().isEmpty()) {
            return NameSource.CP2_LOCAL;
          }
          break;
        case NameSource.CP2_REMOTE:
          if (firstCp2RemoteContact != null && !firstCp2RemoteContact.getName().isEmpty()) {
            return NameSource.CP2_REMOTE;
          }
          break;
        case NameSource.PEOPLE_API:
          if (phoneLookupInfo.hasPeopleApiInfo()
              && !phoneLookupInfo.getPeopleApiInfo().getDisplayName().isEmpty()) {
            return NameSource.PEOPLE_API;
          }
          break;
        default:
          throw Assert.createUnsupportedOperationFailException(
              String.format("Unsupported name source: %s", nameSource));
      }
    }

    return NameSource.NONE;
  }
}
Loading