• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     private static final String TAG = SmsMmsContacts.class.getSimpleName();
46 
47     private HashMap<Long, String> mPhoneNumbers = null;
48 
49     @VisibleForTesting
50     final HashMap<String, MapContact> mNames = new HashMap<String, MapContact>(10);
51 
52     private static final Uri ADDRESS_URI =
53             MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-addresses").build();
54 
55     @VisibleForTesting
56     static final String[] ADDRESS_PROJECTION = {
57         CanonicalAddressesColumns._ID, CanonicalAddressesColumns.ADDRESS
58     };
59 
60     private static final int COL_ADDR_ID =
61             Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns._ID);
62     private static final int COL_ADDR_ADDR =
63             Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns.ADDRESS);
64 
65     @VisibleForTesting
66     static final String[] CONTACT_PROJECTION = {Contacts._ID, Contacts.DISPLAY_NAME};
67 
68     private static final String CONTACT_SEL_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
69     private static final int COL_CONTACT_ID =
70             Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts._ID);
71     private static final int COL_CONTACT_NAME =
72             Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts.DISPLAY_NAME);
73 
74     /**
75      * Get a contacts phone number based on the canonical addresses id of the contact. (The ID
76      * listed in the Threads table.)
77      *
78      * @param resolver the ContentResolver to be used.
79      * @param id the id of the contact, as listed in the Threads table
80      * @return the phone number of the contact - or null if id does not exist.
81      */
getPhoneNumber(ContentResolver resolver, long id)82     public String getPhoneNumber(ContentResolver resolver, long id) {
83         String number;
84         if (mPhoneNumbers != null && (number = mPhoneNumbers.get(id)) != null) {
85             return number;
86         }
87         fillPhoneCache(resolver);
88         return mPhoneNumbers.get(id);
89     }
90 
getPhoneNumberUncached(ContentResolver resolver, long id)91     public static String getPhoneNumberUncached(ContentResolver resolver, long id) {
92         String where = CanonicalAddressesColumns._ID + " = " + id;
93         Cursor c =
94                 BluetoothMethodProxy.getInstance()
95                         .contentResolverQuery(
96                                 resolver, ADDRESS_URI, ADDRESS_PROJECTION, where, null, null);
97         try {
98             if (c != null) {
99                 if (c.moveToPosition(0)) {
100                     return c.getString(COL_ADDR_ADDR);
101                 }
102             }
103             Log.e(TAG, "query failed");
104             ContentProfileErrorReportUtils.report(
105                     BluetoothProfile.MAP,
106                     BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS,
107                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
108                     0);
109         } finally {
110             if (c != null) {
111                 c.close();
112             }
113         }
114         return null;
115     }
116 
117     /** Clears the local cache. Call after a listing is complete, to avoid using invalid data. */
clearCache()118     public void clearCache() {
119         if (mPhoneNumbers != null) {
120             mPhoneNumbers.clear();
121         }
122         if (mNames != null) {
123             mNames.clear();
124         }
125     }
126 
127     /**
128      * Refreshes the cache, by clearing all cached values and fill the cache with the result of a
129      * new query.
130      *
131      * @param resolver the ContentResolver to be used.
132      */
133     @VisibleForTesting
fillPhoneCache(ContentResolver resolver)134     void fillPhoneCache(ContentResolver resolver) {
135         Cursor c =
136                 BluetoothMethodProxy.getInstance()
137                         .contentResolverQuery(
138                                 resolver, ADDRESS_URI, ADDRESS_PROJECTION, null, null, null);
139         if (mPhoneNumbers == null) {
140             int size = 0;
141             if (c != null) {
142                 size = c.getCount();
143             }
144             mPhoneNumbers = new HashMap<Long, String>(size);
145         } else {
146             mPhoneNumbers.clear();
147         }
148         try {
149             if (c != null) {
150                 long id;
151                 String addr;
152                 c.moveToPosition(-1);
153                 while (c.moveToNext()) {
154                     id = c.getLong(COL_ADDR_ID);
155                     addr = c.getString(COL_ADDR_ADDR);
156                     mPhoneNumbers.put(id, addr);
157                 }
158             } else {
159                 Log.e(TAG, "query failed");
160                 ContentProfileErrorReportUtils.report(
161                         BluetoothProfile.MAP,
162                         BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS,
163                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
164                         1);
165             }
166         } finally {
167             if (c != null) {
168                 c.close();
169             }
170         }
171     }
172 
getContactNameFromPhone(String phone, ContentResolver resolver)173     public MapContact getContactNameFromPhone(String phone, ContentResolver resolver) {
174         return getContactNameFromPhone(phone, resolver, null);
175     }
176 
177     /**
178      * Lookup a contacts name in the Android Contacts database.
179      *
180      * @param phone the phone number of the contact
181      * @param resolver the ContentResolver to use.
182      * @return the name of the contact or null, if no contact was found.
183      */
getContactNameFromPhone( String phone, ContentResolver resolver, String contactNameFilter)184     public MapContact getContactNameFromPhone(
185             String phone, ContentResolver resolver, String contactNameFilter) {
186         MapContact contact = mNames.get(phone);
187 
188         if (contact != null) {
189             if (contact.id() < 0) {
190                 return null;
191             }
192             if (contactNameFilter == null) {
193                 return contact;
194             }
195             // Validate filter
196             String searchString = contactNameFilter.replace("*", ".*");
197             searchString = ".*" + searchString + ".*";
198             Pattern p = Pattern.compile(Pattern.quote(searchString), Pattern.CASE_INSENSITIVE);
199             if (p.matcher(contact.name()).find()) {
200                 return contact;
201             }
202             return null;
203         }
204 
205         // TODO: Should we change to extract both formatted name, and display name?
206 
207         Uri uri =
208                 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(phone));
209         String selection = CONTACT_SEL_VISIBLE;
210         String[] selectionArgs = null;
211         if (contactNameFilter != null) {
212             selection = selection + "AND " + ContactsContract.Contacts.DISPLAY_NAME + " like ?";
213             selectionArgs = new String[] {"%" + contactNameFilter.replace("*", "%") + "%"};
214         }
215 
216         Cursor c =
217                 BluetoothMethodProxy.getInstance()
218                         .contentResolverQuery(
219                                 resolver, uri, CONTACT_PROJECTION, selection, selectionArgs, null);
220         try {
221             if (c != null && c.getCount() >= 1) {
222                 c.moveToFirst();
223                 long id = c.getLong(COL_CONTACT_ID);
224                 String name = c.getString(COL_CONTACT_NAME);
225                 contact = new MapContact(id, name);
226                 mNames.put(phone, contact);
227             } else {
228                 contact = new MapContact(-1, null);
229                 mNames.put(phone, contact);
230                 contact = null;
231             }
232         } finally {
233             if (c != null) {
234                 c.close();
235             }
236         }
237         return contact;
238     }
239 }
240