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