/* * 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