strequentContacts = getStrequentContacts();
// For each contact, if it isn't starred, add it as a suggestion.
// If it is starred and not already accounted for above, then insert into the SpeedDialEntry DB.
for (SpeedDialUiItem contact : strequentContacts) {
if (!contact.isStarred()) {
// Add this contact as a suggestion
// TODO(77754534): improve suggestions beyond just first channel
speedDialUiItems.add(
contact.toBuilder().setDefaultChannel(contact.channels().get(0)).build());
} else if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) {
entriesToInsert.add(
SpeedDialEntry.builder()
.setLookupKey(contact.lookupKey())
.setContactId(contact.contactId())
.setDefaultChannel(contact.defaultChannel())
.build());
// These are our newly starred contacts
speedDialUiItems.add(contact);
}
}
db.insertUpdateAndDelete(
ImmutableList.copyOf(entriesToInsert),
ImmutableList.copyOf(entriesToUpdate),
ImmutableList.copyOf(entriesToDelete));
return ImmutableList.copyOf(speedDialUiItems);
}
/**
* Returns the same list of SpeedDialEntries that are passed in except their contact ids and
* lookup keys are updated to current values.
*
* Unfortunately, we need to look up each contact individually to update the contact id and
* lookup key. Luckily though, this query is highly optimized on the framework side and very
* quick.
*/
@WorkerThread
private List updateContactIdsAndLookupKeys(List entries) {
Assert.isWorkerThread();
List updatedEntries = new ArrayList<>();
for (SpeedDialEntry entry : entries) {
try (Cursor cursor =
appContext
.getContentResolver()
.query(
Contacts.getLookupUri(entry.contactId(), entry.lookupKey()),
new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
null,
null,
null)) {
if (cursor == null) {
LogUtil.e("SpeedDialUiItemLoader.updateContactIdsAndLookupKeys", "null cursor");
return new ArrayList<>();
}
if (cursor.getCount() == 0) {
// No need to update this entry, the contact was deleted. We'll clear it up later.
updatedEntries.add(entry);
continue;
}
// Since all cursor rows will be have the same contact id and lookup key, just grab the
// first one.
cursor.moveToFirst();
updatedEntries.add(
entry
.toBuilder()
.setContactId(cursor.getLong(0))
.setLookupKey(cursor.getString(1))
.build());
}
}
return updatedEntries;
}
/**
* Returns a map of SpeedDialEntries to their corresponding SpeedDialUiItems. Mappings to null
* elements imply that the contact was deleted.
*/
@WorkerThread
private Map getSpeedDialUiItemsFromEntries(
List entries) {
Assert.isWorkerThread();
// Fetch the contact ids from the SpeedDialEntries
Set contactIds = new HashSet<>();
entries.forEach(entry -> contactIds.add(Long.toString(entry.contactId())));
if (contactIds.isEmpty()) {
return new ArrayMap<>();
}
// Build SpeedDialUiItems from those contact ids and map them to their entries
Selection selection =
Selection.builder().and(Selection.column(Phone.CONTACT_ID).in(contactIds)).build();
try (Cursor cursor =
appContext
.getContentResolver()
.query(
Phone.CONTENT_URI,
SpeedDialUiItem.PHONE_PROJECTION,
selection.getSelection(),
selection.getSelectionArgs(),
null)) {
Map map = new ArrayMap<>();
for (cursor.moveToFirst(); !cursor.isAfterLast(); /* Iterate in the loop */ ) {
SpeedDialUiItem item = SpeedDialUiItem.fromCursor(cursor);
for (SpeedDialEntry entry : entries) {
if (entry.contactId() == item.contactId()) {
// It's impossible for two contacts to exist with the same contact id, so if this entry
// was previously matched to a SpeedDialUiItem and is being matched again, something
// went horribly wrong.
Assert.checkArgument(
map.put(entry, item) == null,
"Each SpeedDialEntry only has one correct SpeedDialUiItem");
}
}
}
// Contact must have been deleted
for (SpeedDialEntry entry : entries) {
map.putIfAbsent(entry, null);
}
return map;
}
}
@WorkerThread
private List getStrequentContacts() {
Assert.isWorkerThread();
Set contactIds = new ArraySet<>();
// Fetch the contact ids of all strequent contacts
Uri strequentUri =
Contacts.CONTENT_STREQUENT_URI
.buildUpon()
.appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
.build();
try (Cursor cursor =
appContext
.getContentResolver()
.query(strequentUri, new String[] {Phone.CONTACT_ID}, null, null, null)) {
if (cursor == null) {
LogUtil.e("SpeedDialUiItemLoader.getStrequentContacts", "null cursor");
return new ArrayList<>();
}
if (cursor.getCount() == 0) {
return new ArrayList<>();
}
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
contactIds.add(Long.toString(cursor.getLong(0)));
}
}
// Build SpeedDialUiItems from those contact ids
Selection selection =
Selection.builder().and(Selection.column(Phone.CONTACT_ID).in(contactIds)).build();
try (Cursor cursor =
appContext
.getContentResolver()
.query(
Phone.CONTENT_URI,
SpeedDialUiItem.PHONE_PROJECTION,
selection.getSelection(),
selection.getSelectionArgs(),
null)) {
List contacts = new ArrayList<>();
if (cursor == null) {
LogUtil.e("SpeedDialUiItemLoader.getStrequentContacts", "null cursor");
return new ArrayList<>();
}
if (cursor.getCount() == 0) {
return contacts;
}
for (cursor.moveToFirst(); !cursor.isAfterLast(); /* Iterate in the loop */ ) {
contacts.add(SpeedDialUiItem.fromCursor(cursor));
}
return contacts;
}
}
/**
* Returns a new list with duo reachable channels inserted. Duo channels won't replace ViLTE
* channels.
*/
@MainThread
public ImmutableList insertDuoChannels(
Context context, ImmutableList speedDialUiItems) {
Assert.isMainThread();
Duo duo = DuoComponent.get(context).getDuo();
int maxDuoSuggestions = MAX_DUO_SUGGESTIONS;
ImmutableList.Builder newSpeedDialItemList = ImmutableList.builder();
// for each existing item
for (SpeedDialUiItem item : speedDialUiItems) {
// If the item is a suggestion
if (!item.isStarred()) {
// And duo reachable, insert a duo suggestion
if (maxDuoSuggestions > 0 && duo.isReachable(context, item.defaultChannel().number())) {
maxDuoSuggestions--;
Channel defaultChannel =
item.defaultChannel().toBuilder().setTechnology(Channel.DUO).build();
newSpeedDialItemList.add(item.toBuilder().setDefaultChannel(defaultChannel).build());
}
// Insert the voice suggestion too
newSpeedDialItemList.add(item);
} else if (item.defaultChannel() == null) {
// If the contact is starred and doesn't have a default channel, insert duo channels
newSpeedDialItemList.add(insertDuoChannelsToStarredContact(context, item));
} // if starred and has a default channel, leave it as is, the user knows what they want.
}
return newSpeedDialItemList.build();
}
@MainThread
private SpeedDialUiItem insertDuoChannelsToStarredContact(Context context, SpeedDialUiItem item) {
Assert.isMainThread();
Assert.checkArgument(item.isStarred());
// build a new list of channels
ImmutableList.Builder newChannelsList = ImmutableList.builder();
Channel previousChannel = item.channels().get(0);
newChannelsList.add(previousChannel);
for (int i = 1; i < item.channels().size(); i++) {
Channel currentChannel = item.channels().get(i);
// If the previous and current channel are voice channels, that means the previous number
// didn't have a video channel.
// If the previous number is duo reachable, insert a duo channel.
if (!previousChannel.isVideoTechnology()
&& !currentChannel.isVideoTechnology()
&& DuoComponent.get(context).getDuo().isReachable(context, previousChannel.number())) {
newChannelsList.add(previousChannel.toBuilder().setTechnology(Channel.DUO).build());
}
newChannelsList.add(currentChannel);
previousChannel = currentChannel;
}
// Check the last channel
if (!previousChannel.isVideoTechnology()
&& DuoComponent.get(context).getDuo().isReachable(context, previousChannel.number())) {
newChannelsList.add(previousChannel.toBuilder().setTechnology(Channel.DUO).build());
}
return item.toBuilder().setChannels(newChannelsList.build()).build();
}
private SpeedDialEntryDao getSpeedDialEntryDao() {
return new SpeedDialEntryDatabaseHelper(appContext);
}
}