• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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