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;
19import com.android.internal.telephony.CallerInfo;
20
21import android.content.ContentResolver;
22import android.content.Context;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteException;
25import android.net.Uri;
26import android.provider.CallLog;
27import android.provider.CallLog.Calls;
28import android.text.TextUtils;
29import android.text.format.Time;
30import android.util.Log;
31
32import com.android.vcard.VCardBuilder;
33import com.android.vcard.VCardConfig;
34import com.android.vcard.VCardConstants;
35import com.android.vcard.VCardUtils;
36
37import java.util.Arrays;
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 = "RECEIVED";
75    private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "DIALED";
76    private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
77
78    private final Context mContext;
79    private ContentResolver mContentResolver;
80    private Cursor mCursor;
81
82    private boolean mTerminateIsCalled;
83
84    private String mErrorReason = NO_ERROR;
85
86    public BluetoothPbapCallLogComposer(final Context context) {
87        mContext = context;
88        mContentResolver = context.getContentResolver();
89    }
90
91    public boolean init(final Uri contentUri, final String selection,
92            final String[] selectionArgs, final String sortOrder) {
93        final String[] projection;
94        if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
95            projection = sCallLogProjection;
96        } else {
97            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
98            return false;
99        }
100
101        mCursor = mContentResolver.query(
102                contentUri, projection, selection, selectionArgs, sortOrder);
103
104        if (mCursor == null) {
105            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
106            return false;
107        }
108
109        if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
110            try {
111                mCursor.close();
112            } catch (SQLiteException e) {
113                Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
114            } finally {
115                mErrorReason = FAILURE_REASON_NO_ENTRY;
116                mCursor = null;
117            }
118            return false;
119        }
120
121        return true;
122    }
123
124    public String createOneEntry(boolean vcardVer21) {
125        if (mCursor == null || mCursor.isAfterLast()) {
126            mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
127            return null;
128        }
129        try {
130            return createOneCallLogEntryInternal(vcardVer21);
131        } finally {
132            mCursor.moveToNext();
133        }
134    }
135
136    private String createOneCallLogEntryInternal(boolean vcardVer21) {
137        final int vcardType = (vcardVer21 ? VCardConfig.VCARD_TYPE_V21_GENERIC :
138                VCardConfig.VCARD_TYPE_V30_GENERIC) |
139                VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
140        final VCardBuilder builder = new VCardBuilder(vcardType);
141        String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
142        if (TextUtils.isEmpty(name)) {
143            name = "";
144        }
145        if (CallerInfo.UNKNOWN_NUMBER.equals(name) || CallerInfo.PRIVATE_NUMBER.equals(name) ||
146                CallerInfo.PAYPHONE_NUMBER.equals(name)) {
147            // setting name to "" as FN/N must be empty fields in this case.
148            name = "";
149        }
150        final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
151        builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false);
152        builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false);
153
154        String number = mCursor.getString(NUMBER_COLUMN_INDEX);
155        if (CallerInfo.UNKNOWN_NUMBER.equals(number) ||
156                CallerInfo.PRIVATE_NUMBER.equals(number) ||
157                CallerInfo.PAYPHONE_NUMBER.equals(number)) {
158            number = mContext.getString(R.string.unknownNumber);
159        }
160        final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
161        String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
162        if (TextUtils.isEmpty(label)) {
163            label = Integer.toString(type);
164        }
165        builder.appendTelLine(type, label, number, false);
166        tryAppendCallHistoryTimeStampField(builder);
167
168        return builder.toString();
169    }
170
171    /**
172     * This static function is to compose vCard for phone own number
173     */
174    public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
175            String phoneNumber, boolean vcardVer21) {
176        final int vcardType = (vcardVer21 ?
177                VCardConfig.VCARD_TYPE_V21_GENERIC :
178                    VCardConfig.VCARD_TYPE_V30_GENERIC) |
179                VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
180        final VCardBuilder builder = new VCardBuilder(vcardType);
181        boolean needCharset = false;
182        if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
183            needCharset = true;
184        }
185        builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false);
186        builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false);
187
188        if (!TextUtils.isEmpty(phoneNumber)) {
189            String label = Integer.toString(phonetype);
190            builder.appendTelLine(phonetype, label, phoneNumber, false);
191        }
192
193        return builder.toString();
194    }
195
196    /**
197     * Format according to RFC 2445 DATETIME type.
198     * The format is: ("%Y%m%dT%H%M%S").
199     */
200    private final String toRfc2455Format(final long millSecs) {
201        Time startDate = new Time();
202        startDate.set(millSecs);
203        return startDate.format2445();
204    }
205
206    /**
207     * Try to append the property line for a call history time stamp field if possible.
208     * Do nothing if the call log type gotton from the database is invalid.
209     */
210    private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) {
211        // Extension for call history as defined in
212        // in the Specification for Ic Mobile Communcation - ver 1.1,
213        // Oct 2000. This is used to send the details of the call
214        // history - missed, incoming, outgoing along with date and time
215        // to the requesting device (For example, transferring phone book
216        // when connected over bluetooth)
217        //
218        // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000"
219        final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
220        final String callLogTypeStr;
221        switch (callLogType) {
222            case Calls.INCOMING_TYPE: {
223                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
224                break;
225            }
226            case Calls.OUTGOING_TYPE: {
227                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
228                break;
229            }
230            case Calls.MISSED_TYPE: {
231                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
232                break;
233            }
234            default: {
235                Log.w(TAG, "Call log type not correct.");
236                return;
237            }
238        }
239
240        final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
241        builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP,
242                Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong));
243    }
244
245    public void terminate() {
246        if (mCursor != null) {
247            try {
248                mCursor.close();
249            } catch (SQLiteException e) {
250                Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
251            }
252            mCursor = null;
253        }
254
255        mTerminateIsCalled = true;
256    }
257
258    @Override
259    public void finalize() {
260        if (!mTerminateIsCalled) {
261            terminate();
262        }
263    }
264
265    public int getCount() {
266        if (mCursor == null) {
267            return 0;
268        }
269        return mCursor.getCount();
270    }
271
272    public boolean isAfterLast() {
273        if (mCursor == null) {
274            return false;
275        }
276        return mCursor.isAfterLast();
277    }
278
279    public String getErrorReason() {
280        return mErrorReason;
281    }
282}
283