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