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
16package com.android.bluetooth.map;
17
18import java.util.Arrays;
19import java.util.HashMap;
20import java.util.regex.Pattern;
21
22import android.annotation.TargetApi;
23import android.content.ContentResolver;
24import android.database.Cursor;
25import android.net.Uri;
26import android.provider.ContactsContract;
27import android.provider.ContactsContract.Contacts;
28import android.provider.ContactsContract.PhoneLookup;
29import android.provider.Telephony.CanonicalAddressesColumns;
30import android.provider.Telephony.MmsSms;
31import android.util.Log;
32
33/**
34 * Use these functions when extracting data for listings. It caches frequently used data to
35 * speed up building large listings - e.g. before applying filtering.
36 */
37@TargetApi(19)
38public class SmsMmsContacts {
39
40    private static final String TAG = "SmsMmsContacts";
41
42    private HashMap<Long,String> mPhoneNumbers = null;
43    private final HashMap<String,MapContact> mNames = new HashMap<String, MapContact>(10);
44
45    private static final Uri ADDRESS_URI =
46            MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-addresses").build();
47
48    private static final String[] ADDRESS_PROJECTION = { CanonicalAddressesColumns._ID,
49                    CanonicalAddressesColumns.ADDRESS };
50    private static final int COL_ADDR_ID =
51            Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns._ID);
52    private static final int COL_ADDR_ADDR =
53            Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns.ADDRESS);
54
55    private static final String[] CONTACT_PROJECTION = {Contacts._ID, Contacts.DISPLAY_NAME};
56    private static final String CONTACT_SEL_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
57    private static final int COL_CONTACT_ID =
58            Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts._ID);
59    private static final int COL_CONTACT_NAME =
60            Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts.DISPLAY_NAME);
61
62    /**
63     * Get a contacts phone number based on the canonical addresses id of the contact.
64     * (The ID listed in the Threads table.)
65     * @param resolver the ContantResolver to be used.
66     * @param id the id of the contact, as listed in the Threads table
67     * @return the phone number of the contact - or null if id does not exist.
68     */
69    public String getPhoneNumber(ContentResolver resolver, long id) {
70        String number;
71        if(mPhoneNumbers != null && (number = mPhoneNumbers.get(id)) != null) {
72            return number;
73        }
74        fillPhoneCache(resolver);
75        return mPhoneNumbers.get(id);
76    }
77
78    public static String getPhoneNumberUncached(ContentResolver resolver, long id) {
79        String where = CanonicalAddressesColumns._ID + " = " + id;
80        Cursor c = resolver.query(ADDRESS_URI, ADDRESS_PROJECTION, where, null, null);
81        try {
82            if (c != null) {
83                if(c.moveToPosition(0)) {
84                    return c.getString(COL_ADDR_ADDR);
85                }
86            }
87            Log.e(TAG, "query failed");
88        } finally {
89            if(c != null) c.close();
90        }
91        return null;
92    }
93
94    /**
95     * Clears the local cache. Call after a listing is complete, to avoid using invalid data.
96     */
97    public void clearCache() {
98        if(mPhoneNumbers != null) mPhoneNumbers.clear();
99        if(mNames != null) mNames.clear();
100    }
101
102    /**
103     * Refreshes the cache, by clearing all cached values and fill the cache with the result of
104     * a new query.
105     * @param resolver the ContantResolver to be used.
106     */
107    private void fillPhoneCache(ContentResolver resolver){
108        Cursor c = resolver.query(ADDRESS_URI, ADDRESS_PROJECTION, null, null, null);
109        if(mPhoneNumbers == null) {
110            int size = 0;
111            if(c != null)
112            {
113                size = c.getCount();
114            }
115            mPhoneNumbers = new HashMap<Long, String>(size);
116        } else {
117            mPhoneNumbers.clear();
118        }
119        try {
120            if (c != null) {
121                long id;
122                String addr;
123                c.moveToPosition(-1);
124                while (c.moveToNext()) {
125                    id = c.getLong(COL_ADDR_ID);
126                    addr = c.getString(COL_ADDR_ADDR);
127                    mPhoneNumbers.put(id, addr);
128                }
129            } else {
130                Log.e(TAG, "query failed");
131            }
132        } finally {
133            if(c != null) c.close();
134        }
135    }
136
137    public MapContact getContactNameFromPhone(String phone, ContentResolver resolver) {
138        return getContactNameFromPhone(phone, resolver, null);
139    }
140    /**
141     * Lookup a contacts name in the Android Contacts database.
142     * @param phone the phone number of the contact
143     * @param resolver the ContentResolver to use.
144     * @return the name of the contact or null, if no contact was found.
145     */
146    public MapContact getContactNameFromPhone(String phone, ContentResolver resolver,
147            String contactNameFilter) {
148        MapContact contact = mNames.get(phone);
149
150        if(contact != null){
151            if(contact.getId() < 0) {
152                return null;
153            }
154            if(contactNameFilter == null) {
155                return contact;
156            }
157            // Validate filter
158            String searchString = contactNameFilter.replace("*", ".*");
159            searchString = ".*" + searchString + ".*";
160            Pattern p = Pattern.compile(Pattern.quote(searchString), Pattern.CASE_INSENSITIVE);
161            if(p.matcher(contact.getName()).find()) {
162                return contact;
163            }
164            return null;
165        }
166
167        // TODO: Should we change to extract both formatted name, and display name?
168
169        Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,Uri.encode(phone));
170        String selection = CONTACT_SEL_VISIBLE;
171        String[] selectionArgs = null;
172        if(contactNameFilter != null) {
173            selection += "AND " + ContactsContract.Contacts.DISPLAY_NAME + " like ?";
174            selectionArgs = new String[]{"%" + contactNameFilter.replace("*", "%") + "%"};
175        }
176
177        Cursor c = resolver.query(uri, CONTACT_PROJECTION, selection, selectionArgs, null);
178        try {
179            if (c != null && c.getCount() >= 1) {
180                c.moveToFirst();
181                long id = c.getLong(COL_CONTACT_ID);
182                String name = c.getString(COL_CONTACT_NAME);
183                contact = MapContact.create(id, name);
184                mNames.put(phone, contact);
185            } else {
186                contact = MapContact.create(-1, null);
187                mNames.put(phone, contact);
188                contact = null;
189            }
190        } finally {
191            if (c != null) c.close();
192        }
193        return contact;
194    }
195}
196