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