--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.bluetooth.pbap;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.pim.vcard.VCardBuilder;
+import android.pim.vcard.VCardConfig;
+import android.pim.vcard.VCardConstants;
+import android.pim.vcard.VCardUtils;
+import android.pim.vcard.VCardComposer.OneEntryHandler;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * VCard composer especially for Call Log used in Bluetooth.
+ */
+public class BluetoothPbapCallLogComposer {
+ private static final String TAG = "CallLogComposer";
+
+ private static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
+ "Failed to get database information";
+
+ private static final String FAILURE_REASON_NO_ENTRY =
+ "There's no exportable in the database";
+
+ private static final String FAILURE_REASON_NOT_INITIALIZED =
+ "The vCard composer object is not correctly initialized";
+
+ /** Should be visible only from developers... (no need to translate, hopefully) */
+ private static final String FAILURE_REASON_UNSUPPORTED_URI =
+ "The Uri vCard composer received is not supported by the composer.";
+
+ private static final String NO_ERROR = "No error";
+
+ /** The projection to use when querying the call log table */
+ private static final String[] sCallLogProjection = new String[] {
+ Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE,
+ Calls.CACHED_NUMBER_LABEL
+ };
+ private static final int NUMBER_COLUMN_INDEX = 0;
+ private static final int DATE_COLUMN_INDEX = 1;
+ private static final int CALL_TYPE_COLUMN_INDEX = 2;
+ private static final int CALLER_NAME_COLUMN_INDEX = 3;
+ private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4;
+ private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5;
+
+ // Property for call log entry
+ private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME";
+ private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING";
+ private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING";
+ private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
+
+ private static final String FLAG_TIMEZONE_UTC = "Z";
+
+ private final Context mContext;
+ private ContentResolver mContentResolver;
+ private Cursor mCursor;
+ private final boolean mCareHandlerErrors;
+
+ private boolean mTerminateIsCalled;
+ private final List<OneEntryHandler> mHandlerList;
+
+ private String mErrorReason = NO_ERROR;
+
+ public BluetoothPbapCallLogComposer(final Context context, boolean careHandlerErrors) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mCareHandlerErrors = careHandlerErrors;
+ mHandlerList = new ArrayList<OneEntryHandler>();
+ }
+
+ public boolean init(final Uri contentUri, final String selection,
+ final String[] selectionArgs, final String sortOrder) {
+ final String[] projection;
+ if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
+ projection = sCallLogProjection;
+ } else {
+ mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
+ return false;
+ }
+
+ mCursor = mContentResolver.query(
+ contentUri, projection, selection, selectionArgs, sortOrder);
+
+ if (mCursor == null) {
+ mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
+ return false;
+ }
+
+ if (mCareHandlerErrors) {
+ List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
+ mHandlerList.size());
+ for (OneEntryHandler handler : mHandlerList) {
+ if (!handler.onInit(mContext)) {
+ for (OneEntryHandler finished : finishedList) {
+ finished.onTerminate();
+ }
+ return false;
+ }
+ }
+ } else {
+ // Just ignore the false returned from onInit().
+ for (OneEntryHandler handler : mHandlerList) {
+ handler.onInit(mContext);
+ }
+ }
+
+ if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
+ try {
+ mCursor.close();
+ } catch (SQLiteException e) {
+ Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
+ } finally {
+ mErrorReason = FAILURE_REASON_NO_ENTRY;
+ mCursor = null;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ public void addHandler(OneEntryHandler handler) {
+ if (handler != null) {
+ mHandlerList.add(handler);
+ }
+ }
+
+ public boolean createOneEntry() {
+ if (mCursor == null || mCursor.isAfterLast()) {
+ mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
+ return false;
+ }
+
+ final String vcard;
+ try {
+ vcard = createOneCallLogEntryInternal();
+ } catch (OutOfMemoryError error) {
+ Log.e(TAG, "OutOfMemoryError occured. Ignore the entry");
+ System.gc();
+ return true;
+ }
+
+ if (mCareHandlerErrors) {
+ List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
+ mHandlerList.size());
+ for (OneEntryHandler handler : mHandlerList) {
+ if (!handler.onEntryCreated(vcard)) {
+ return false;
+ }
+ }
+ } else {
+ for (OneEntryHandler handler : mHandlerList) {
+ handler.onEntryCreated(vcard);
+ }
+ }
+
+ return true;
+ }
+
+ private String createOneCallLogEntryInternal() {
+ final VCardBuilder builder = new VCardBuilder(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8);
+ String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
+ if (TextUtils.isEmpty(name)) {
+ name = mCursor.getString(NUMBER_COLUMN_INDEX);
+ }
+ final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
+ builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false);
+ builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false);
+
+ final String number = mCursor.getString(NUMBER_COLUMN_INDEX);
+ final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
+ String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
+ if (TextUtils.isEmpty(label)) {
+ label = Integer.toString(type);
+ }
+ builder.appendTelLine(type, label, number, false);
+ tryAppendCallHistoryTimeStampField(builder);
+
+ return builder.toString();
+ }
+
+ /**
+ * This static function is to compose vCard for phone own number
+ */
+ public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
+ String phoneNumber, boolean vcardVer21) {
+ final int vcardType = (vcardVer21 ?
+ VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 :
+ VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8);
+ final VCardBuilder builder = new VCardBuilder(vcardType);
+ boolean needCharset = false;
+ if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
+ needCharset = true;
+ }
+ builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false);
+ builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false);
+
+ if (!TextUtils.isEmpty(phoneNumber)) {
+ String label = Integer.toString(phonetype);
+ builder.appendTelLine(phonetype, label, phoneNumber, false);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Format according to RFC 2445 DATETIME type.
+ * The format is: ("%Y%m%dT%H%M%SZ").
+ */
+ private final String toRfc2455Format(final long millSecs) {
+ Time startDate = new Time();
+ startDate.set(millSecs);
+ String date = startDate.format2445();
+ return date + FLAG_TIMEZONE_UTC;
+ }
+
+ /**
+ * Try to append the property line for a call history time stamp field if possible.
+ * Do nothing if the call log type gotton from the database is invalid.
+ */
+ private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) {
+ // Extension for call history as defined in
+ // in the Specification for Ic Mobile Communcation - ver 1.1,
+ // Oct 2000. This is used to send the details of the call
+ // history - missed, incoming, outgoing along with date and time
+ // to the requesting device (For example, transferring phone book
+ // when connected over bluetooth)
+ //
+ // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z"
+ final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
+ final String callLogTypeStr;
+ switch (callLogType) {
+ case Calls.INCOMING_TYPE: {
+ callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
+ break;
+ }
+ case Calls.OUTGOING_TYPE: {
+ callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
+ break;
+ }
+ case Calls.MISSED_TYPE: {
+ callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
+ break;
+ }
+ default: {
+ Log.w(TAG, "Call log type not correct.");
+ return;
+ }
+ }
+
+ final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
+ builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP,
+ Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong));
+ }
+
+ public void terminate() {
+ for (OneEntryHandler handler : mHandlerList) {
+ handler.onTerminate();
+ }
+
+ if (mCursor != null) {
+ try {
+ mCursor.close();
+ } catch (SQLiteException e) {
+ Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
+ }
+ mCursor = null;
+ }
+
+ mTerminateIsCalled = true;
+ }
+
+ @Override
+ public void finalize() {
+ if (!mTerminateIsCalled) {
+ terminate();
+ }
+ }
+
+ public int getCount() {
+ if (mCursor == null) {
+ return 0;
+ }
+ return mCursor.getCount();
+ }
+
+ public boolean isAfterLast() {
+ if (mCursor == null) {
+ return false;
+ }
+ return mCursor.isAfterLast();
+ }
+
+ public String getErrorReason() {
+ return mErrorReason;
+ }
+}
package com.android.bluetooth.pbap;
-import com.android.bluetooth.R;
-
-import android.net.Uri;
-import android.os.Handler;
-import android.text.TextUtils;
-import android.util.Log;
-import android.database.Cursor;
import android.content.ContentResolver;
import android.content.Context;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.PhoneLookup;
+import android.database.Cursor;
+import android.net.Uri;
import android.pim.vcard.VCardComposer;
import android.pim.vcard.VCardConfig;
import android.pim.vcard.VCardComposer.OneEntryHandler;
-import android.provider.ContactsContract;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.bluetooth.R;
-import javax.obex.ResponseCodes;
-import javax.obex.Operation;
import java.io.IOException;
import java.io.OutputStream;
-import java.io.Writer;
import java.util.ArrayList;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+
public class BluetoothPbapVcardManager {
private static final String TAG = "BluetoothPbapVcardManager";
}
public final String getOwnerPhoneNumberVcard(final boolean vcardType21) {
- VCardComposer composer = new VCardComposer(mContext);
+ BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext, false);
String name = BluetoothPbapService.getLocalPhoneName();
String number = BluetoothPbapService.getLocalPhoneNum();
String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
long timestamp = 0;
if (V) timestamp = System.currentTimeMillis();
- VCardComposer composer = null;
- try {
- // Currently only support Generic Vcard 2.1 and 3.0
- int vcardType;
- if (vcardType21) {
- vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8;
- } else {
- vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8;
- }
-
- composer = new VCardComposer(mContext, vcardType, true);
- composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
- final Uri contentUri = (isContacts ? Contacts.CONTENT_URI : CallLog.Calls.CONTENT_URI);
- if (!composer.init(contentUri, selection, null, null)) {
- return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
+ if (isContacts) {
+ VCardComposer composer = null;
+ try {
+ // Currently only support Generic Vcard 2.1 and 3.0
+ int vcardType;
+ if (vcardType21) {
+ vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8;
+ } else {
+ vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8;
+ }
- while (!composer.isAfterLast()) {
- if (!composer.createOneEntry()) {
- Log.e(TAG, "Failed to read a contact. Error reason: "
- + composer.getErrorReason());
+ composer = new VCardComposer(mContext, vcardType, true);
+ if (!composer.init(Contacts.CONTENT_URI, selection, null, null)) {
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
+
+ while (!composer.isAfterLast()) {
+ if (!composer.createOneEntry()) {
+ Log.e(TAG, "Failed to read a contact. Error reason: "
+ + composer.getErrorReason());
+ return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+ }
+ }
+ } finally {
+ if (composer != null) {
+ composer.terminate();
+ }
}
- } finally {
- if (composer != null) {
- composer.terminate();
+ } else { // CallLog
+ BluetoothPbapCallLogComposer composer = null;
+ try {
+ composer = new BluetoothPbapCallLogComposer(mContext, true);
+ composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
+ if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, null)) {
+ return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+ }
+
+ while (!composer.isAfterLast()) {
+ if (!composer.createOneEntry()) {
+ Log.e(TAG, "Failed to read a contact. Error reason: "
+ + composer.getErrorReason());
+ return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+ }
+ }
+ } finally {
+ if (composer != null) {
+ composer.terminate();
+ }
}
}