OSDN Git Service

Move Pbap logic to Bluetooth package so that we do not have unnecessary tight coupling.
authorDaisuke Miyakawa <dmiyakawa@google.com>
Tue, 23 Feb 2010 06:55:19 +0000 (15:55 +0900)
committerDaisuke Miyakawa <dmiyakawa@google.com>
Wed, 24 Feb 2010 10:59:50 +0000 (19:59 +0900)
src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java [new file with mode: 0644]
src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java

diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java b/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
new file mode 100644 (file)
index 0000000..abaf3ec
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+ * 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;
+    }
+}
index 634bef8..f51f284 100644 (file)
 
 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";
 
@@ -115,7 +111,7 @@ public class 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,
@@ -425,33 +421,54 @@ public class BluetoothPbapVcardManager {
         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();
+                }
             }
         }