packages/apps/Dialer/java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java

374 lines
14 KiB
Java

/*
* 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.
*
* <p>Contains all data needed for the UI so that the UI never needs do additional contact queries.
*
* <p>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}.
*
* <p>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.
*
* <p>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<Channel> channels = new ArrayList<>();
Set<String> numbers = new ArraySet<>();
do {
String number = cursor.getString(NUMBER);
// TODO(78492722): consider using lib phone number to compare numbers
if (!numbers.add(number)) {
// Number is identical to an existing number, skip this number
continue;
}
Channel channel =
Channel.builder()
.setNumber(number)
.setPhoneType(cursor.getInt(TYPE))
.setLabel(getLabel(resources, cursor))
.setTechnology(Channel.VOICE)
.build();
channels.add(channel);
if (isImsEnabled
&& (cursor.getInt(CARRIER_PRESENCE) & Data.CARRIER_PRESENCE_VT_CAPABLE) == 1) {
// Add another channel if the number is ViLTE reachable
channels.add(channel.toBuilder().setTechnology(Channel.IMS_VIDEO).build());
}
// TODO(a bug): add another channel for Duo (needs to happen on main thread)
} while (cursor.moveToNext() && Objects.equals(lookupKey, cursor.getString(LOOKUP_KEY)));
builder.setChannels(ImmutableList.copyOf(channels));
Trace.endSection();
return builder.build();
}
private static String getLabel(Resources resources, Cursor cursor) {
int numberType = cursor.getInt(TYPE);
String numberLabel = cursor.getString(LABEL);
// Returns empty label instead of "custom" if the custom label is empty.
if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) {
return "";
}
return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
}
public PhotoInfo getPhotoInfo() {
return PhotoInfo.newBuilder()
.setPhotoId(photoId())
.setPhotoUri(photoUri())
.setName(name())
.setIsVideo(defaultChannel() != null && defaultChannel().isVideoTechnology())
.setLookupUri(Contacts.getLookupUri(contactId(), lookupKey()).toString())
.build();
}
public SpeedDialEntry buildSpeedDialEntry() {
return SpeedDialEntry.builder()
.setId(speedDialEntryId())
.setPinnedPosition(pinnedPosition())
.setLookupKey(lookupKey())
.setContactId(contactId())
.setDefaultChannel(defaultChannel())
.build();
}
/**
* Returns one of the following:
*
* <ul>
* <li>The default channel if it's a video channel.
* <li>A video channel if it has the same attributes as the default channel, OR
* <li>null. (This is a deliberate product decision, even if there is only a single video
* reachable channel, we should still return null if it has different attributes from those
* in the default channel).
* </ul>
*/
@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<Integer> pinnedPosition();
/** @see android.provider.ContactsContract.Contacts#DISPLAY_NAME */
public abstract String name();
/** @see android.provider.ContactsContract.Contacts#_ID */
public abstract long contactId();
/** @see android.provider.ContactsContract.Contacts#LOOKUP_KEY */
public abstract String lookupKey();
/** @see android.provider.ContactsContract.Contacts#STARRED */
public abstract boolean isStarred();
/** @see Phone#PHOTO_ID */
public abstract long photoId();
/** @see Phone#PHOTO_URI */
public abstract String photoUri();
/**
* Returns a list of channels available. A Duo channel is included iff it is reachable. Since a
* contact can have multiple phone numbers and each number can have multiple technologies,
* enumerate each one here so that the user can choose the correct one. Each channel here
* represents a row in the {@link com.android.dialer.speeddial.DisambigDialog}.
*
* <p>These channels have a few very strictly enforced assumption that are used heavily throughout
* the codebase. Those assumption are that:
*
* <ol>
* <li>Each of the contact's numbers are voice reachable. So if a channel has it's technology
* set to anything other than {@link Channel#VOICE}, there is gaurenteed to be another
* channel with the exact same attributes, but technology will be {@link Channel#VOICE}.
* <li>For each of the contact's phone numbers, there will be a voice channel, then the next
* channel will either be the same phone number but a video channel, or a new number.
* </ol>
*
* 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<Channel> channels();
/**
* Will be null when the user hasn't chosen a default yet. Note that a default channel may not be
* in the list returned by {@link #channels()}. This is because that list does not contain an
* unreachable Duo channel. When the default channel is a Duo channel and it becomes unreachable,
* it will remain as the default channel but disappear in the list returned by {@link
* #channels()}.
*
* @see com.android.dialer.speeddial.database.SpeedDialEntry#defaultChannel()
*/
public abstract @Nullable Channel defaultChannel();
public abstract Builder toBuilder();
/** Builder class for speed dial contact. */
@AutoValue.Builder
public abstract static class Builder {
/** Set to null if {@link #isStarred()} is false. */
public abstract Builder setSpeedDialEntryId(@Nullable Long id);
public abstract Builder setPinnedPosition(Optional<Integer> pinnedPosition);
public abstract Builder setName(String name);
public abstract Builder setContactId(long contactId);
public abstract Builder setLookupKey(String lookupKey);
public abstract Builder setIsStarred(boolean isStarred);
public abstract Builder setPhotoId(long photoId);
public abstract Builder setPhotoUri(String photoUri);
public abstract Builder setChannels(ImmutableList<Channel> channels);
/** Set to null if the user hasn't chosen a default or the channel no longer exists. */
public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel);
public abstract SpeedDialUiItem build();
}
}