/* * 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.speeddial.loader; import android.content.res.Resources; import android.database.Cursor; import android.os.Trace; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.ArraySet; import com.android.dialer.common.Assert; import com.android.dialer.glidephotomanager.PhotoInfo; import com.android.dialer.speeddial.database.SpeedDialEntry; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; import com.google.auto.value.AutoValue; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; /** * POJO representation of each speed dial list element. * *
Contains all data needed for the UI so that the UI never needs do additional contact queries. * *
Differs from {@link SpeedDialEntry} in that entries are specific to favorited/starred contacts * and {@link SpeedDialUiItem}s can be both favorites and suggested contacts. */ @AutoValue public abstract class SpeedDialUiItem { public static final int LOOKUP_KEY = 0; public static final int CONTACT_ID = 1; public static final int DISPLAY_NAME = 2; public static final int STARRED = 3; public static final int NUMBER = 4; public static final int TYPE = 5; public static final int LABEL = 6; public static final int PHOTO_ID = 7; public static final int PHOTO_URI = 8; public static final int CARRIER_PRESENCE = 9; private static final String[] PHONE_PROJECTION = { Phone.LOOKUP_KEY, Phone.CONTACT_ID, Phone.DISPLAY_NAME, Phone.STARRED, Phone.NUMBER, Phone.TYPE, Phone.LABEL, Phone.PHOTO_ID, Phone.PHOTO_URI, Phone.CARRIER_PRESENCE }; private static final String[] PHONE_PROJECTION_ALTERNATIVE = { Phone.LOOKUP_KEY, Phone.CONTACT_ID, Phone.DISPLAY_NAME_ALTERNATIVE, Phone.STARRED, Phone.NUMBER, Phone.TYPE, Phone.LABEL, Phone.PHOTO_ID, Phone.PHOTO_URI, Phone.CARRIER_PRESENCE }; public static String[] getPhoneProjection(boolean primaryDisplayOrder) { return primaryDisplayOrder ? PHONE_PROJECTION : PHONE_PROJECTION_ALTERNATIVE; } public static Builder builder() { return new AutoValue_SpeedDialUiItem.Builder() .setChannels(ImmutableList.of()) .setPinnedPosition(Optional.absent()); } /** * Convert a cursor with projection {@link #getPhoneProjection(boolean)} into a {@link * SpeedDialUiItem}. * *
This cursor is structured such that contacts are grouped by contact id and lookup key and * each row that shares the same contact id and lookup key represents a phone number that belongs * to a single contact. * *
If the cursor started at row X, this method will advance to row Y s.t. rows X, X + 1, ... Y
   * - 1 all belong to the same contact (that is, share the same contact id and lookup key).
   */
  public static SpeedDialUiItem fromCursor(
      Resources resources, Cursor cursor, boolean isImsEnabled) {
    Trace.beginSection("fromCursor");
    Assert.checkArgument(cursor != null);
    Assert.checkArgument(cursor.getCount() != 0);
    String lookupKey = cursor.getString(LOOKUP_KEY);
    SpeedDialUiItem.Builder builder =
        SpeedDialUiItem.builder()
            .setLookupKey(lookupKey)
            .setContactId(cursor.getLong(CONTACT_ID))
            // TODO(a bug): handle last name first preference
            .setName(cursor.getString(DISPLAY_NAME))
            .setIsStarred(cursor.getInt(STARRED) == 1)
            .setPhotoId(cursor.getLong(PHOTO_ID))
            .setPhotoUri(
                TextUtils.isEmpty(cursor.getString(PHOTO_URI)) ? "" : cursor.getString(PHOTO_URI));
    // While there are more rows and the lookup keys are the same, add a channel for each of the
    // contact's phone numbers.
    List These channels have a few very strictly enforced assumption that are used heavily throughout
   * the codebase. Those assumption are that:
   *
   * 
   *   
   */
  @Nullable
  public Channel getDefaultVideoChannel() {
    if (defaultChannel() == null) {
      return null;
    }
    if (defaultChannel().isVideoTechnology()) {
      return defaultChannel();
    }
    if (channels().size() == 1) {
      // If there is only a single channel, it can't be a video channel
      return null;
    }
    // At this point, the default channel is a *voice* channel and there are more than
    // one channel in total.
    //
    // Our defined assumptions about the channel list include that if a video channel
    // follows a voice channel, it has the same attributes as that voice channel
    // (see comments on method channels() for details).
    //
    // Therefore, if the default video channel exists, it must be the immediate successor
    // of the default channel in the list.
    //
    // Note that we don't have to check if the last channel in the list is the default
    // channel because even if it is, there will be no video channel under the assumption
    // above.
    for (int i = 0; i < channels().size() - 1; i++) {
      // Find the default channel
      if (Objects.equals(defaultChannel(), channels().get(i))) {
        // Our defined assumptions about the list of channels is that if a video channel follows a
        // voice channel, it has the same attributes as that voice channel.
        Channel channel = channels().get(i + 1);
        if (channel.isVideoTechnology()) {
          return channel;
        }
        // Since the default voice channel isn't video reachable, we can't video call this number
        return null;
      }
    }
    throw Assert.createIllegalStateFailException("channels() doesn't contain defaultChannel().");
  }
  /**
   * Returns a voice channel if there is exactly one channel or the default channel is a voice
   * channel.
   */
  @Nullable
  public Channel getDefaultVoiceChannel() {
    if (channels().size() == 1) {
      // If there is only a single channel, it must be a voice channel as per our defined
      // assumptions (detailed in comments on method channels()).
      return channels().get(0);
    }
    if (defaultChannel() == null) {
      return null;
    }
    if (!defaultChannel().isVideoTechnology()) {
      return defaultChannel();
    }
    // Default channel is a video channel, so find it's corresponding voice channel by number since
    // unreachable channels may not be in the list
    for (Channel currentChannel : channels()) {
      if (currentChannel.number().equals(defaultChannel().number())
          && currentChannel.technology() == Channel.VOICE) {
        return currentChannel;
      }
    }
    return null;
  }
  /**
   * The id of the corresponding SpeedDialEntry. Null if the UI item does not have an entry, for
   * example suggested contacts (isStarred() will also be false)
   *
   * @see SpeedDialEntry#id()
   */
  @Nullable
  public abstract Long speedDialEntryId();
  /** @see SpeedDialEntry#pinnedPosition() */
  public abstract Optional
   *   
   *
   * For example: Say a contact has two phone numbers (A & B) and A is duo reachable. Then you can
   * assume the list of channels will be ordered as either {A_voice, A_duo, B_voice} or {B_voice,
   * A_voice, A_duo}.
   *
   * @see com.android.dialer.speeddial.database.SpeedDialEntry.Channel
   */
  public abstract ImmutableList