AtPhonebook.java revision 6c91bc0a163cc7600c40d7fb979777fd911d1ef1
1/*
2 * Copyright (C) 2008 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
17package com.android.bluetooth.hfp;
18
19import com.android.internal.telephony.GsmAlphabet;
20
21import android.bluetooth.BluetoothDevice;
22import android.content.Context;
23import android.content.Intent;
24import android.database.Cursor;
25import android.net.Uri;
26import android.provider.CallLog.Calls;
27import android.provider.ContactsContract.CommonDataKinds.Phone;
28import android.provider.ContactsContract.PhoneLookup;
29import android.telephony.PhoneNumberUtils;
30import android.util.Log;
31
32import java.util.HashMap;
33
34/**
35 * Helper for managing phonebook presentation over AT commands
36 * @hide
37 */
38public class AtPhonebook {
39    private static final String TAG = "BluetoothAtPhonebook";
40    private static final boolean DBG = false;
41
42    /** The projection to use when querying the call log database in response
43     *  to AT+CPBR for the MC, RC, and DC phone books (missed, received, and
44     *   dialed calls respectively)
45     */
46    private static final String[] CALLS_PROJECTION = new String[] {
47        Calls._ID, Calls.NUMBER
48    };
49
50    /** The projection to use when querying the contacts database in response
51     *   to AT+CPBR for the ME phonebook (saved phone numbers).
52     */
53    private static final String[] PHONES_PROJECTION = new String[] {
54        Phone._ID, Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE
55    };
56
57    /** Android supports as many phonebook entries as the flash can hold, but
58     *  BT periphals don't. Limit the number we'll report. */
59    private static final int MAX_PHONEBOOK_SIZE = 16384;
60
61    private static final String OUTGOING_CALL_WHERE = Calls.TYPE + "=" + Calls.OUTGOING_TYPE;
62    private static final String INCOMING_CALL_WHERE = Calls.TYPE + "=" + Calls.INCOMING_TYPE;
63    private static final String MISSED_CALL_WHERE = Calls.TYPE + "=" + Calls.MISSED_TYPE;
64    private static final String VISIBLE_PHONEBOOK_WHERE = Phone.IN_VISIBLE_GROUP + "=1";
65
66    private class PhonebookResult {
67        public Cursor  cursor; // result set of last query
68        public int     numberColumn;
69        public int     typeColumn;
70        public int     nameColumn;
71    };
72
73    private final Context mContext;
74
75    private String mCurrentPhonebook;
76    private String mCharacterSet = "UTF-8";
77
78    private int mCpbrIndex1, mCpbrIndex2;
79    private boolean mCheckingAccessPermission;
80
81    // package and class name to which we send intent to check phone book access permission
82    private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings";
83    private static final String ACCESS_AUTHORITY_CLASS =
84        "com.android.settings.bluetooth.BluetoothPermissionRequest";
85    private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
86
87    private final HashMap<String, PhonebookResult> mPhonebooks =
88            new HashMap<String, PhonebookResult>(4);
89
90    public AtPhonebook(Context context) {
91        mContext = context;
92        mPhonebooks.put("DC", new PhonebookResult());  // dialled calls
93        mPhonebooks.put("RC", new PhonebookResult());  // received calls
94        mPhonebooks.put("MC", new PhonebookResult());  // missed calls
95        mPhonebooks.put("ME", new PhonebookResult());  // mobile phonebook
96
97        mCurrentPhonebook = "ME";  // default to mobile phonebook
98
99        mCpbrIndex1 = mCpbrIndex2 = -1;
100        mCheckingAccessPermission = false;
101    }
102
103    /** Returns the last dialled number, or null if no numbers have been called */
104    public String getLastDialledNumber() {
105        String[] projection = {Calls.NUMBER};
106        Cursor cursor = mContext.getContentResolver().query(Calls.CONTENT_URI, projection,
107                Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER +
108                " LIMIT 1");
109        if (cursor == null) return null;
110
111        if (cursor.getCount() < 1) {
112            cursor.close();
113            return null;
114        }
115        cursor.moveToNext();
116        int column = cursor.getColumnIndexOrThrow(Calls.NUMBER);
117        String number = cursor.getString(column);
118        cursor.close();
119        return number;
120    }
121
122    /* package */ void handleAccessPermissionResult(Intent intent) {
123        if (!mCheckingAccessPermission) {
124            return;
125        }
126
127        //HeadsetBase headset = mHandsfree.getHeadset();
128        // ASSERT: (headset != null) && headSet.isConnected()
129        // REASON: mCheckingAccessPermission is true, otherwise resetAtState
130        //         has set mCheckingAccessPermission to false
131
132        if (intent.getAction().equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
133
134            if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
135                                   BluetoothDevice.CONNECTION_ACCESS_NO) ==
136                BluetoothDevice.CONNECTION_ACCESS_YES) {
137                // BluetoothDevice remoteDevice = headset.getRemoteDevice();
138                // TODO(BT) when we do CPBR, fix this NullPointerException
139                BluetoothDevice remoteDevice = null;
140                if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
141                    remoteDevice.setTrust(true);
142                }
143
144                // AtCommandResult cpbrResult = processCpbrCommand();
145                // headset.sendURC(cpbrResult.toString());
146            } else {
147                // headset.sendURC("ERROR");
148            }
149        }
150        mCpbrIndex1 = mCpbrIndex2 = -1;
151        mCheckingAccessPermission = false;
152    }
153
154    /** Get the most recent result for the given phone book,
155     *  with the cursor ready to go.
156     *  If force then re-query that phonebook
157     *  Returns null if the cursor is not ready
158     */
159    private synchronized PhonebookResult getPhonebookResult(String pb, boolean force) {
160        if (pb == null) {
161            return null;
162        }
163        PhonebookResult pbr = mPhonebooks.get(pb);
164        if (pbr == null) {
165            pbr = new PhonebookResult();
166        }
167        if (force || pbr.cursor == null) {
168            if (!queryPhonebook(pb, pbr)) {
169                return null;
170            }
171        }
172
173        return pbr;
174    }
175
176    private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) {
177        String where;
178        boolean ancillaryPhonebook = true;
179
180        if (pb.equals("ME")) {
181            ancillaryPhonebook = false;
182            where = VISIBLE_PHONEBOOK_WHERE;
183        } else if (pb.equals("DC")) {
184            where = OUTGOING_CALL_WHERE;
185        } else if (pb.equals("RC")) {
186            where = INCOMING_CALL_WHERE;
187        } else if (pb.equals("MC")) {
188            where = MISSED_CALL_WHERE;
189        } else {
190            return false;
191        }
192
193        if (pbr.cursor != null) {
194            pbr.cursor.close();
195            pbr.cursor = null;
196        }
197
198        if (ancillaryPhonebook) {
199            pbr.cursor = mContext.getContentResolver().query(
200                    Calls.CONTENT_URI, CALLS_PROJECTION, where, null,
201                    Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
202            if (pbr.cursor == null) return false;
203
204            pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER);
205            pbr.typeColumn = -1;
206            pbr.nameColumn = -1;
207        } else {
208            pbr.cursor = mContext.getContentResolver().query(Phone.CONTENT_URI, PHONES_PROJECTION,
209                    where, null, Phone.NUMBER + " LIMIT " + MAX_PHONEBOOK_SIZE);
210            if (pbr.cursor == null) return false;
211
212            pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER);
213            pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE);
214            pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME);
215        }
216        Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results");
217        return true;
218    }
219
220    synchronized void resetAtState() {
221        mCharacterSet = "UTF-8";
222        mCpbrIndex1 = mCpbrIndex2 = -1;
223        mCheckingAccessPermission = false;
224    }
225
226    private synchronized int getMaxPhoneBookSize(int currSize) {
227        // some car kits ignore the current size and request max phone book
228        // size entries. Thus, it takes a long time to transfer all the
229        // entries. Use a heuristic to calculate the max phone book size
230        // considering future expansion.
231        // maxSize = currSize + currSize / 2 rounded up to nearest power of 2
232        // If currSize < 100, use 100 as the currSize
233
234        int maxSize = (currSize < 100) ? 100 : currSize;
235        maxSize += maxSize / 2;
236        return roundUpToPowerOfTwo(maxSize);
237    }
238
239    private int roundUpToPowerOfTwo(int x) {
240        x |= x >> 1;
241        x |= x >> 2;
242        x |= x >> 4;
243        x |= x >> 8;
244        x |= x >> 16;
245        return x + 1;
246    }
247
248    // process CPBR command after permission check
249    private String processCpbrCommand()
250    {
251        // Shortcut SM phonebook
252        if ("SM".equals(mCurrentPhonebook)) {
253            // return new AtCommandResult(AtCommandResult.OK);
254        }
255
256        // Check phonebook
257        PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, false);
258        if (pbr == null) {
259            // return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
260            return null;
261        }
262
263        // More sanity checks
264        // Send OK instead of ERROR if these checks fail.
265        // When we send error, certain kits like BMW disconnect the
266        // Handsfree connection.
267        if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1  ||
268            mCpbrIndex2 > pbr.cursor.getCount() || mCpbrIndex1 > pbr.cursor.getCount()) {
269            // return new AtCommandResult(AtCommandResult.OK);
270            return null;
271        }
272
273        // Process
274        //AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
275        int errorDetected = -1; // no error
276        pbr.cursor.moveToPosition(mCpbrIndex1 - 1);
277        for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) {
278            String number = pbr.cursor.getString(pbr.numberColumn);
279            String name = null;
280            int type = -1;
281            if (pbr.nameColumn == -1) {
282                // try caller id lookup
283                // TODO: This code is horribly inefficient. I saw it
284                // take 7 seconds to process 100 missed calls.
285                Cursor c = mContext.getContentResolver().
286                    query(Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
287                          new String[] {PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE},
288                          null, null, null);
289                if (c != null) {
290                    if (c.moveToFirst()) {
291                        name = c.getString(0);
292                        type = c.getInt(1);
293                    }
294                    c.close();
295                }
296                if (DBG && name == null) log("Caller ID lookup failed for " + number);
297
298            } else {
299                name = pbr.cursor.getString(pbr.nameColumn);
300            }
301            if (name == null) name = "";
302            name = name.trim();
303            if (name.length() > 28) name = name.substring(0, 28);
304
305            if (pbr.typeColumn != -1) {
306                type = pbr.cursor.getInt(pbr.typeColumn);
307                name = name + "/" + getPhoneType(type);
308            }
309
310            if (number == null) number = "";
311            int regionType = PhoneNumberUtils.toaFromString(number);
312
313            number = number.trim();
314            number = PhoneNumberUtils.stripSeparators(number);
315            if (number.length() > 30) number = number.substring(0, 30);
316            if (number.equals("-1")) {
317                // unknown numbers are stored as -1 in our database
318                number = "";
319                // name = mContext.getString(R.string.unknown);
320            }
321
322            // TODO(): Handle IRA commands. It's basically
323            // a 7 bit ASCII character set.
324            if (!name.equals("") && mCharacterSet.equals("GSM")) {
325                byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name);
326                if (nameByte == null) {
327                    // name = mContext.getString(R.string.unknown);
328                } else {
329                    name = new String(nameByte);
330                }
331            }
332
333            // result.addResponse("+CPBR: " + index + ",\"" + number + "\"," +
334            //                   regionType + ",\"" + name + "\"");
335            if (!pbr.cursor.moveToNext()) {
336                break;
337            }
338        }
339        // return result;
340        return null;
341    }
342
343    private static String getPhoneType(int type) {
344        switch (type) {
345            case Phone.TYPE_HOME:
346                return "H";
347            case Phone.TYPE_MOBILE:
348                return "M";
349            case Phone.TYPE_WORK:
350                return "W";
351            case Phone.TYPE_FAX_HOME:
352            case Phone.TYPE_FAX_WORK:
353                return "F";
354            case Phone.TYPE_OTHER:
355            case Phone.TYPE_CUSTOM:
356            default:
357                return "O";
358        }
359    }
360
361    private static void log(String msg) {
362        Log.d(TAG, msg);
363    }
364}
365