1 /*
2  * Copyright (C) 2015 Samsung System LSI
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package com.android.bluetooth.map;
17 
18 import android.bluetooth.BluetoothProfile;
19 import android.bluetooth.BluetoothProtoEnums;
20 import android.content.ContentResolver;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.provider.ContactsContract;
24 import android.provider.ContactsContract.Contacts;
25 import android.provider.ContactsContract.PhoneLookup;
26 import android.provider.Telephony.CanonicalAddressesColumns;
27 import android.provider.Telephony.MmsSms;
28 import android.util.Log;
29 
30 import com.android.bluetooth.BluetoothMethodProxy;
31 import com.android.bluetooth.BluetoothStatsLog;
32 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.util.Arrays;
36 import java.util.HashMap;
37 import java.util.regex.Pattern;
38 
39 /**
40  * Use these functions when extracting data for listings. It caches frequently used data to speed up
41  * building large listings - e.g. before applying filtering.
42  */
43 // Next tag value for ContentProfileErrorReportUtils.report(): 2
44 public class SmsMmsContacts {
45 
46     private static final String TAG = "SmsMmsContacts";
47 
48     private HashMap<Long, String> mPhoneNumbers = null;
49 
50     @VisibleForTesting
51     final HashMap<String, MapContact> mNames = new HashMap<String, MapContact>(10);
52 
53     private static final Uri ADDRESS_URI =
54             MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-addresses").build();
55 
56     @VisibleForTesting
57     static final String[] ADDRESS_PROJECTION = {
58         CanonicalAddressesColumns._ID, CanonicalAddressesColumns.ADDRESS
59     };
60 
61     private static final int COL_ADDR_ID =
62             Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns._ID);
63     private static final int COL_ADDR_ADDR =
64             Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns.ADDRESS);
65 
66     @VisibleForTesting
67     static final String[] CONTACT_PROJECTION = {Contacts._ID, Contacts.DISPLAY_NAME};
68 
69     private static final String CONTACT_SEL_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
70     private static final int COL_CONTACT_ID =
71             Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts._ID);
72     private static final int COL_CONTACT_NAME =
73             Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts.DISPLAY_NAME);
74 
75     /**
76      * Get a contacts phone number based on the canonical addresses id of the contact. (The ID
77      * listed in the Threads table.)
78      *
79      * @param resolver the ContantResolver to be used.
80      * @param id the id of the contact, as listed in the Threads table
81      * @return the phone number of the contact - or null if id does not exist.
82      */
getPhoneNumber(ContentResolver resolver, long id)83     public String getPhoneNumber(ContentResolver resolver, long id) {
84         String number;
85         if (mPhoneNumbers != null && (number = mPhoneNumbers.get(id)) != null) {
86             return number;
87         }
88         fillPhoneCache(resolver);
89         return mPhoneNumbers.get(id);
90     }
91 
getPhoneNumberUncached(ContentResolver resolver, long id)92     public static String getPhoneNumberUncached(ContentResolver resolver, long id) {
93         String where = CanonicalAddressesColumns._ID + " = " + id;
94         Cursor c =
95                 BluetoothMethodProxy.getInstance()
96                         .contentResolverQuery(
97                                 resolver, ADDRESS_URI, ADDRESS_PROJECTION, where, null, null);
98         try {
99             if (c != null) {
100                 if (c.moveToPosition(0)) {
101                     return c.getString(COL_ADDR_ADDR);
102                 }
103             }
104             Log.e(TAG, "query failed");
105             ContentProfileErrorReportUtils.report(
106                     BluetoothProfile.MAP,
107                     BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS,
108                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
109                     0);
110         } finally {
111             if (c != null) {
112                 c.close();
113             }
114         }
115         return null;
116     }
117 
118     /** Clears the local cache. Call after a listing is complete, to avoid using invalid data. */
clearCache()119     public void clearCache() {
120         if (mPhoneNumbers != null) {
121             mPhoneNumbers.clear();
122         }
123         if (mNames != null) {
124             mNames.clear();
125         }
126     }
127 
128     /**
129      * Refreshes the cache, by clearing all cached values and fill the cache with the result of a
130      * new query.
131      *
132      * @param resolver the ContantResolver to be used.
133      */
134     @VisibleForTesting
fillPhoneCache(ContentResolver resolver)135     void fillPhoneCache(ContentResolver resolver) {
136         Cursor c =
137                 BluetoothMethodProxy.getInstance()
138                         .contentResolverQuery(
139                                 resolver, ADDRESS_URI, ADDRESS_PROJECTION, null, null, null);
140         if (mPhoneNumbers == null) {
141             int size = 0;
142             if (c != null) {
143                 size = c.getCount();
144             }
145             mPhoneNumbers = new HashMap<Long, String>(size);
146         } else {
147             mPhoneNumbers.clear();
148         }
149         try {
150             if (c != null) {
151                 long id;
152                 String addr;
153                 c.moveToPosition(-1);
154                 while (c.moveToNext()) {
155                     id = c.getLong(COL_ADDR_ID);
156                     addr = c.getString(COL_ADDR_ADDR);
157                     mPhoneNumbers.put(id, addr);
158                 }
159             } else {
160                 Log.e(TAG, "query failed");
161                 ContentProfileErrorReportUtils.report(
162                         BluetoothProfile.MAP,
163                         BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS,
164                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
165                         1);
166             }
167         } finally {
168             if (c != null) {
169                 c.close();
170             }
171         }
172     }
173 
getContactNameFromPhone(String phone, ContentResolver resolver)174     public MapContact getContactNameFromPhone(String phone, ContentResolver resolver) {
175         return getContactNameFromPhone(phone, resolver, null);
176     }
177 
178     /**
179      * Lookup a contacts name in the Android Contacts database.
180      *
181      * @param phone the phone number of the contact
182      * @param resolver the ContentResolver to use.
183      * @return the name of the contact or null, if no contact was found.
184      */
getContactNameFromPhone( String phone, ContentResolver resolver, String contactNameFilter)185     public MapContact getContactNameFromPhone(
186             String phone, ContentResolver resolver, String contactNameFilter) {
187         MapContact contact = mNames.get(phone);
188 
189         if (contact != null) {
190             if (contact.getId() < 0) {
191                 return null;
192             }
193             if (contactNameFilter == null) {
194                 return contact;
195             }
196             // Validate filter
197             String searchString = contactNameFilter.replace("*", ".*");
198             searchString = ".*" + searchString + ".*";
199             Pattern p = Pattern.compile(Pattern.quote(searchString), Pattern.CASE_INSENSITIVE);
200             if (p.matcher(contact.getName()).find()) {
201                 return contact;
202             }
203             return null;
204         }
205 
206         // TODO: Should we change to extract both formatted name, and display name?
207 
208         Uri uri =
209                 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(phone));
210         String selection = CONTACT_SEL_VISIBLE;
211         String[] selectionArgs = null;
212         if (contactNameFilter != null) {
213             selection += "AND " + ContactsContract.Contacts.DISPLAY_NAME + " like ?";
214             selectionArgs = new String[] {"%" + contactNameFilter.replace("*", "%") + "%"};
215         }
216 
217         Cursor c =
218                 BluetoothMethodProxy.getInstance()
219                         .contentResolverQuery(
220                                 resolver, uri, CONTACT_PROJECTION, selection, selectionArgs, null);
221         try {
222             if (c != null && c.getCount() >= 1) {
223                 c.moveToFirst();
224                 long id = c.getLong(COL_CONTACT_ID);
225                 String name = c.getString(COL_CONTACT_NAME);
226                 contact = MapContact.create(id, name);
227                 mNames.put(phone, contact);
228             } else {
229                 contact = MapContact.create(-1, null);
230                 mNames.put(phone, contact);
231                 contact = null;
232             }
233         } finally {
234             if (c != null) {
235                 c.close();
236             }
237         }
238         return contact;
239     }
240 }
241