1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.dialer.speeddial.loader; 18 19 import android.content.res.Resources; 20 import android.database.Cursor; 21 import android.os.Trace; 22 import android.provider.ContactsContract.CommonDataKinds.Phone; 23 import android.provider.ContactsContract.Contacts; 24 import android.provider.ContactsContract.Data; 25 import android.support.annotation.Nullable; 26 import android.text.TextUtils; 27 import android.util.ArraySet; 28 import com.android.dialer.common.Assert; 29 import com.android.dialer.glidephotomanager.PhotoInfo; 30 import com.android.dialer.speeddial.database.SpeedDialEntry; 31 import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; 32 import com.google.auto.value.AutoValue; 33 import com.google.common.base.Optional; 34 import com.google.common.collect.ImmutableList; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.Set; 39 40 /** 41 * POJO representation of each speed dial list element. 42 * 43 * <p>Contains all data needed for the UI so that the UI never needs do additional contact queries. 44 * 45 * <p>Differs from {@link SpeedDialEntry} in that entries are specific to favorited/starred contacts 46 * and {@link SpeedDialUiItem}s can be both favorites and suggested contacts. 47 */ 48 @AutoValue 49 public abstract class SpeedDialUiItem { 50 51 public static final int LOOKUP_KEY = 0; 52 public static final int CONTACT_ID = 1; 53 public static final int DISPLAY_NAME = 2; 54 public static final int STARRED = 3; 55 public static final int NUMBER = 4; 56 public static final int TYPE = 5; 57 public static final int LABEL = 6; 58 public static final int PHOTO_ID = 7; 59 public static final int PHOTO_URI = 8; 60 public static final int CARRIER_PRESENCE = 9; 61 62 private static final String[] PHONE_PROJECTION = { 63 Phone.LOOKUP_KEY, 64 Phone.CONTACT_ID, 65 Phone.DISPLAY_NAME, 66 Phone.STARRED, 67 Phone.NUMBER, 68 Phone.TYPE, 69 Phone.LABEL, 70 Phone.PHOTO_ID, 71 Phone.PHOTO_URI, 72 Phone.CARRIER_PRESENCE 73 }; 74 75 private static final String[] PHONE_PROJECTION_ALTERNATIVE = { 76 Phone.LOOKUP_KEY, 77 Phone.CONTACT_ID, 78 Phone.DISPLAY_NAME_ALTERNATIVE, 79 Phone.STARRED, 80 Phone.NUMBER, 81 Phone.TYPE, 82 Phone.LABEL, 83 Phone.PHOTO_ID, 84 Phone.PHOTO_URI, 85 Phone.CARRIER_PRESENCE 86 }; 87 getPhoneProjection(boolean primaryDisplayOrder)88 public static String[] getPhoneProjection(boolean primaryDisplayOrder) { 89 return primaryDisplayOrder ? PHONE_PROJECTION : PHONE_PROJECTION_ALTERNATIVE; 90 } 91 builder()92 public static Builder builder() { 93 return new AutoValue_SpeedDialUiItem.Builder() 94 .setChannels(ImmutableList.of()) 95 .setPinnedPosition(Optional.absent()); 96 } 97 98 /** 99 * Convert a cursor with projection {@link #getPhoneProjection(boolean)} into a {@link 100 * SpeedDialUiItem}. 101 * 102 * <p>This cursor is structured such that contacts are grouped by contact id and lookup key and 103 * each row that shares the same contact id and lookup key represents a phone number that belongs 104 * to a single contact. 105 * 106 * <p>If the cursor started at row X, this method will advance to row Y s.t. rows X, X + 1, ... Y 107 * - 1 all belong to the same contact (that is, share the same contact id and lookup key). 108 */ fromCursor( Resources resources, Cursor cursor, boolean isImsEnabled)109 public static SpeedDialUiItem fromCursor( 110 Resources resources, Cursor cursor, boolean isImsEnabled) { 111 Trace.beginSection("fromCursor"); 112 Assert.checkArgument(cursor != null); 113 Assert.checkArgument(cursor.getCount() != 0); 114 String lookupKey = cursor.getString(LOOKUP_KEY); 115 SpeedDialUiItem.Builder builder = 116 SpeedDialUiItem.builder() 117 .setLookupKey(lookupKey) 118 .setContactId(cursor.getLong(CONTACT_ID)) 119 // TODO(a bug): handle last name first preference 120 .setName(cursor.getString(DISPLAY_NAME)) 121 .setIsStarred(cursor.getInt(STARRED) == 1) 122 .setPhotoId(cursor.getLong(PHOTO_ID)) 123 .setPhotoUri( 124 TextUtils.isEmpty(cursor.getString(PHOTO_URI)) ? "" : cursor.getString(PHOTO_URI)); 125 126 // While there are more rows and the lookup keys are the same, add a channel for each of the 127 // contact's phone numbers. 128 List<Channel> channels = new ArrayList<>(); 129 Set<String> numbers = new ArraySet<>(); 130 do { 131 String number = cursor.getString(NUMBER); 132 // TODO(78492722): consider using lib phone number to compare numbers 133 if (!numbers.add(number)) { 134 // Number is identical to an existing number, skip this number 135 continue; 136 } 137 138 Channel channel = 139 Channel.builder() 140 .setNumber(number) 141 .setPhoneType(cursor.getInt(TYPE)) 142 .setLabel(getLabel(resources, cursor)) 143 .setTechnology(Channel.VOICE) 144 .build(); 145 channels.add(channel); 146 147 if (isImsEnabled 148 && (cursor.getInt(CARRIER_PRESENCE) & Data.CARRIER_PRESENCE_VT_CAPABLE) == 1) { 149 // Add another channel if the number is ViLTE reachable 150 channels.add(channel.toBuilder().setTechnology(Channel.IMS_VIDEO).build()); 151 } 152 // TODO(a bug): add another channel for Duo (needs to happen on main thread) 153 } while (cursor.moveToNext() && Objects.equals(lookupKey, cursor.getString(LOOKUP_KEY))); 154 155 builder.setChannels(ImmutableList.copyOf(channels)); 156 Trace.endSection(); 157 return builder.build(); 158 } 159 getLabel(Resources resources, Cursor cursor)160 private static String getLabel(Resources resources, Cursor cursor) { 161 int numberType = cursor.getInt(TYPE); 162 String numberLabel = cursor.getString(LABEL); 163 164 // Returns empty label instead of "custom" if the custom label is empty. 165 if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) { 166 return ""; 167 } 168 return (String) Phone.getTypeLabel(resources, numberType, numberLabel); 169 } 170 getPhotoInfo()171 public PhotoInfo getPhotoInfo() { 172 return PhotoInfo.newBuilder() 173 .setPhotoId(photoId()) 174 .setPhotoUri(photoUri()) 175 .setName(name()) 176 .setIsVideo(defaultChannel() != null && defaultChannel().isVideoTechnology()) 177 .setLookupUri(Contacts.getLookupUri(contactId(), lookupKey()).toString()) 178 .build(); 179 } 180 buildSpeedDialEntry()181 public SpeedDialEntry buildSpeedDialEntry() { 182 return SpeedDialEntry.builder() 183 .setId(speedDialEntryId()) 184 .setPinnedPosition(pinnedPosition()) 185 .setLookupKey(lookupKey()) 186 .setContactId(contactId()) 187 .setDefaultChannel(defaultChannel()) 188 .build(); 189 } 190 191 /** 192 * Returns one of the following: 193 * 194 * <ul> 195 * <li>The default channel if it's a video channel. 196 * <li>A video channel if it has the same attributes as the default channel, OR 197 * <li>null. (This is a deliberate product decision, even if there is only a single video 198 * reachable channel, we should still return null if it has different attributes from those 199 * in the default channel). 200 * </ul> 201 */ 202 @Nullable getDefaultVideoChannel()203 public Channel getDefaultVideoChannel() { 204 if (defaultChannel() == null) { 205 return null; 206 } 207 208 if (defaultChannel().isVideoTechnology()) { 209 return defaultChannel(); 210 } 211 212 if (channels().size() == 1) { 213 // If there is only a single channel, it can't be a video channel 214 return null; 215 } 216 217 // At this point, the default channel is a *voice* channel and there are more than 218 // one channel in total. 219 // 220 // Our defined assumptions about the channel list include that if a video channel 221 // follows a voice channel, it has the same attributes as that voice channel 222 // (see comments on method channels() for details). 223 // 224 // Therefore, if the default video channel exists, it must be the immediate successor 225 // of the default channel in the list. 226 // 227 // Note that we don't have to check if the last channel in the list is the default 228 // channel because even if it is, there will be no video channel under the assumption 229 // above. 230 for (int i = 0; i < channels().size() - 1; i++) { 231 // Find the default channel 232 if (Objects.equals(defaultChannel(), channels().get(i))) { 233 // Our defined assumptions about the list of channels is that if a video channel follows a 234 // voice channel, it has the same attributes as that voice channel. 235 Channel channel = channels().get(i + 1); 236 if (channel.isVideoTechnology()) { 237 return channel; 238 } 239 // Since the default voice channel isn't video reachable, we can't video call this number 240 return null; 241 } 242 } 243 throw Assert.createIllegalStateFailException("channels() doesn't contain defaultChannel()."); 244 } 245 246 /** 247 * Returns a voice channel if there is exactly one channel or the default channel is a voice 248 * channel. 249 */ 250 @Nullable getDefaultVoiceChannel()251 public Channel getDefaultVoiceChannel() { 252 if (channels().size() == 1) { 253 // If there is only a single channel, it must be a voice channel as per our defined 254 // assumptions (detailed in comments on method channels()). 255 return channels().get(0); 256 } 257 258 if (defaultChannel() == null) { 259 return null; 260 } 261 262 if (!defaultChannel().isVideoTechnology()) { 263 return defaultChannel(); 264 } 265 266 // Default channel is a video channel, so find it's corresponding voice channel by number since 267 // unreachable channels may not be in the list 268 for (Channel currentChannel : channels()) { 269 if (currentChannel.number().equals(defaultChannel().number()) 270 && currentChannel.technology() == Channel.VOICE) { 271 return currentChannel; 272 } 273 } 274 return null; 275 } 276 277 /** 278 * The id of the corresponding SpeedDialEntry. Null if the UI item does not have an entry, for 279 * example suggested contacts (isStarred() will also be false) 280 * 281 * @see SpeedDialEntry#id() 282 */ 283 @Nullable speedDialEntryId()284 public abstract Long speedDialEntryId(); 285 286 /** @see SpeedDialEntry#pinnedPosition() */ pinnedPosition()287 public abstract Optional<Integer> pinnedPosition(); 288 289 /** @see android.provider.ContactsContract.Contacts#DISPLAY_NAME */ name()290 public abstract String name(); 291 292 /** @see android.provider.ContactsContract.Contacts#_ID */ contactId()293 public abstract long contactId(); 294 295 /** @see android.provider.ContactsContract.Contacts#LOOKUP_KEY */ lookupKey()296 public abstract String lookupKey(); 297 298 /** @see android.provider.ContactsContract.Contacts#STARRED */ isStarred()299 public abstract boolean isStarred(); 300 301 /** @see Phone#PHOTO_ID */ photoId()302 public abstract long photoId(); 303 304 /** @see Phone#PHOTO_URI */ photoUri()305 public abstract String photoUri(); 306 307 /** 308 * Returns a list of channels available. A Duo channel is included iff it is reachable. Since a 309 * contact can have multiple phone numbers and each number can have multiple technologies, 310 * enumerate each one here so that the user can choose the correct one. Each channel here 311 * represents a row in the {@link com.android.dialer.speeddial.DisambigDialog}. 312 * 313 * <p>These channels have a few very strictly enforced assumption that are used heavily throughout 314 * the codebase. Those assumption are that: 315 * 316 * <ol> 317 * <li>Each of the contact's numbers are voice reachable. So if a channel has it's technology 318 * set to anything other than {@link Channel#VOICE}, there is gaurenteed to be another 319 * channel with the exact same attributes, but technology will be {@link Channel#VOICE}. 320 * <li>For each of the contact's phone numbers, there will be a voice channel, then the next 321 * channel will either be the same phone number but a video channel, or a new number. 322 * </ol> 323 * 324 * For example: Say a contact has two phone numbers (A & B) and A is duo reachable. Then you can 325 * assume the list of channels will be ordered as either {A_voice, A_duo, B_voice} or {B_voice, 326 * A_voice, A_duo}. 327 * 328 * @see com.android.dialer.speeddial.database.SpeedDialEntry.Channel 329 */ channels()330 public abstract ImmutableList<Channel> channels(); 331 332 /** 333 * Will be null when the user hasn't chosen a default yet. Note that a default channel may not be 334 * in the list returned by {@link #channels()}. This is because that list does not contain an 335 * unreachable Duo channel. When the default channel is a Duo channel and it becomes unreachable, 336 * it will remain as the default channel but disappear in the list returned by {@link 337 * #channels()}. 338 * 339 * @see com.android.dialer.speeddial.database.SpeedDialEntry#defaultChannel() 340 */ defaultChannel()341 public abstract @Nullable Channel defaultChannel(); 342 toBuilder()343 public abstract Builder toBuilder(); 344 345 /** Builder class for speed dial contact. */ 346 @AutoValue.Builder 347 public abstract static class Builder { 348 349 /** Set to null if {@link #isStarred()} is false. */ setSpeedDialEntryId(@ullable Long id)350 public abstract Builder setSpeedDialEntryId(@Nullable Long id); 351 setPinnedPosition(Optional<Integer> pinnedPosition)352 public abstract Builder setPinnedPosition(Optional<Integer> pinnedPosition); 353 setName(String name)354 public abstract Builder setName(String name); 355 setContactId(long contactId)356 public abstract Builder setContactId(long contactId); 357 setLookupKey(String lookupKey)358 public abstract Builder setLookupKey(String lookupKey); 359 setIsStarred(boolean isStarred)360 public abstract Builder setIsStarred(boolean isStarred); 361 setPhotoId(long photoId)362 public abstract Builder setPhotoId(long photoId); 363 setPhotoUri(String photoUri)364 public abstract Builder setPhotoUri(String photoUri); 365 setChannels(ImmutableList<Channel> channels)366 public abstract Builder setChannels(ImmutableList<Channel> channels); 367 368 /** Set to null if the user hasn't chosen a default or the channel no longer exists. */ setDefaultChannel(@ullable Channel defaultChannel)369 public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel); 370 build()371 public abstract SpeedDialUiItem build(); 372 } 373 } 374