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