/* * 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.database.Cursor; 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 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; /** * 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(Cursor cursor) { 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 channels = new ArrayList<>(); do { Channel channel = Channel.builder() .setNumber(cursor.getString(NUMBER)) .setPhoneType(cursor.getInt(TYPE)) .setLabel(TextUtils.isEmpty(cursor.getString(LABEL)) ? "" : cursor.getString(LABEL)) .setTechnology(Channel.VOICE) .build(); channels.add(channel); if ((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)); return builder.build(); } 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 a video channel if there is exactly one video channel or the default channel is a video * channel. */ @Nullable public Channel getDeterministicVideoChannel() { if (defaultChannel() != null && defaultChannel().isVideoTechnology()) { return defaultChannel(); } Channel videoChannel = null; for (Channel channel : channels()) { if (channel.isVideoTechnology()) { if (videoChannel != null) { // We found two video channels, so we can't determine which one is correct.. return null; } videoChannel = channel; } } // Only found one channel, so return it return videoChannel; } /** Returns true if any channels are video channels. */ public boolean hasVideoChannels() { for (Channel channel : channels()) { if (channel.isVideoTechnology()) { return true; } } return false; } /** * Returns a voice channel if there is exactly one voice channel or the default channel is a voice * channel. */ @Nullable public Channel getDeterministicVoiceChannel() { if (defaultChannel() != null && !defaultChannel().isVideoTechnology()) { return defaultChannel(); } Channel voiceChannel = null; for (Channel channel : channels()) { if (!channel.isVideoTechnology()) { if (voiceChannel != null) { // We found two voice channels, so we can't determine which one is correct.. return null; } voiceChannel = channel; } } // Only found one channel, so return it return voiceChannel; } /** * 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 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(); /** * 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}. * * @see com.android.dialer.speeddial.database.SpeedDialEntry.Channel */ public abstract ImmutableList channels(); /** * Will be null when the user hasn't chosen a default yet. * * @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 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 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(); } }