1 /** 2 * Copyright (C) 2021 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.car.voicecontrol; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.util.Log; 22 import android.util.Pair; 23 24 import androidx.lifecycle.Observer; 25 26 import com.android.car.telephony.common.Contact; 27 import com.android.car.telephony.common.InMemoryPhoneBook; 28 29 import java.util.HashMap; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.Set; 34 import java.util.function.Consumer; 35 36 /** 37 * A provider of contact information. Because reading and indexing contacts can take some time, 38 * it is best to index them in the background ahead of time. On a production version, this work 39 * would be done by a 40 */ 41 public class ContactsProvider { 42 private static final String TAG = "Mica.Contacts"; 43 44 private Observer<List<Contact>> mContactsObserver = this::indexContacts; 45 private Map<String, Set<Contact>> mContactsByPhoneticKey = new HashMap<>(); 46 ContactsProvider(Context context)47 public ContactsProvider(Context context) { 48 Log.d(TAG, "Current user: " + context.getUser()); 49 InMemoryPhoneBook.init(context); 50 InMemoryPhoneBook.get().getContactsLiveData().observeForever(mContactsObserver); 51 } 52 53 /** 54 * Releases resources used by this provider 55 */ destroy()56 public void destroy() { 57 InMemoryPhoneBook.get().getContactsLiveData().removeObserver(mContactsObserver); 58 InMemoryPhoneBook.tearDown(); 59 } 60 61 /** 62 * A production voice control application would add contacts to their models for recognition and 63 * disambiguation. 64 * The code below uses Soundex (https://en.wikipedia.org/wiki/Soundex) algorithm to index 65 * contacts by the sound of their names. This is for demonstration purposes only 66 */ indexContacts(List<Contact> contacts)67 private void indexContacts(List<Contact> contacts) { 68 Log.d(TAG, "Indexing contact: " + (contacts != null ? contacts.size() : null)); 69 if (contacts == null) { 70 mContactsByPhoneticKey = new HashMap<>(); 71 return; 72 } 73 Map<String, Set<Contact>> contactsForPhoneticKey = new HashMap<>(); 74 Consumer<Pair<String, Contact>> indexer = p -> { 75 if (p.first == null) { 76 return; 77 } 78 String phoneticKey = StringUtils.soundex(p.first.toLowerCase()); 79 Set<Contact> contactSet = contactsForPhoneticKey.computeIfAbsent(phoneticKey, 80 k -> new HashSet<>()); 81 contactSet.add(p.second); 82 Log.d(TAG, String.format("Indexing contact: '%s' - word: '%s' - phonetic key: '%s'", 83 p.second.getLookupKey(), p.first.toLowerCase(), phoneticKey)); 84 }; 85 for (Contact contact : contacts) { 86 indexer.accept(Pair.create(contact.getDisplayName(), contact)); 87 indexer.accept(Pair.create(contact.getDisplayNameAlt(), contact)); 88 indexer.accept(Pair.create(contact.getFamilyName(), contact)); 89 indexer.accept(Pair.create(contact.getGivenName(), contact)); 90 } 91 92 mContactsByPhoneticKey = contactsForPhoneticKey; 93 } 94 95 /** 96 * A production voice control application would apply phonetic matching to best recognize the 97 * requested name. Additionally, a disambiguation flow, asking the user to select between 98 * multiple potential candidates could improve accuracy. 99 * This implementation just find the contact with the minimum edit distance to the recognized 100 * string. 101 * 102 * @param deviceAddress The bluetooth device address. If provided, only return contacts from 103 * the specified device, otherwise return all contacts. 104 */ 105 @Nullable getContact(String query, @Nullable String deviceAddress)106 public Contact getContact(String query, @Nullable String deviceAddress) { 107 String normalizedInput = query.toLowerCase().trim(); 108 String phoneticKey = StringUtils.soundex(normalizedInput); 109 110 Set<Contact> contactsForPhoneticKey; 111 contactsForPhoneticKey = mContactsByPhoneticKey.get(phoneticKey); 112 if (deviceAddress != null) { 113 contactsForPhoneticKey.removeIf(contact -> 114 !contact.getAccountName().equals(deviceAddress)); 115 } 116 117 Log.d(TAG, "Device: " + deviceAddress 118 + " contact query: " + query 119 + " phonetic key: " + phoneticKey 120 + " contacts: " + (contactsForPhoneticKey != null 121 ? contactsForPhoneticKey.size() : "-")); 122 if (contactsForPhoneticKey == null) { 123 return null; 124 } 125 126 Contact bestMatch = null; 127 int distance = Integer.MAX_VALUE; 128 Log.d(TAG, "Found " + contactsForPhoneticKey.size() + " phonetic matches"); 129 for (Contact c : contactsForPhoneticKey) { 130 int newDistance = StringUtils.getMinDistance(normalizedInput, c.getDisplayName(), 131 c.getDisplayNameAlt(), c.getFamilyName(), c.getGivenName()); 132 if (newDistance < distance) { 133 distance = newDistance; 134 bestMatch = c; 135 } 136 } 137 138 Log.d(TAG, "Best match: " + bestMatch.getDisplayName()); 139 return bestMatch; 140 } 141 } 142