BluetoothPbapCallLogComposer.java revision 00aee7a70e3c76b6c9ef59133d59fcdc592fea99
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package com.android.bluetooth.pbap;
17
18import com.android.bluetooth.R;
19
20import android.content.ContentResolver;
21import android.content.Context;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteException;
24import android.net.Uri;
25import android.pim.vcard.VCardBuilder;
26import android.pim.vcard.VCardConfig;
27import android.pim.vcard.VCardConstants;
28import android.pim.vcard.VCardUtils;
29import android.pim.vcard.VCardComposer.OneEntryHandler;
30import android.provider.CallLog;
31import android.provider.CallLog.Calls;
32import android.text.TextUtils;
33import android.text.format.Time;
34import android.util.Log;
35
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.List;
39
40/**
41 * VCard composer especially for Call Log used in Bluetooth.
42 */
43public class BluetoothPbapCallLogComposer {
44    private static final String TAG = "CallLogComposer";
45
46    private static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
47        "Failed to get database information";
48
49    private static final String FAILURE_REASON_NO_ENTRY =
50        "There's no exportable in the database";
51
52    private static final String FAILURE_REASON_NOT_INITIALIZED =
53        "The vCard composer object is not correctly initialized";
54
55    /** Should be visible only from developers... (no need to translate, hopefully) */
56    private static final String FAILURE_REASON_UNSUPPORTED_URI =
57        "The Uri vCard composer received is not supported by the composer.";
58
59    private static final String NO_ERROR = "No error";
60
61    /** The projection to use when querying the call log table */
62    private static final String[] sCallLogProjection = new String[] {
63            Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE,
64            Calls.CACHED_NUMBER_LABEL
65    };
66    private static final int NUMBER_COLUMN_INDEX = 0;
67    private static final int DATE_COLUMN_INDEX = 1;
68    private static final int CALL_TYPE_COLUMN_INDEX = 2;
69    private static final int CALLER_NAME_COLUMN_INDEX = 3;
70    private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4;
71    private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5;
72
73    // Property for call log entry
74    private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME";
75    private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "RECEIVED";
76    private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "DIALED";
77    private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
78
79    private static final String FLAG_TIMEZONE_UTC = "Z";
80
81    private final Context mContext;
82    private ContentResolver mContentResolver;
83    private Cursor mCursor;
84    private final boolean mCareHandlerErrors;
85
86    private boolean mTerminateIsCalled;
87    private final List<OneEntryHandler> mHandlerList;
88
89    private String mErrorReason = NO_ERROR;
90
91    public BluetoothPbapCallLogComposer(final Context context, boolean careHandlerErrors) {
92        mContext = context;
93        mContentResolver = context.getContentResolver();
94        mCareHandlerErrors = careHandlerErrors;
95        mHandlerList = new ArrayList<OneEntryHandler>();
96    }
97
98    public boolean init(final Uri contentUri, final String selection,
99            final String[] selectionArgs, final String sortOrder) {
100        final String[] projection;
101        if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
102            projection = sCallLogProjection;
103        } else {
104            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
105            return false;
106        }
107
108        mCursor = mContentResolver.query(
109                contentUri, projection, selection, selectionArgs, sortOrder);
110
111        if (mCursor == null) {
112            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
113            return false;
114        }
115
116        if (mCareHandlerErrors) {
117            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
118                    mHandlerList.size());
119            for (OneEntryHandler handler : mHandlerList) {
120                if (!handler.onInit(mContext)) {
121                    for (OneEntryHandler finished : finishedList) {
122                        finished.onTerminate();
123                    }
124                    return false;
125                }
126            }
127        } else {
128            // Just ignore the false returned from onInit().
129            for (OneEntryHandler handler : mHandlerList) {
130                handler.onInit(mContext);
131            }
132        }
133
134        if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
135            try {
136                mCursor.close();
137            } catch (SQLiteException e) {
138                Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
139            } finally {
140                mErrorReason = FAILURE_REASON_NO_ENTRY;
141                mCursor = null;
142            }
143            return false;
144        }
145
146        return true;
147    }
148
149    public void addHandler(OneEntryHandler handler) {
150        if (handler != null) {
151            mHandlerList.add(handler);
152        }
153    }
154
155    public boolean createOneEntry() {
156        if (mCursor == null || mCursor.isAfterLast()) {
157            mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
158            return false;
159        }
160
161        final String vcard;
162        try {
163            vcard = createOneCallLogEntryInternal();
164        } catch (OutOfMemoryError error) {
165            Log.e(TAG, "OutOfMemoryError occured. Ignore the entry");
166            System.gc();
167            return true;
168        } finally {
169            mCursor.moveToNext();
170        }
171
172        if (mCareHandlerErrors) {
173            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
174                    mHandlerList.size());
175            for (OneEntryHandler handler : mHandlerList) {
176                if (!handler.onEntryCreated(vcard)) {
177                    return false;
178                }
179            }
180        } else {
181            for (OneEntryHandler handler : mHandlerList) {
182                handler.onEntryCreated(vcard);
183            }
184        }
185
186        return true;
187    }
188
189    private String createOneCallLogEntryInternal() {
190        // We should not allow vCard composer to re-format phone numbers, since
191        // some characters are (inappropriately) removed and devices do not work fine.
192        final int vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 |
193                VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
194        final VCardBuilder builder = new VCardBuilder(vcardType);
195        String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
196        if (TextUtils.isEmpty(name)) {
197            name = mCursor.getString(NUMBER_COLUMN_INDEX);
198        }
199        final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
200        builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false);
201        builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false);
202
203        String number = mCursor.getString(NUMBER_COLUMN_INDEX);
204        if (number.equals("-1")) {
205            number = mContext.getString(R.string.unknownNumber);
206        }
207        final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
208        String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
209        if (TextUtils.isEmpty(label)) {
210            label = Integer.toString(type);
211        }
212        builder.appendTelLine(type, label, number, false);
213        tryAppendCallHistoryTimeStampField(builder);
214
215        return builder.toString();
216    }
217
218    /**
219     * This static function is to compose vCard for phone own number
220     */
221    public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
222            String phoneNumber, boolean vcardVer21) {
223        final int vcardType = (vcardVer21 ?
224                VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 :
225                    VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8) |
226                VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
227        final VCardBuilder builder = new VCardBuilder(vcardType);
228        boolean needCharset = false;
229        if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
230            needCharset = true;
231        }
232        builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false);
233        builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false);
234
235        if (!TextUtils.isEmpty(phoneNumber)) {
236            String label = Integer.toString(phonetype);
237            builder.appendTelLine(phonetype, label, phoneNumber, false);
238        }
239
240        return builder.toString();
241    }
242
243    /**
244     * Format according to RFC 2445 DATETIME type.
245     * The format is: ("%Y%m%dT%H%M%SZ").
246     */
247    private final String toRfc2455Format(final long millSecs) {
248        Time startDate = new Time();
249        startDate.set(millSecs);
250        String date = startDate.format2445();
251        return date + FLAG_TIMEZONE_UTC;
252    }
253
254    /**
255     * Try to append the property line for a call history time stamp field if possible.
256     * Do nothing if the call log type gotton from the database is invalid.
257     */
258    private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) {
259        // Extension for call history as defined in
260        // in the Specification for Ic Mobile Communcation - ver 1.1,
261        // Oct 2000. This is used to send the details of the call
262        // history - missed, incoming, outgoing along with date and time
263        // to the requesting device (For example, transferring phone book
264        // when connected over bluetooth)
265        //
266        // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z"
267        final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
268        final String callLogTypeStr;
269        switch (callLogType) {
270            case Calls.INCOMING_TYPE: {
271                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
272                break;
273            }
274            case Calls.OUTGOING_TYPE: {
275                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
276                break;
277            }
278            case Calls.MISSED_TYPE: {
279                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
280                break;
281            }
282            default: {
283                Log.w(TAG, "Call log type not correct.");
284                return;
285            }
286        }
287
288        final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
289        builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP,
290                Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong));
291    }
292
293    public void terminate() {
294        for (OneEntryHandler handler : mHandlerList) {
295            handler.onTerminate();
296        }
297
298        if (mCursor != null) {
299            try {
300                mCursor.close();
301            } catch (SQLiteException e) {
302                Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
303            }
304            mCursor = null;
305        }
306
307        mTerminateIsCalled = true;
308    }
309
310    @Override
311    public void finalize() {
312        if (!mTerminateIsCalled) {
313            terminate();
314        }
315    }
316
317    public int getCount() {
318        if (mCursor == null) {
319            return 0;
320        }
321        return mCursor.getCount();
322    }
323
324    public boolean isAfterLast() {
325        if (mCursor == null) {
326            return false;
327        }
328        return mCursor.isAfterLast();
329    }
330
331    public String getErrorReason() {
332        return mErrorReason;
333    }
334}
335