diff options
author | Jackson Fan <xyfan@motorola.com> | 2009-07-19 12:25:14 +0800 |
---|---|---|
committer | Jaikumar Ganesh <jaikumar@google.com> | 2009-07-23 11:53:12 -0700 |
commit | 2c282d5898ac0916470ebfa9ff26ba784cf4bb24 (patch) | |
tree | 6f459a34f97d82ef7754617be3b25eafdc37a969 | |
parent | eb37b3c2c9d59bc1c0f09f911f5bde077a63f4da (diff) | |
download | android_packages_apps_Bluetooth-2c282d5898ac0916470ebfa9ff26ba784cf4bb24.tar.gz android_packages_apps_Bluetooth-2c282d5898ac0916470ebfa9ff26ba784cf4bb24.tar.bz2 android_packages_apps_Bluetooth-2c282d5898ac0916470ebfa9ff26ba784cf4bb24.zip |
Add pbap into bluetooth package
Update PBAP as patchset 5
Adjust some resouces strings after UI test
Change localized resource usage
Modify exception handle, rename a confusing variable name
Remove compile dependency on 240 for now. Add TODO in code
Some minor changes to address the comments
Submit on-behalf of Yue Lixin <a5206c@motorola.com>
Update PBAP as patchset 4 according to comments
Format resource files
Re-organize the logs
Submit on-behalf of Jiafa Liu <pbx376@motorola.com>
-rw-r--r-- | AndroidManifest.xml | 18 | ||||
-rw-r--r-- | res/layout/access.xml | 28 | ||||
-rw-r--r-- | res/layout/auth.xml | 30 | ||||
-rw-r--r-- | res/values/strings_pbap.xml | 28 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapActivity.java | 284 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java | 75 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java | 884 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapReceiver.java | 111 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java | 59 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapService.java | 634 | ||||
-rw-r--r-- | src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java | 511 |
11 files changed, 2662 insertions, 0 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 491e02e26..9323cfc18 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -13,6 +13,9 @@ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_CONTACTS" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> <application android:icon="@drawable/stat_sys_data_bt" android:label="@string/app_name"> <uses-library android:name="javax.obex" /> @@ -75,5 +78,20 @@ <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> + <activity android:name=".pbap.BluetoothPbapActivity" + android:label=" " + android:theme="@*android:style/Theme.Dialog.Alert"> + <intent-filter> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <service android:name=".pbap.BluetoothPbapService" > + <action android:name="android.bluetooth.IBluetoothPbap"/> + </service> + <receiver android:name=".pbap.BluetoothPbapReceiver"> + <intent-filter> + <action android:name="android.bluetooth.intent.action.BLUETOOTH_STATE_CHANGED"/> + </intent-filter> + </receiver> </application> </manifest> diff --git a/res/layout/access.xml b/res/layout/access.xml new file mode 100644 index 000000000..9fdb9587c --- /dev/null +++ b/res/layout/access.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="fill_parent" + android:layout_width="fill_parent"> + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="fill_parent" + android:layout_width="fill_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/message" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <CheckBox android:id="@+id/alwaysallowed" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:text="@string/alwaysallowed" /> + + </LinearLayout> + +</ScrollView> diff --git a/res/layout/auth.xml b/res/layout/auth.xml new file mode 100644 index 000000000..d6a35cd0d --- /dev/null +++ b/res/layout/auth.xml @@ -0,0 +1,30 @@ +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="fill_parent" + android:layout_width="fill_parent"> + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="fill_parent" + android:layout_width="fill_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/message" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <EditText + android:id="@+id/text" + android:layout_height="wrap_content" + android:layout_width="fill_parent" + android:layout_marginTop="20dip" + android:layout_marginLeft="20dip" + android:layout_marginRight="20dip" + android:singleLine="true" /> + + </LinearLayout> + +</ScrollView> diff --git a/res/values/strings_pbap.xml b/res/values/strings_pbap.xml new file mode 100644 index 000000000..44f1fbdba --- /dev/null +++ b/res/values/strings_pbap.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pbap_app_name">Bluetooth Pbap</string> + <string name="pbap_acceptance_dialog_title">%1$s would like to access your contacts and call history. Give access to %2$s?</string> + <string name="pbap_acceptance_dialog_header">File transfer</string> + <string name="pbap_session_key_dialog_title">Type session key for %1$s</string> + <string name="pbap_session_key_dialog_header">Bluetooth session key required</string> + <string name="pbap_acceptance_timeout_message">There was time out to accept connection with %1$s</string> + <string name="pbap_authentication_timeout_message">There was time out to input session key with %1$s</string> + <string name="bluetooth_transfer_header">Bluetooth transfer</string> + <string name="bluetooth_transfer_text">could not successfully establish phone book sharing with</string> + <string name="toast_connected">%1$s can now access your contacts and call history</string> + <string name="toast_disconnected">%1$s will not be given access</string> + <string name="pbap_notif_ticker">Phonebook access request</string> + <!-- Notification title when a Bluetooth device wants to pair with us --> + <string name="pbap_notif_title">PBAP request</string> + <!-- Notification message when a Bluetooth device wants to pair with us --> + <string name="pbap_notif_message">Allow phonebook access by %1$s</string> + <string name="auth_notif_ticker">Obex authentication request</string> + <!-- Notification title when a Bluetooth device wants to pair with us --> + <string name="auth_notif_title">Session Key</string> + <!-- Notification message when a Bluetooth device wants to pair with us --> + <string name="auth_notif_message">Type session key for %1$s</string> + <string name="alwaysallowed">Always allowed?</string> + <string name="defaultname">carkit</string> + <string name="unknownName">Unknown name</string> + <string name="defaultnumber">000000</string> +</resources> diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java b/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java new file mode 100644 index 000000000..ab823f24a --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java @@ -0,0 +1,284 @@ + +package com.android.bluetooth.pbap; + +import com.android.bluetooth.R; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.preference.Preference; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Button; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.text.InputFilter.LengthFilter; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; + +/** + * PbapActivity shows two dialogues: One for accepting incoming pbap request and + * the other prompts the user to enter a session key for authentication with a + * remote Bluetooth device. + */ +public class BluetoothPbapActivity extends AlertActivity implements + DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener, TextWatcher { + private static final String TAG = "BluetoothPbapActivity"; + + private static final int BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH = 16; + + private static final int DIALOG_YES_NO_CONNECT = 1; + + private static final int DIALOG_YES_NO_AUTH = 2; + + private View mView; + + private EditText mKeyView; + + private TextView messageView; + + private String mSessionKey = ""; + + private int mCurrentDialog = 0; + + private Button mOkButton; + + private CheckBox mAlwaysAllowed; + + private static final String USER_TIMEOUT = "user_timeout"; + + private boolean mUserConfirmTimeout = false; + + private boolean mAlwaysAllowedValue = false; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!BluetoothPbapService.USER_CONFIRM_TIMEOUT.equals(intent.getAction())) { + return; + } + onUserConfirmTimeout(); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent i = getIntent(); + String action = i.getAction(); + if (action.equals(BluetoothPbapService.ACCESS_REQUEST)) { + showPbapDialog(DIALOG_YES_NO_CONNECT); + mCurrentDialog = DIALOG_YES_NO_CONNECT; + } else if (action.equals(BluetoothPbapService.AUTH_CHALL)) { + showPbapDialog(DIALOG_YES_NO_AUTH); + mCurrentDialog = DIALOG_YES_NO_AUTH; + } else { + Log.e(TAG, "Error: this activity may be started only with intent " + + "PBAP_ACCESS_REQUEST or PBAP_AUTH_CHALL "); + finish(); + } + registerReceiver(mReceiver, new IntentFilter(BluetoothPbapService.USER_CONFIRM_TIMEOUT)); + } + + private void showPbapDialog(int id) { + final AlertController.AlertParams p = mAlertParams; + switch (id) { + case DIALOG_YES_NO_CONNECT: + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.pbap_acceptance_dialog_header); + p.mView = createView(DIALOG_YES_NO_CONNECT); + p.mPositiveButtonText = getString(android.R.string.yes); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(android.R.string.no); + p.mNegativeButtonListener = this; + mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + setupAlert(); + break; + case DIALOG_YES_NO_AUTH: + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.pbap_session_key_dialog_header); + p.mView = createView(DIALOG_YES_NO_AUTH); + p.mPositiveButtonText = getString(android.R.string.ok); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(android.R.string.cancel); + p.mNegativeButtonListener = this; + setupAlert(); + mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + mOkButton.setEnabled(false); + break; + default: + break; + } + } + + private String createDisplayText(final int id) { + String mRemoteName = BluetoothPbapService.getRemoteDeviceName(); + switch (id) { + case DIALOG_YES_NO_CONNECT: + String mMessage1 = getString(R.string.pbap_acceptance_dialog_title, mRemoteName, + mRemoteName); + return mMessage1; + case DIALOG_YES_NO_AUTH: + String mMessage2 = getString(R.string.pbap_session_key_dialog_title, mRemoteName); + return mMessage2; + default: + return null; + } + } + + private View createView(final int id) { + switch (id) { + case DIALOG_YES_NO_CONNECT: + mView = getLayoutInflater().inflate(R.layout.access, null); + messageView = (TextView)mView.findViewById(R.id.message); + messageView.setText(createDisplayText(id)); + mAlwaysAllowed = (CheckBox)mView.findViewById(R.id.alwaysallowed); + mAlwaysAllowed.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mAlwaysAllowedValue = true; + } + } + }); + return mView; + case DIALOG_YES_NO_AUTH: + mView = getLayoutInflater().inflate(R.layout.auth, null); + messageView = (TextView)mView.findViewById(R.id.message); + messageView.setText(createDisplayText(id)); + mKeyView = (EditText)mView.findViewById(R.id.text); + mKeyView.addTextChangedListener(this); + mKeyView.setFilters(new InputFilter[] { + new LengthFilter(BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH) + }); + return mView; + default: + return null; + } + } + + private void onPositive() { + if (!mUserConfirmTimeout) { + if (mCurrentDialog == DIALOG_YES_NO_CONNECT) { + sendIntentToReceiver(BluetoothPbapService.ACCESS_ALLOWED, + BluetoothPbapService.ALWAYS_ALLOWED, mAlwaysAllowedValue); + } else if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + sendIntentToReceiver(BluetoothPbapService.AUTH_RESPONSE, + BluetoothPbapService.SESSION_KEY, mSessionKey); + mKeyView.removeTextChangedListener(this); + } + } + mUserConfirmTimeout = false; + finish(); + } + + private void onNegative() { + if (mCurrentDialog == DIALOG_YES_NO_CONNECT) { + sendIntentToReceiver(BluetoothPbapService.ACCESS_DISALLOWED, null, null); + } else if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + sendIntentToReceiver(BluetoothPbapService.AUTH_CANCELLED, null, null); + mKeyView.removeTextChangedListener(this); + } + finish(); + } + + private void sendIntentToReceiver(final String intentName, final String extraName, + final String extraValue) { + Intent intent = new Intent(intentName); + intent.setClassName(BluetoothPbapService.THIS_PACKAGE_NAME, BluetoothPbapReceiver.class + .getName()); + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + sendBroadcast(intent); + } + + private void sendIntentToReceiver(final String intentName, final String extraName, + final boolean extraValue) { + Intent intent = new Intent(intentName); + intent.setClassName(BluetoothPbapService.THIS_PACKAGE_NAME, BluetoothPbapReceiver.class + .getName()); + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + sendBroadcast(intent); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + mSessionKey = mKeyView.getText().toString(); + } + onPositive(); + break; + + case DialogInterface.BUTTON_NEGATIVE: + onNegative(); + break; + } + } + + private void onUserConfirmTimeout() { + mUserConfirmTimeout = true; + if (mCurrentDialog == DIALOG_YES_NO_CONNECT) { + messageView.setText(getString(R.string.pbap_acceptance_timeout_message, + BluetoothPbapService.getRemoteDeviceName())); + mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE); + mAlwaysAllowed.setVisibility(View.GONE); + mAlwaysAllowed.clearFocus(); + } else if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + messageView.setText(getString(R.string.pbap_authentication_timeout_message, + BluetoothPbapService.getRemoteDeviceName())); + mKeyView.setVisibility(View.GONE); + mKeyView.clearFocus(); + mKeyView.removeTextChangedListener(this); + mOkButton.setEnabled(true); + mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE); + } + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mUserConfirmTimeout = savedInstanceState.getBoolean(USER_TIMEOUT); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(USER_TIMEOUT, mUserConfirmTimeout); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(mReceiver); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + return true; + } + + // Not used + public void beforeTextChanged(CharSequence s, int start, int before, int after) { + } + + // Not used + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(android.text.Editable s) { + if (s.length() > 0) { + mOkButton.setEnabled(true); + } + } +} diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java b/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java new file mode 100644 index 000000000..d8305dec1 --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java @@ -0,0 +1,75 @@ + +package com.android.bluetooth.pbap; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import javax.obex.Authenticator; +import javax.obex.PasswordAuthentication; + +/** + * BluetoothPbapAuthenticator is a used by BluetoothObexServer for obex + * authentication procedure. + */ +public class BluetoothPbapAuthenticator implements Authenticator { + private static final String TAG = "BluetoothPbapAuthenticator"; + + private boolean mChallenged; + + private boolean mAuthCancelled; + + private String mSessionKey; + + private Handler mCallback; + + public BluetoothPbapAuthenticator(final Handler callback) { + mCallback = callback; + mChallenged = false; + mAuthCancelled = false; + mSessionKey = null; + } + + public final void setChallenged(final boolean bool) { + mChallenged = bool; + } + + public final synchronized void setCancelled(final boolean bool) { + mAuthCancelled = bool; + } + + public final void setSessionKey(final String string) { + mSessionKey = string; + } + + private void waitUserConfirmation() { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothPbapService.MSG_OBEX_AUTH_CHALL; + msg.sendToTarget(); + synchronized (this) { + while (!mChallenged && !mAuthCancelled) { + try { + wait(); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted while waiting on isChalled"); + } + } + } + } + + public PasswordAuthentication onAuthenticationChallenge(final String description, + final boolean isUserIdRequired, final boolean isFullAccess) { + waitUserConfirmation(); + if (mSessionKey.trim().length() != 0) { + PasswordAuthentication pa = new PasswordAuthentication(null, mSessionKey.getBytes()); + return pa; + } + return null; + } + + // TODO reserved for future use only.In case PSE challenge PCE + public byte[] onAuthenticationResponse(final byte[] userName) { + byte[] b = null; + return b; + } +} diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java new file mode 100644 index 000000000..8d6d5dc05 --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java @@ -0,0 +1,884 @@ + +package com.android.bluetooth.pbap; + +import android.os.Message; +import android.os.Handler; +import android.util.Log; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; + +import javax.obex.ServerRequestHandler; +import javax.obex.ResponseCodes; +import javax.obex.ApplicationParameter; +import javax.obex.Operation; +import javax.obex.HeaderSet; + +public class BluetoothPbapObexServer extends ServerRequestHandler { + + private static final String TAG = "BluetoothPbapObexServer"; + + private static final int UUID_LENGTH = 16; + + private static final int LEAGAL_PATH_NUM = 10; + + private static final int VCARD_NAME_MIN_LEN = 5; + + private long mConnectionId; + + private Handler mCallback = null; + + // 128 bit UUID for PBAP + private static final byte[] PBAP_TARGET = new byte[] { + 0x79, 0x61, 0x35, (byte)0xf0, (byte)0xf0, (byte)0xc5, 0x11, (byte)0xd8, 0x09, 0x66, + 0x08, 0x00, 0x20, 0x0c, (byte)0x9a, 0x66 + }; + + private static final String[] LEAGEL_PATH = { + "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch", + "/telecom/cch", "/SIM1", "/SIM1/telecom", "/SIM1/telecom/ich", "/SIM1/telecom/och", + "/SIM1/telecom/mch", "/SIM1/telecom/cch", "/SIM1/telecom/pb" + }; + + // missed call history + private static final String MCH = "mch"; + + // incoming call history + private static final String ICH = "ich"; + + // outgoing call history + private static final String OCH = "och"; + + // combined call history + private static final String CCH = "cch"; + + // phone book + private static final String PB = "pb"; + + private static final String ICH_PATH = "/telecom/ich"; + + private static final String OCH_PATH = "/telecom/och"; + + private static final String MCH_PATH = "/telecom/mch"; + + private static final String CCH_PATH = "/telecom/cch"; + + private static final String PB_PATH = "/telecom/pb"; + + // type for list vcard objects + private static final String TYPE_LISTING = "x-bt/vcard-listing"; + + // type for get single vcard object + private static final String TYPE_VCARD = "x-bt/vcard"; + + // type for download all vcard objects + private static final String TYPE_PB = "x-bt/phonebook"; + + // record current path the client are browsing + private String mCurrentPath = ""; + + // record how many missed call happens since last client operation + private int mMissedCallSize = 0; + + private static final int NEED_PB_SIZE = 0x01; + + private static final int NEED_NEW_MISSED_CALL_NUMBER = 0x02; + + public static final int NEED_PHONEBOOK = 0x04; + + public static final int NEED_INCOMING_CALL_NUMBER = 0x08; + + public static final int NEED_OUTGOING_CALL_NUMBER = 0x10; + + public static final int NEED_MISSED_CALL_NUMBER = 0x20; + + public static final int NEED_COMBINED_CALL_NUMBER = 0x40; + + private static final int PRECONDITION_FAILED = -1; + + private static final int INTERNAL_ERROR = -2; + + public BluetoothPbapObexServer(Handler callback) { + super(); + mConnectionId = -1; + mCallback = callback; + } + + @Override + public int onConnect(final HeaderSet request, final HeaderSet reply) { + try { + byte[] uuid_tmp = (byte[])request.getHeader(HeaderSet.TARGET); + if (uuid_tmp.length != UUID_LENGTH) { + Log.w(TAG, "Wrong UUID length"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + for (int i = 0; i < UUID_LENGTH; i++) { + if (uuid_tmp[i] != PBAP_TARGET[i]) { + Log.w(TAG, "Wrong UUID"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + } + } catch (IOException e) { + Log.e(TAG, e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + Message msg = Message.obtain(mCallback); + msg.what = BluetoothPbapService.MSG_SESSION_ESTABLISHED; + msg.sendToTarget(); + + return ResponseCodes.OBEX_HTTP_OK; + } + + @Override + public void onDisconnect(final HeaderSet req, final HeaderSet resp) { + resp.responseCode = ResponseCodes.OBEX_HTTP_OK; + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothPbapService.MSG_SESSION_DISCONNECTED; + msg.sendToTarget(); + } + } + + @Override + public int onPut(final Operation op) { + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + @Override + public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, + final boolean create) { + String current_path_tmp = mCurrentPath; + String tmp_path = null; + try { + tmp_path = (String)request.getHeader(HeaderSet.NAME); + } catch (IOException e) { + Log.e(TAG, "Get name header fail"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (BluetoothPbapService.DBG) { + Log.d(TAG, "backup=" + backup + " create=" + create); + } + if (!backup && create) { + if (tmp_path == null) { + current_path_tmp = ""; + } else { + current_path_tmp = current_path_tmp + "/" + tmp_path; + } + } + + if (backup && create) { + if (current_path_tmp.length() != 0) { + current_path_tmp = current_path_tmp.substring(0, current_path_tmp.lastIndexOf("/")); + } + } + + if ((current_path_tmp.length() != 0) && (!isLegalPath(current_path_tmp))) { + Log.w(TAG, "path is not legal"); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + mCurrentPath = current_path_tmp; + return ResponseCodes.OBEX_HTTP_OK; + } + + @Override + public void onClose() { + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothPbapService.MSG_SERVERSESSION_CLOSE; + msg.sendToTarget(); + } + } + + @Override + public int onGet(final Operation op) { + HeaderSet request = null; + HeaderSet reply = new HeaderSet(); + String type = ""; + String name = ""; + byte[] appParam = null; + AppParamValue appParamValue = new AppParamValue(); + try { + request = op.getReceivedHeader(); + type = (String)request.getHeader(HeaderSet.TYPE); + name = (String)request.getHeader(HeaderSet.NAME); + appParam = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER); + } catch (IOException e) { + Log.e(TAG, "request headers error"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + // Accroding to specification,the name header could be omitted such as + // sony erriccsonHBH-DS980 + if (BluetoothPbapService.DBG) { + Log.d(TAG, "OnGet type is " + type + " name is " + name); + } + if (name == null) { + if (BluetoothPbapService.DBG) { + Log.i(TAG, "name =null,guess what carkit actually want from current path"); + } + if (mCurrentPath.compareTo(PB_PATH) == 0) { + appParamValue.needTag |= NEED_PHONEBOOK; + } + if (mCurrentPath.compareTo(ICH_PATH) == 0) { + appParamValue.needTag |= NEED_INCOMING_CALL_NUMBER; + } + if (mCurrentPath.compareTo(OCH_PATH) == 0) { + appParamValue.needTag |= NEED_OUTGOING_CALL_NUMBER; + } + if (mCurrentPath.compareTo(CCH_PATH) == 0) { + appParamValue.needTag |= NEED_COMBINED_CALL_NUMBER; + } + if (mCurrentPath.compareTo(MCH_PATH) == 0) { + appParamValue.needTag |= NEED_MISSED_CALL_NUMBER; + } + } else {// we have weak name checking here to provide better + // compatibility with other devices,although unique name such as + // "pb.vcf" is required by SIG spec. + if (name.contains(PB.subSequence(0, PB.length()))) { + appParamValue.needTag |= NEED_PHONEBOOK; + if (BluetoothPbapService.DBG) { + Log.v(TAG, "download phonebook request"); + } + } + if (name.contains(ICH.subSequence(0, ICH.length()))) { + appParamValue.needTag |= NEED_INCOMING_CALL_NUMBER; + if (BluetoothPbapService.DBG) { + Log.v(TAG, "download incoming calls request"); + } + } + if (name.contains(OCH.subSequence(0, OCH.length()))) { + appParamValue.needTag |= NEED_OUTGOING_CALL_NUMBER; + if (BluetoothPbapService.DBG) { + Log.v(TAG, "download outgoing calls request"); + } + } + if (name.contains(CCH.subSequence(0, CCH.length()))) { + appParamValue.needTag |= NEED_COMBINED_CALL_NUMBER; + if (BluetoothPbapService.DBG) { + Log.v(TAG, "download combined calls request"); + } + } + if (name.contains(MCH.subSequence(0, MCH.length()))) { + appParamValue.needTag |= NEED_MISSED_CALL_NUMBER; + if (BluetoothPbapService.DBG) { + Log.v(TAG, "download missed calls request"); + } + } + } + // listing request + if (type.equals(TYPE_LISTING)) { + return pullVcardListing(appParam, appParamValue, reply, op); + } + // pull vcard entry request + else if (type.equals(TYPE_VCARD)) { + return pullVcardEntry(appParam, appParamValue, op, name, mCurrentPath); + } + // down load phone book request + else if (type.equals(TYPE_PB)) { + return pullPhonebook(appParam, appParamValue, reply, op, name); + } else { + Log.w(TAG, "unknown type request!!!"); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + } // end of onGet() + + /** check whether path is legal */ + private final boolean isLegalPath(final String str) { + if (str.length() == 0) { + return true; + } + for (int i = 0; i < LEAGAL_PATH_NUM; i++) { + if (str.equals(LEAGEL_PATH[i])) { + return true; + } + } + return false; + } + + private class AppParamValue { + public int maxListCount; + + public int listStartOffset; + + public String searchValue; + + public String searchAttr; + + public int needTag; + + public boolean vcard21; + + public AppParamValue() { + maxListCount = 0; + listStartOffset = 0; + searchValue = ""; + searchAttr = ""; + needTag = 0x00; + vcard21 = true; + } + + public void dump() { + Log.i(TAG, "maxListCount=" + maxListCount + " listStartOffset=" + listStartOffset + + " searchValue=" + searchValue + " searchAttr=" + searchAttr + " needTag=" + + needTag + " vcard21=" + vcard21); + } + } + + /** To parse obex application parameter */ + private final boolean parseApplicationParameter(final byte[] appParam, + AppParamValue appParamValue) { + int i = 0; + boolean badRequest = true; + while (i < appParam.length) { + switch (appParam[i]) { + case ApplicationParameter.TRIPLET_TAGID.FILTER_TAGID: + i += 2; // length and tag field in triplet + i += ApplicationParameter.TRIPLET_LENGTH.FILTER_LENGTH; + break; + case ApplicationParameter.TRIPLET_TAGID.ORDER_TAGID: + i += 2; // length and tag field in triplet + i += ApplicationParameter.TRIPLET_LENGTH.ORDER_LENGTH; + break; + case ApplicationParameter.TRIPLET_TAGID.SEARCH_VALUE_TAGID: + i += 1; // length field in triplet + for (int k = 1; k <= appParam[i]; k++) { + appParamValue.searchValue += Byte.toString(appParam[i + k]); + } + // length of search value is variable + i += appParam[i]; + i += 1; + break; + case ApplicationParameter.TRIPLET_TAGID.SEARCH_ATTRIBUTE_TAGID: + i += 2; + appParamValue.searchAttr = Byte.toString(appParam[i]); + i += ApplicationParameter.TRIPLET_LENGTH.SEARCH_ATTRIBUTE_LENGTH; + break; + case ApplicationParameter.TRIPLET_TAGID.MAXLISTCOUNT_TAGID: + i += 2; + if (appParam[i] == 0 && appParam[i + 1] == 0) { + appParamValue.needTag |= NEED_PB_SIZE; + } else { + int highValue = appParam[i] & 0xff; + int lowValue = appParam[i + 1] & 0xff; + appParamValue.maxListCount = highValue * 256 + lowValue; + } + i += ApplicationParameter.TRIPLET_LENGTH.MAXLISTCOUNT_LENGTH; + break; + case ApplicationParameter.TRIPLET_TAGID.LISTSTARTOFFSET_TAGID: + i += 2; + if (appParam[i] == 0 && appParam[i + 1] == 0) { + appParamValue.listStartOffset = 0; + } else { + int highValue = appParam[i] & 0xff; + int lowValue = appParam[i + 1] & 0xff; + appParamValue.listStartOffset = highValue * 256 + lowValue; + } + i += ApplicationParameter.TRIPLET_LENGTH.LISTSTARTOFFSET_LENGTH; + break; + case ApplicationParameter.TRIPLET_TAGID.PHONEBOOKSIZE_TAGID: + i += 2;// length field in triplet + i += ApplicationParameter.TRIPLET_LENGTH.PHONEBOOKSIZE_LENGTH; + appParamValue.needTag |= NEED_PB_SIZE; + break; + case ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID: + i += 1; + appParamValue.needTag |= NEED_NEW_MISSED_CALL_NUMBER; + i += ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH; + break; + case ApplicationParameter.TRIPLET_TAGID.FORMAT_TAGID: + i += 2;// length field in triplet + if (Byte.toString(appParam[i]).compareTo("0") != 0) { + appParamValue.vcard21 = false; + } + i += ApplicationParameter.TRIPLET_LENGTH.FORMAT_LENGTH; + break; + default: + badRequest = false; + break; + } + } + return badRequest; + } + + /** Form and Send an XML format String to client for Phone book listing */ + private final int createVcardListingXml(final int type, final Operation op, + final int maxListCount, final int listStartOffset, final String searchValue, + String searchAttr) { + OutputStream out = null; + StringBuilder result = new StringBuilder(); + int itemsFound = 0; + result.append("<?xml version=\"1.0\"?>"); + result.append("<!DOCTYPE vcard-listing SYSTEM \"vcard-listing.dtd\">"); + result.append("<vCard-listing version=\"1.0\">"); + + // Phonebook listing request + if (type == NEED_PHONEBOOK) { + // if searchAttr is not set by client,make searchAttr by name + // as default; + if (searchAttr == null || searchAttr.trim().length() == 0) { + if (BluetoothPbapService.DBG) { + Log.i(TAG, "searchAttr is not set, assume search by name"); + } + searchAttr = "0"; + } + // begin of search by name + if (searchAttr.compareTo("0") == 0) { + ArrayList<String> nameList = BluetoothPbapService.getPhonebookNameList(); + int size = nameList.size() >= maxListCount ? maxListCount : nameList.size(); + if (BluetoothPbapService.DBG) { + Log.d(TAG, "search by name, size=" + size + " offset=" + listStartOffset + + " searchValue=" + searchValue); + } + // if searchValue if not set by client,provide the entire + // list by name + if (searchValue == null || searchValue.trim().length() == 0) { + for (int j = listStartOffset, i = 0; i < size; i++, j++) { + result.append("<card handle=\"" + j + ".vcf\" name=\"" + nameList.get(j) + + "\"" + "/>"); + itemsFound++; + } + } else { + for (int j = listStartOffset, i = 0; i < size; i++, j++) { + // only find the name which begins with the searchValue + if (nameList.get(j).indexOf(searchValue) == 0) { + itemsFound++; + result.append("<card handle=\"" + j + ".vcf\" name=\"" + + nameList.get(j) + "\"" + "/>"); + } + } + } + }// end of search by name + // begin of search by number + else if (searchAttr.compareTo("1") == 0) { + ArrayList<String> numberList = BluetoothPbapService.getPhonebookNumberList(); + int size = numberList.size() >= maxListCount ? maxListCount : numberList.size(); + if (BluetoothPbapService.DBG) { + Log.d(TAG, "search by number, size=" + size + " offset=" + listStartOffset + + " searchValue=" + searchValue); + } + // if searchValue if not set by client,provide the entire + // list by number + if (searchValue == null || searchValue.trim().length() == 0) { + for (int j = listStartOffset, i = 0; i < size; i++, j++) { + result.append("<card handle=\"" + j + ".vcf\" number=\"" + + numberList.get(j) + "\"" + "/>"); + itemsFound++; + } + } else { + for (int j = listStartOffset, i = 0; i < size; i++, j++) { + // only find the name which begins with the searchValue + if (numberList.get(j).indexOf(searchValue.trim()) == 0) { + itemsFound++; + result.append("<card handle=\"" + j + ".vcf\" number=\"" + + numberList.get(j) + "\"" + "/>"); + } + } + } + }// end of search by number + else { + return PRECONDITION_FAILED; + } + } + // Call history listing request + else { + ArrayList<String> nameList = BluetoothPbapService.getCallLogList(type); + int size = nameList.size() >= maxListCount ? maxListCount : nameList.size(); + if (BluetoothPbapService.DBG) { + Log.d(TAG, "call log list, size=" + size + " offset=" + listStartOffset); + } + // listing object begin with 1.vcf + for (int j = listStartOffset + 1, i = 0; i < size; i++, j++) { + result.append("<card handle=\"" + j + ".vcf\" name=\"" + nameList.get(j - 1) + "\"" + + "/>"); + itemsFound++; + } + } + result.append("</vCard-listing>"); + try { + out = op.openOutputStream(); + out.write(result.toString().getBytes()); + out.flush(); + } catch (IOException e) { + Log.e(TAG, "output stream fail " + e.toString()); + itemsFound = INTERNAL_ERROR; + } finally { + if (!closeStream(out, op)) { + itemsFound = INTERNAL_ERROR; + } + } + + if (BluetoothPbapService.DBG) { + Log.d(TAG, "itemsFound =" + itemsFound); + } + return itemsFound; + } + + /** + * Function to send obex header back to client such as get phonebook size + * request + */ + private final int pushHeader(final Operation op, final HeaderSet reply) { + OutputStream outputStream = null; + int ret = ResponseCodes.OBEX_HTTP_OK; + try { + op.sendHeaders(reply); + outputStream = op.openOutputStream(); + outputStream.flush(); + } catch (IOException e) { + Log.e(TAG, e.toString()); + ret = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } finally { + if (!closeStream(outputStream, op)) { + ret = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + return ret; + } + + /** Function to send vcard data to client */ + private final int pushBytes(final Operation op, final String string) { + if (string == null) { + return ResponseCodes.OBEX_HTTP_OK; + } + byte[] filebytes; + int ret = ResponseCodes.OBEX_HTTP_OK; + OutputStream outputStream = null; + if (BluetoothPbapService.DBG) { + Log.d(TAG, "Send Data"); + Log.d(TAG, string); + } + try { + outputStream = op.openOutputStream(); + filebytes = string.getBytes(); + outputStream.write(filebytes); + } catch (IOException e) { + Log.e(TAG, "open outputstrem failed" + e.toString()); + ret = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } finally { + if (!closeStream(outputStream, op)) { + ret = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + return ret; + } + + private final int pullVcardListing(byte[] appParam, AppParamValue appParamValue, + HeaderSet reply, final Operation op) { + String searchAttr = appParamValue.searchAttr.trim(); + if (!parseApplicationParameter(appParam, appParamValue)) { + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + if (BluetoothPbapService.DBG) { + appParamValue.dump(); + } + if (searchAttr == null || searchAttr.length() == 0) { + return ResponseCodes.OBEX_HTTP_PRECON_FAILED; + } + if (searchAttr.compareTo("0") != 0 && searchAttr.compareTo("1") != 0) { + Log.w(TAG, "search attr not supported"); + if (searchAttr.compareTo("2") == 0) { + // search by sound is not supported currently + Log.w(TAG, "do not support search by sound"); + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + return ResponseCodes.OBEX_HTTP_PRECON_FAILED; + } + + ApplicationParameter ap = new ApplicationParameter(); + if ((appParamValue.needTag & NEED_PB_SIZE) == NEED_PB_SIZE) { + int size = 0; + byte[] pbsize = new byte[2]; + if ((appParamValue.needTag & NEED_PHONEBOOK) == NEED_PHONEBOOK) { + size = BluetoothPbapService.getPhonebookSize(NEED_PHONEBOOK); + } else if ((appParamValue.needTag & NEED_INCOMING_CALL_NUMBER) == NEED_INCOMING_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_INCOMING_CALL_NUMBER); + } else if ((appParamValue.needTag & NEED_OUTGOING_CALL_NUMBER) == NEED_OUTGOING_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_OUTGOING_CALL_NUMBER); + } else if ((appParamValue.needTag & NEED_COMBINED_CALL_NUMBER) == NEED_COMBINED_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_COMBINED_CALL_NUMBER); + } else if ((appParamValue.needTag & NEED_MISSED_CALL_NUMBER) == NEED_MISSED_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_MISSED_CALL_NUMBER); + mMissedCallSize = size; + } + pbsize[0] = (byte)((size / 256) & 0xff);// HIGH VALUE + pbsize[1] = (byte)((size % 256) & 0xff);// LOW VALUE + ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PHONEBOOKSIZE_TAGID, + ApplicationParameter.TRIPLET_LENGTH.PHONEBOOKSIZE_LENGTH, pbsize); + reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); + return pushHeader(op, reply); + } + + if ((appParamValue.needTag & NEED_NEW_MISSED_CALL_NUMBER) == NEED_NEW_MISSED_CALL_NUMBER) { + byte[] misnum = new byte[1]; + int nmnum = BluetoothPbapService.getPhonebookSize(NEED_MISSED_CALL_NUMBER) + - mMissedCallSize; + nmnum = nmnum > 0 ? nmnum : 0; + misnum[0] = (byte)nmnum; + ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID, + ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum); + reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); + return pushHeader(op, reply); + } + + else if ((appParamValue.needTag & NEED_PB_SIZE) == 0 + && ((appParamValue.needTag & NEED_PHONEBOOK) == NEED_PHONEBOOK)) { + if (createVcardListingXml(NEED_PHONEBOOK, op, appParamValue.maxListCount, + appParamValue.listStartOffset, appParamValue.searchValue, + appParamValue.searchAttr) <= 0) { + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + return ResponseCodes.OBEX_HTTP_OK; + } + + else if ((appParamValue.needTag & NEED_PB_SIZE) == 0 + && ((appParamValue.needTag & NEED_INCOMING_CALL_NUMBER) == NEED_INCOMING_CALL_NUMBER)) { + if (createVcardListingXml(NEED_INCOMING_CALL_NUMBER, op, appParamValue.maxListCount, + appParamValue.listStartOffset, null, null) <= 0) { + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + return ResponseCodes.OBEX_HTTP_OK; + } + + else if ((appParamValue.needTag & NEED_PB_SIZE) == 0 + && ((appParamValue.needTag & NEED_OUTGOING_CALL_NUMBER) == NEED_OUTGOING_CALL_NUMBER)) { + if (createVcardListingXml(NEED_OUTGOING_CALL_NUMBER, op, appParamValue.maxListCount, + appParamValue.listStartOffset, null, null) <= 0) { + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + return ResponseCodes.OBEX_HTTP_OK; + } + + else if ((appParamValue.needTag & NEED_PB_SIZE) == 0 + && ((appParamValue.needTag & NEED_COMBINED_CALL_NUMBER) == NEED_COMBINED_CALL_NUMBER)) { + if (createVcardListingXml(NEED_COMBINED_CALL_NUMBER, op, appParamValue.maxListCount, + appParamValue.listStartOffset, null, null) <= 0) { + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + return ResponseCodes.OBEX_HTTP_OK; + } + + else if ((appParamValue.needTag & NEED_PB_SIZE) == 0 + && ((appParamValue.needTag & NEED_MISSED_CALL_NUMBER) == NEED_MISSED_CALL_NUMBER)) { + if (createVcardListingXml(NEED_MISSED_CALL_NUMBER, op, appParamValue.maxListCount, + appParamValue.listStartOffset, null, null) <= 0) { + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + return ResponseCodes.OBEX_HTTP_OK; + } + return ResponseCodes.OBEX_HTTP_OK; + } + + private final int pullVcardEntry(byte[] appParam, AppParamValue appParamValue, + final Operation op, final String name, final String current_path) { + boolean vcard21 = true; + if (!parseApplicationParameter(appParam, appParamValue)) + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + String str = ""; + String strIndex = ""; + int intIndex = 0; + if (name == null || name.length() < VCARD_NAME_MIN_LEN) { + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + strIndex = name.substring(0, name.length() - VCARD_NAME_MIN_LEN + 1); + if (strIndex.trim().length() != 0) { + try { + intIndex = Integer.parseInt(strIndex); + } catch (NumberFormatException e) { + Log.e(TAG, "catch number format exception " + e.toString()); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + } + if (current_path.compareTo(PB_PATH) == 0) { + if (intIndex < 0 || intIndex >= BluetoothPbapService.getPhonebookSize(NEED_PHONEBOOK)) + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + if (intIndex >= 0) + str = BluetoothPbapService.getPhonebook(NEED_PHONEBOOK, intIndex, vcard21); + } else if (current_path.compareTo(ICH_PATH) == 0) { + if (intIndex <= 0 + || intIndex > BluetoothPbapService.getPhonebookSize(NEED_INCOMING_CALL_NUMBER)) + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + if (intIndex >= 1) + str = BluetoothPbapService.getPhonebook(NEED_INCOMING_CALL_NUMBER, intIndex - 1, + vcard21); + } else if (current_path.compareTo(OCH_PATH) == 0) { + if (intIndex <= 0 + || intIndex > BluetoothPbapService.getPhonebookSize(NEED_OUTGOING_CALL_NUMBER)) + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + if (intIndex >= 1) + str = BluetoothPbapService.getPhonebook(NEED_OUTGOING_CALL_NUMBER, intIndex - 1, + vcard21); + } else if (current_path.compareTo(CCH_PATH) == 0) { + if (intIndex == 0) + return ResponseCodes.OBEX_HTTP_OK; + if (intIndex < 0 + || intIndex > BluetoothPbapService.getPhonebookSize(NEED_COMBINED_CALL_NUMBER)) + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + if (intIndex >= 1) + str = BluetoothPbapService.getPhonebook(NEED_COMBINED_CALL_NUMBER, intIndex - 1, + vcard21); + } else if (current_path.compareTo(MCH_PATH) == 0) { + if (intIndex <= 0 + || intIndex > BluetoothPbapService.getPhonebookSize(NEED_MISSED_CALL_NUMBER)) + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + if (intIndex >= 1) + str = BluetoothPbapService.getPhonebook(NEED_MISSED_CALL_NUMBER, intIndex - 1, + vcard21); + } else { + Log.w(TAG, "wrong path!"); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + return pushBytes(op, str); + } + + private final int pullPhonebook(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, + final Operation op, final String name) { + boolean vcard21 = true; + String str; + String strstr = ""; + int pbSize = 0; + int size = 0; + int pos = 0; + // code start for passing PTS3.2 TC_PSE_PBD_BI_01_C + if (name != null) { + int dotIndex = name.indexOf("."); + String vcf = "vcf"; + if (dotIndex >= 0 && dotIndex <= name.length()) { + if (name.regionMatches(dotIndex + 1, vcf, 0, vcf.length()) == false) { + Log.w(TAG, "name is not .vcf"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + } + } + // code end for passing PTS3.2 TC_PSE_PBD_BI_01_C + if (!parseApplicationParameter(appParam, appParamValue)) { + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + // check whether to send application response parameter to client + ApplicationParameter ap = new ApplicationParameter(); + if ((appParamValue.needTag & NEED_PB_SIZE) == NEED_PB_SIZE) { + byte[] pbsize = new byte[2]; + if ((appParamValue.needTag & NEED_PHONEBOOK) == NEED_PHONEBOOK) { + size = BluetoothPbapService.getPhonebookSize(NEED_PHONEBOOK); + } else if ((appParamValue.needTag & NEED_INCOMING_CALL_NUMBER) == NEED_INCOMING_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_INCOMING_CALL_NUMBER); + } else if ((appParamValue.needTag & NEED_OUTGOING_CALL_NUMBER) == NEED_OUTGOING_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_OUTGOING_CALL_NUMBER); + } else if ((appParamValue.needTag & NEED_COMBINED_CALL_NUMBER) == NEED_COMBINED_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_COMBINED_CALL_NUMBER); + } else if ((appParamValue.needTag & NEED_MISSED_CALL_NUMBER) == NEED_MISSED_CALL_NUMBER) { + size = BluetoothPbapService.getPhonebookSize(NEED_MISSED_CALL_NUMBER); + mMissedCallSize = size; + } + pbsize[0] = (byte)((size / 256) & 0xff);// HIGH VALUE + pbsize[1] = (byte)((size % 256) & 0xff);// LOW VALUE + ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PHONEBOOKSIZE_TAGID, + ApplicationParameter.TRIPLET_LENGTH.PHONEBOOKSIZE_LENGTH, pbsize); + reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); + return pushHeader(op, reply); + } + + if ((appParamValue.needTag & NEED_NEW_MISSED_CALL_NUMBER) == NEED_NEW_MISSED_CALL_NUMBER) { + byte[] misnum = new byte[1]; + int nmnum = BluetoothPbapService.getPhonebookSize(NEED_MISSED_CALL_NUMBER) + - mMissedCallSize; + nmnum = nmnum > 0 ? nmnum : 0; + misnum[0] = (byte)nmnum; + ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID, + ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum); + reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); + return pushHeader(op, reply); + } + + if ((appParamValue.needTag & NEED_PHONEBOOK) == NEED_PHONEBOOK) { + pbSize = BluetoothPbapService.getPhonebookSize(NEED_PHONEBOOK); + size = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; + for (pos = appParamValue.listStartOffset; pos < size; pos++) { + str = BluetoothPbapService.getPhonebook(NEED_PHONEBOOK, pos, vcard21); + strstr += str; + } + } + + if ((appParamValue.needTag & NEED_INCOMING_CALL_NUMBER) == NEED_INCOMING_CALL_NUMBER) { + pbSize = BluetoothPbapService.getPhonebookSize(NEED_INCOMING_CALL_NUMBER); + size = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; + for (pos = appParamValue.listStartOffset; pos < size; pos++) { + str = BluetoothPbapService.getPhonebook(NEED_INCOMING_CALL_NUMBER, pos, vcard21); + strstr += str; + } + } + + if ((appParamValue.needTag & NEED_OUTGOING_CALL_NUMBER) == NEED_OUTGOING_CALL_NUMBER) { + pbSize = BluetoothPbapService.getPhonebookSize(NEED_OUTGOING_CALL_NUMBER); + size = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; + for (pos = appParamValue.listStartOffset; pos < size; pos++) { + str = BluetoothPbapService.getPhonebook(NEED_OUTGOING_CALL_NUMBER, pos, vcard21); + strstr += str; + } + } + + if ((appParamValue.needTag & NEED_COMBINED_CALL_NUMBER) == NEED_COMBINED_CALL_NUMBER) { + pbSize = BluetoothPbapService.getPhonebookSize(NEED_COMBINED_CALL_NUMBER); + size = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; + for (pos = appParamValue.listStartOffset; pos < size; pos++) { + str = BluetoothPbapService.getPhonebook(NEED_COMBINED_CALL_NUMBER, pos, vcard21); + strstr += str; + } + } + + if ((appParamValue.needTag & NEED_MISSED_CALL_NUMBER) == NEED_MISSED_CALL_NUMBER) { + pbSize = BluetoothPbapService.getPhonebookSize(NEED_MISSED_CALL_NUMBER); + size = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; + for (pos = appParamValue.listStartOffset; pos < size; pos++) { + str = BluetoothPbapService.getPhonebook(NEED_MISSED_CALL_NUMBER, pos, vcard21); + strstr += str; + } + } + return pushBytes(op, strstr); + } + + private boolean closeStream(final OutputStream out, final Operation op) { + boolean returnvalue = true; + try { + if (out != null) { + out.close(); + } + } catch (IOException e) { + Log.e(TAG, "outputStream close failed" + e.toString()); + returnvalue = false; + } + try { + if (op != null) { + op.close(); + } + } catch (IOException e) { + Log.e(TAG, "oeration close failed" + e.toString()); + returnvalue = false; + } + return returnvalue; + } + + public final void setConnectionID(final long id) { + if ((id < -1) || (id > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Illegal Connection ID"); + } + mConnectionId = id; + } + + /** + * Retrieves the connection ID that is being used in the present connection. + * This method will return -1 if no connection ID is being used. + * + * @return the connection id being used or -1 if no connection ID is being + * used + */ + public final long getConnectionID() { + return mConnectionId; + } + + // Reserved for future use.In case PSE challenge PCE and PCE input wrong + // session key. + public final void onAuthenticationFailure(final byte[] userName) { + } +} diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapReceiver.java b/src/com/android/bluetooth/pbap/BluetoothPbapReceiver.java new file mode 100644 index 000000000..d693c155d --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapReceiver.java @@ -0,0 +1,111 @@ + +package com.android.bluetooth.pbap; + +import com.android.bluetooth.R; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.util.Log; + +public class BluetoothPbapReceiver extends BroadcastReceiver { + + private static final String TAG = "BluetoothPbapReceiver"; + + public static final int NOTIFICATION_ID_ACCESS = 1; + + public static final int NOTIFICATION_ID_AUTH = 2; + + static final Object mStartingServiceSync = new Object(); + + @Override + public void onReceive(Context context, Intent intent) { + Intent i = new Intent(); + i.putExtras(intent); + i.setClass(context, BluetoothPbapService.class); + String action = intent.getAction(); + i.putExtra("action", action); + if (action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION) + || action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION)) { + int state = intent.getIntExtra(BluetoothIntent.BLUETOOTH_STATE, BluetoothError.ERROR); + i.putExtra(BluetoothIntent.BLUETOOTH_STATE, state); + if ((state != BluetoothDevice.BLUETOOTH_STATE_TURNING_ON) + && (state != BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF)) { + beginStartingService(context, i); + } + } else { + beginStartingService(context, i); + } + } + + public static void makeNewPbapNotification(Context mContext, String action) { + + NotificationManager nm = (NotificationManager)mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + Notification notification = null; + Resources res = mContext.getResources(); + String name = BluetoothPbapService.getRemoteDeviceName(); + // Create an intent triggered by clicking on the status icon. + Intent clickIntent = new Intent(); + clickIntent.setClass(mContext, BluetoothPbapActivity.class); + clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + clickIntent.setAction(action); + + // Create an intent triggered by clicking on the + // "Clear All Notifications" button + Intent deleteIntent = new Intent(); + deleteIntent.setClass(mContext, BluetoothPbapReceiver.class); + + if (action.equals(BluetoothPbapService.ACCESS_REQUEST)) { + deleteIntent.setAction(BluetoothPbapService.ACCESS_DISALLOWED); + notification = new Notification(android.R.drawable.stat_sys_data_bluetooth, res + .getString(R.string.pbap_notif_ticker), System.currentTimeMillis()); + notification.setLatestEventInfo(mContext, res.getString(R.string.pbap_notif_title), res + .getString(R.string.pbap_notif_message, name), PendingIntent + .getActivity(mContext, 0, clickIntent, 0)); + + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.deleteIntent = PendingIntent.getBroadcast(mContext, 0, deleteIntent, 0); + nm.notify(NOTIFICATION_ID_ACCESS, notification); + } else if (action.equals(BluetoothPbapService.AUTH_CHALL)) { + deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED); + notification = new Notification(android.R.drawable.stat_sys_data_bluetooth, res + .getString(R.string.auth_notif_ticker), System.currentTimeMillis()); + notification.setLatestEventInfo(mContext, res.getString(R.string.auth_notif_title), res + .getString(R.string.auth_notif_message, name), PendingIntent + .getActivity(mContext, 0, clickIntent, 0)); + + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.deleteIntent = PendingIntent.getBroadcast(mContext, 0, deleteIntent, 0); + nm.notify(NOTIFICATION_ID_AUTH, notification); + } + } + + public static void removePbapNotification(Context mContext, int id) { + NotificationManager nm = (NotificationManager)mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(id); + } + + private static void beginStartingService(Context context, Intent intent) { + synchronized (mStartingServiceSync) { + context.startService(intent); + } + } + + public static void finishStartingService(Service service, int startId) { + synchronized (mStartingServiceSync) { + if (service.stopSelfResult(startId)) { + Log.i(TAG, "successfully stopped pbap service"); + } + } + } +} diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java b/src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java new file mode 100644 index 000000000..242f4b1c8 --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java @@ -0,0 +1,59 @@ + +package com.android.bluetooth.pbap; + +import android.bluetooth.BluetoothSocket; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.obex.ObexTransport; + +public class BluetoothPbapRfcommTransport implements ObexTransport { + private BluetoothSocket mSocket = null; + + public BluetoothPbapRfcommTransport(BluetoothSocket rfs) { + super(); + this.mSocket = rfs; + } + + public void close() throws IOException { + mSocket.close(); + } + + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + public InputStream openInputStream() throws IOException { + return mSocket.getInputStream(); + } + + public OutputStream openOutputStream() throws IOException { + return mSocket.getOutputStream(); + } + + public void connect() throws IOException { + } + + public void create() throws IOException { + } + + public void disconnect() throws IOException { + } + + public void listen() throws IOException { + } + + public boolean isConnected() throws IOException { + return true; + // TODO implement + } + +} diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapService.java b/src/com/android/bluetooth/pbap/BluetoothPbapService.java new file mode 100644 index 000000000..3ad61bbb9 --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapService.java @@ -0,0 +1,634 @@ + +package com.android.bluetooth.pbap; + +import com.android.bluetooth.R; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothIntent; +// TODO: have dependency on framework/base +//import android.bluetooth.BluetoothPbap; +import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothServerSocket; +// TODO: have dependency on framework/base +//import android.bluetooth.IBluetoothPbap; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.PowerManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.widget.Toast; + +import java.io.IOException; +import java.util.ArrayList; + +import javax.obex.ServerSession; + +public class BluetoothPbapService extends Service { + + private static final String TAG = "BluetoothPbapService"; + + public static final boolean DBG = true; + + /** + * Intent indicating incoming connection request which is sent to + * BluetoothPbapActivity + */ + public static final String ACCESS_REQUEST = "com.android.bluetooth.pbap.accessrequest"; + + /** + * Intent indicating incoming connection request accepted by user which is + * sent from BluetoothPbapActivity + */ + public static final String ACCESS_ALLOWED = "com.android.bluetooth.pbap.accessallowed"; + + /** + * Intent indicating incoming connection request denied by user which is + * sent from BluetoothPbapActivity + */ + public static final String ACCESS_DISALLOWED = "com.android.bluetooth.pbap.accessdisallowed"; + + /** + * Intent indicating incoming obex authentication request which is from + * PCE(Carkit) + */ + public static final String AUTH_CHALL = "com.android.bluetooth.pbap.authchall"; + + /** + * Intent indicating obex session key input complete by user which is sent + * from BluetoothPbapActivity + */ + public static final String AUTH_RESPONSE = "com.android.bluetooth.pbap.authresponse"; + + /** + * Intent indicating user canceled obex authentication session key input + * which is sent from BluetoothPbapActivity + */ + public static final String AUTH_CANCELLED = "com.android.bluetooth.pbap.authcancelled"; + + /** + * Intent indicating user confirmation timeout which is sent to + * BluetoothPbapActivity + */ + public static final String USER_CONFIRM_TIMEOUT = "com.android.bluetooth.pbap.userconfirmtimeout"; + + /** + * Intent Extra name indicating always allowed which is sent from + * BluetoothPbapActivity + */ + public static final String ALWAYS_ALLOWED = "com.android.bluetooth.pbap.alwaysallowed"; + + /** + * Intent Extra name indicating session key which is sent from + * BluetoothPbapActivity + */ + public static final String SESSION_KEY = "com.android.bluetooth.pbap.sessionkey"; + + public static final String THIS_PACKAGE_NAME = "com.android.bluetooth"; + + public static final int MSG_SERVERSESSION_CLOSE = 5000; + + public static final int MSG_SESSION_ESTABLISHED = 5001; + + public static final int MSG_SESSION_DISCONNECTED = 5002; + + public static final int MSG_OBEX_AUTH_CHALL = 5003; + + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + + private static final int START_LISTENER = 1; + + private static final int USER_TIMEOUT = 2; + + private static final int AUTH_TIMEOUT = 3; + + private PowerManager.WakeLock mWakeLock = null; + + private BluetoothDevice mBluetooth; + + private SocketAcceptThread mAcceptThread = null; + + private BluetoothPbapObexServer mPbapServer; + + private ServerSession mServerSession = null; + + private BluetoothServerSocket mServerSocket = null; + + private BluetoothSocket mConnSocket = null; + + private String mDeviceAddr = null; + + private static String sLocalPhoneNum = null; + + private static String sLocalPhoneName = null; + + private static String sRemoteDeviceName = null; + + private String mRemoteAddr = null; + + private boolean mHasStarted = false; + + private int mState; + + private int mStartId = -1; + + private static final int PORT_NUM = 19; + + private static final int USER_CONFIRM_TIMEOUT_VALUE = 30000; + + private static final int SOCKET_ACCEPT_TIMEOUT_VALUE = 5000; + + private static final int TIME_TO_WAIT_VALUE = 6000; + + private static BluetoothPbapVcardManager sVcardManager; + + private CharSequence mTmpTxt; + + private BluetoothPbapAuthenticator mAuth = null; + + public BluetoothPbapService() { + // TODO: have dependency on framework/base + // mState = BluetoothPbap.STATE_DISCONNECTED; + } + + @Override + public void onCreate() { + super.onCreate(); + mBluetooth = (BluetoothDevice)getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetooth != null) { + mDeviceAddr = mBluetooth.getAddress(); + } + sVcardManager = new BluetoothPbapVcardManager(BluetoothPbapService.this); + if (!mHasStarted && mDeviceAddr != null) { + mHasStarted = true; + Log.i(TAG, "Starting PBAP service"); + int state = mBluetooth.getBluetoothState(); + if (state == BluetoothDevice.BLUETOOTH_STATE_ON) { + mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler + .obtainMessage(START_LISTENER), TIME_TO_WAIT_VALUE); + } + } + } + + @Override + public void onStart(Intent intent, int startId) { + mStartId = startId; + if (mBluetooth == null || mDeviceAddr == null) { + Log.w(TAG, "Stopping BluetoothHeadsetService: " + + "device does not have BT or device is not ready"); + closeService(); // release all resources + } else { + parseIntent(intent); + } + } + + // process the intent from receiver + private void parseIntent(final Intent intent) { + String action = intent.getExtras().getString("action"); + int state = intent.getIntExtra(BluetoothIntent.BLUETOOTH_STATE, BluetoothError.ERROR); + if (action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION)) { + if (state == BluetoothDevice.BLUETOOTH_STATE_OFF) { + closeService(); // release all resources + } + } + + if (action.equals(ACCESS_ALLOWED)) { + mSessionStatusHandler.removeMessages(USER_TIMEOUT); + if (intent.getBooleanExtra(ALWAYS_ALLOWED, false)) { + // TODO: have dependency on device trust feature implementation + // mBluetooth.setTrust(mRemoteAddr, true); + } + try { + // In case carkit time out and try to use HFP for phonebook + // access ,while UI still there for user to comfirm + if (mConnSocket != null) { + startObexServerSession(); + } else { + obexServerSessionClose(); + } + } catch (IOException ex) { + Log.e(TAG, "Caught the error: " + ex.toString()); + } + } + + if (action.equals(ACCESS_DISALLOWED)) { + mSessionStatusHandler.removeMessages(USER_TIMEOUT); + obexServerSessionClose(); + } + + if (action.equals(AUTH_RESPONSE)) { + mSessionStatusHandler.removeMessages(USER_TIMEOUT); + String sessionkey = intent.getStringExtra(SESSION_KEY); + notifyAuthKeyInput(sessionkey); + } + + if (action.equals(AUTH_CANCELLED)) { + mSessionStatusHandler.removeMessages(USER_TIMEOUT); + notifyAuthCancelled(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + // TODO: have dependency on framework/base + // setState(BluetoothPbap.STATE_DISCONNECTED, + // BluetoothPbap.RESULT_CANCELED); + } + + @Override + public IBinder onBind(Intent intent) { + // TODO: have dependency on framework/base + // return mBinder; + return null; + } + + public static int getPhonebookSize(final int type) { + if (DBG) { + Log.d(TAG, "getPhonebookSzie type=" + type); + } + switch (type) { + case BluetoothPbapObexServer.NEED_PHONEBOOK: + return sVcardManager.getPhonebookSize(); + default: + return sVcardManager.getCallHistorySize(type); + } + } + + public static String getPhonebook(final int type, final int pos, final boolean vcardType) { + if (DBG) { + Log.d(TAG, "getPhonebook type=" + type + " pos=" + pos + " vcardType=" + vcardType); + } + switch (type) { + case BluetoothPbapObexServer.NEED_PHONEBOOK: + return sVcardManager.getPhonebook(pos, vcardType); + default: + return sVcardManager.getCallHistory(pos, type, vcardType); + } + } + + public static String getLocalPhoneNum() { + return sLocalPhoneNum; + } + + public static String getLocalPhoneName() { + return sLocalPhoneName; + } + + public static String getRemoteDeviceName() { + return sRemoteDeviceName; + } + + // Used for phone book listing by name + public static ArrayList<String> getPhonebookNameList() { + return sVcardManager.loadNameList(); + } + + // Used for phone book listing by number + public static ArrayList<String> getPhonebookNumberList() { + return sVcardManager.loadNumberList(); + } + + // Used for call history listing + public static ArrayList<String> getCallLogList(final int type) { + return sVcardManager.loadCallHistoryList(type); + } + + private void notifyAuthKeyInput(final String key) { + synchronized (mAuth) { + mAuth.setSessionKey(key); + mAuth.setChallenged(true); + mAuth.notify(); + } + } + + private void notifyAuthCancelled() { + synchronized (mAuth) { + mAuth.setCancelled(true); + mAuth.notify(); + } + } + + private final boolean initSocket() { + try { + // It is mandatory for PSE to support initiation of bonding and + // encryption. + mServerSocket = BluetoothServerSocket.listenUsingRfcommOn(PORT_NUM); + } catch (IOException ex) { + Log.e(TAG, "initSocket " + ex.toString()); + return false; + } + return true; + } + + private final void startObexServerSession() throws IOException { + // acquire the wakeLock before start Obex transaction thread + if (mWakeLock == null) { + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "StartingObexPbapTransaction"); + mWakeLock.setReferenceCounted(false); + mWakeLock.acquire(); + } + TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); + if (tm != null) { + sLocalPhoneNum = tm.getLine1Number(); + sLocalPhoneName = tm.getLine1AlphaTag(); + } + mPbapServer = new BluetoothPbapObexServer(mSessionStatusHandler); + mAuth = new BluetoothPbapAuthenticator(mSessionStatusHandler); + synchronized (mAuth) { + mAuth.setChallenged(false); + mAuth.setCancelled(false); + } + BluetoothPbapRfcommTransport transport = new BluetoothPbapRfcommTransport(mConnSocket); + mServerSession = new ServerSession(transport, mPbapServer, mAuth); + // TODO: have dependency on framework/base + // setState(BluetoothPbap.STATE_CONNECTED); + } + + private final void closeSocket(boolean server, boolean accept) throws IOException { + if (server == true) { + if (mServerSocket != null) { + mServerSocket.close(); + } + mServerSocket = null; + } + + if (accept == true) { + if (mConnSocket != null) { + mConnSocket.close(); + } + mConnSocket = null; + } + } + + private final void closeService() { + if (mAcceptThread != null) { + try { + mAcceptThread.shutdown(); + mAcceptThread.join(); + mAcceptThread = null; + } catch (InterruptedException ex) { + Log.w(TAG, "mAcceptThread close error" + ex); + } + } + if (mServerSession != null) { + mServerSession.close(); + mServerSession = null; + } + try { + closeSocket(true, true); + } catch (IOException ex) { + Log.e(TAG, "Caught the error: " + ex); + } + mHasStarted = false; + BluetoothPbapReceiver.finishStartingService(BluetoothPbapService.this, mStartId); + } + + private void obexServerSessionClose() { + // Release the wake lock if obex transaction is over + if (mWakeLock != null) { + mWakeLock.release(); + mWakeLock = null; + } + mServerSession = null; + mAcceptThread = null; + try { + closeSocket(false, true); + } catch (IOException e) { + Log.e(TAG, "Caught the error: " + e.toString()); + } + // Last obex transaction is finished,we start to listen for incoming + // connection again + if (mBluetooth.isEnabled()) { + startRfcommSocketListener(); + } + // TODO: have dependency on framework/base + // setState(BluetoothPbap.STATE_DISCONNECTED); + } + + /** + * A thread that runs in the background waiting for remote rfcomm + * connect.Once a remote socket connected, this thread shall be + * shutdown.When the remote disconnect,this thread shall run again waiting + * for next request. + */ + private class SocketAcceptThread extends Thread { + + private boolean stopped = false; + + private boolean trust; + + @Override + public void run() { + while (!stopped) { + try { + mConnSocket = mServerSocket.accept(SOCKET_ACCEPT_TIMEOUT_VALUE); + } catch (IOException ex) { + if (stopped) { + break; + } + if (DBG) { + Log.v(TAG, "Caught the error in socketAcceptThread: " + ex); + } + } + if (mConnSocket != null) { + mRemoteAddr = mConnSocket.getAddress(); + if (mRemoteAddr != null) { + sRemoteDeviceName = mBluetooth.getRemoteName(mRemoteAddr); + // In case getRemoteName failed and return null + if (sRemoteDeviceName == null) { + sRemoteDeviceName = getString(R.string.defaultname); + } + } + // TODO: have dependency on device trust feature + // implementation + // trust = mBluetooth.getTrustState(mRemoteAddr); + if (trust) { + try { + startObexServerSession(); + } catch (IOException ex) { + Log.e(TAG, "catch exception starting obex server session" + + ex.toString()); + } + } else { + BluetoothPbapReceiver.makeNewPbapNotification(getApplicationContext(), + ACCESS_REQUEST); + Log.i(TAG, "incomming connection accepted from" + sRemoteDeviceName); + mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler + .obtainMessage(USER_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE); + } + stopped = true; // job done ,close this thread; + } + } + } + + void shutdown() { + stopped = true; + interrupt(); + } + } + + private void startRfcommSocketListener() { + if (mServerSocket == null) { + if (!initSocket()) { + closeService(); + return; + } + } + if (mAcceptThread == null) { + mAcceptThread = new SocketAcceptThread(); + mAcceptThread.setName("BluetoothPbapAcceptThread"); + mAcceptThread.start(); + } + } + + private void setState(int state) { + // TODO: have dependency on framework/base + // setState(state, BluetoothHeadset.RESULT_SUCCESS); + } + + // TODO: have dependency on framework/base + /* + * private synchronized void setState(int state, int result) { if (state != + * mState) { if (DBG) Log.d(TAG, "Pbap state " + mState + " -> " + state + + * ", result = " + result); Intent intent = new + * Intent(BluetoothPbap.PBAP_STATE_CHANGED_ACTION); + * intent.putExtra(BluetoothPbap.PBAP_PREVIOUS_STATE, mState); mState = + * state; intent.putExtra(BluetoothPbap.PBAP_STATE, mState); + * intent.putExtra(BluetoothIntent.ADDRESS, mRemoteAddr); + * sendBroadcast(intent, BLUETOOTH_PERM); } } + */ + + /** + * Handlers for incoming service calls + */ + // TODO: have dependency on framework/base + /* + private final IBluetoothPbap.Stub mBinder = new IBluetoothPbap.Stub() { + private static final String TAG1 = "IBluetoothPbap.Stub"; + + public int getState() { + if (DBG) { + Log.d(TAG1, "getState " + mState); + } + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mState; + } + + public String getPceAddress() { + if (DBG) { + Log.d(TAG1, "getPceAddress" + mRemoteAddr); + } + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (mState == BluetoothPbap.STATE_DISCONNECTED) { + return null; + } + return mRemoteAddr; + } + + public boolean isConnected(String address) { + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mState == BluetoothPbap.STATE_CONNECTED && mRemoteAddr.equals(address); + } + + public boolean connectPce(String address) { + enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); + return false; + } + + public void disconnectPce() { + if (DBG) { + Log.d(TAG1, "disconnectPce"); + } + enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); + synchronized (BluetoothPbapService.this) { + switch (mState) { + case BluetoothPbap.STATE_CONNECTED: + if (mServerSession != null) { + mServerSession.close(); + mServerSession = null; + } + try { + closeSocket(false, true); + } catch (IOException ex) { + Log.e(TAG1, "Caught the error: " + ex); + } + setState(BluetoothPbap.STATE_DISCONNECTED, BluetoothPbap.RESULT_CANCELED); + break; + } + } + } + + // Not used + public boolean setPriority(String address, int priority) { + if (DBG) { + Log.d(TAG1, "setPriority"); + } + enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); + return true; + } + + // Not used + public int getPriority(String address) { + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return 0; + } + }; +*/ + private final Handler mSessionStatusHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case START_LISTENER: + if (mBluetooth.isEnabled()) { + startRfcommSocketListener(); + } else { + closeService();// release all resources + } + break; + case USER_TIMEOUT: + Intent intent = new Intent(USER_CONFIRM_TIMEOUT); + sendBroadcast(intent); + BluetoothPbapReceiver.removePbapNotification(getApplicationContext(), + BluetoothPbapReceiver.NOTIFICATION_ID_ACCESS); + obexServerSessionClose(); + break; + case AUTH_TIMEOUT: + Intent i = new Intent(USER_CONFIRM_TIMEOUT); + sendBroadcast(i); + BluetoothPbapReceiver.removePbapNotification(getApplicationContext(), + BluetoothPbapReceiver.NOTIFICATION_ID_AUTH); + notifyAuthCancelled(); + break; + case MSG_SERVERSESSION_CLOSE: + obexServerSessionClose(); + mTmpTxt = getString(R.string.toast_disconnected, sRemoteDeviceName); + Toast.makeText(BluetoothPbapService.this, mTmpTxt, Toast.LENGTH_SHORT).show(); + break; + case MSG_SESSION_ESTABLISHED: + mTmpTxt = getString(R.string.toast_connected, sRemoteDeviceName); + Toast.makeText(BluetoothPbapService.this, mTmpTxt, Toast.LENGTH_SHORT).show(); + break; + case MSG_SESSION_DISCONNECTED: + // case MSG_SERVERSESSION_CLOSE will handle ,so just skip + break; + case MSG_OBEX_AUTH_CHALL: + BluetoothPbapReceiver.makeNewPbapNotification(getApplicationContext(), + AUTH_CHALL); + mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler + .obtainMessage(AUTH_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE); + break; + } + } + }; + +} diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java new file mode 100644 index 000000000..27c118e9f --- /dev/null +++ b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java @@ -0,0 +1,511 @@ + +package com.android.bluetooth.pbap; + +import com.android.bluetooth.R; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.Contacts; +import android.provider.CallLog.Calls; +import android.provider.Contacts.Organizations; +import android.provider.CallLog; +import android.provider.Contacts.People; +import android.provider.Contacts.Phones; +import android.syncml.pim.vcard.ContactStruct; +import android.syncml.pim.vcard.VCardComposer; +import android.syncml.pim.vcard.VCardException; +import android.syncml.pim.vcard.VCardParser; +import android.util.Log; + +import java.util.ArrayList; + +public class BluetoothPbapVcardManager { + static private final String TAG = "BluetoothPbapVcardManager"; + + private ContentResolver mResolver; + + private Context mContext; + + private String mDefaultName = null; + + private String mDefaultNumber = null; + + /** The projection to use when querying the call log table */ + public static final String[] CALL_LOG_PROJECTION = new String[] { + Calls._ID, Calls.NUMBER, Calls.DATE, Calls.DURATION, Calls.TYPE, Calls.CACHED_NAME, + Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_LABEL + }; + + public static final int ID_COLUMN_INDEX = 0; + + public static final int NUMBER_COLUMN_INDEX = 1; + + public static final int DATE_COLUMN_INDEX = 2; + + public static final int DURATION_COLUMN_INDEX = 3; + + public static final int CALL_TYPE_COLUMN_INDEX = 4; + + public static final int CALLER_NAME_COLUMN_INDEX = 5; + + public static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 6; + + public static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 7; + + /** The projection to use when querying the phone book table */ + public static final String[] CONTACT_PROJECTION = new String[] { + People._ID, // 0 + People.NAME, // 1 + People.NOTES, // 2 + People.PRIMARY_PHONE_ID, // 3 + People.PRESENCE_STATUS, // 4 + People.STARRED, // 5 + People.CUSTOM_RINGTONE, // 6 + People.SEND_TO_VOICEMAIL, // 7 + People.PHONETIC_NAME, // 8 + }; + + public static final int CONTACT_ID_COLUMN = 0; + + public static final int CONTACT_NAME_COLUMN = 1; + + public static final int CONTACT_NOTES_COLUMN = 2; + + public static final int CONTACT_PREFERRED_PHONE_COLUMN = 3; + + public static final int CONTACT_SERVER_STATUS_COLUMN = 4; + + public static final int CONTACT_STARRED_COLUMN = 5; + + public static final int CONTACT_CUSTOM_RINGTONE_COLUMN = 6; + + public static final int CONTACT_SEND_TO_VOICEMAIL_COLUMN = 7; + + public static final int CONTACT_PHONETIC_NAME_COLUMN = 8; + + public static final String[] PHONES_PROJECTION = new String[] { + People.Phones._ID, // 0 + People.Phones.NUMBER, // 1 + People.Phones.TYPE, // 2 + People.Phones.LABEL, // 3 + People.Phones.ISPRIMARY, // 4 + }; + + public static final int PHONES_ID_COLUMN = 0; + + public static final int PHONES_NUMBER_COLUMN = 1; + + public static final int PHONES_TYPE_COLUMN = 2; + + public static final int PHONES_LABEL_COLUMN = 3; + + public static final int PHONES_ISPRIMARY_COLUMN = 4; + + public static final String[] METHODS_PROJECTION = new String[] { + People.ContactMethods._ID, // 0 + People.ContactMethods.KIND, // 1 + People.ContactMethods.DATA, // 2 + People.ContactMethods.TYPE, // 3 + People.ContactMethods.LABEL, // 4 + People.ContactMethods.ISPRIMARY, // 5 + People.ContactMethods.AUX_DATA, // 6 + }; + + public static final int METHODS_ID_COLUMN = 0; + + public static final int METHODS_KIND_COLUMN = 1; + + public static final int METHODS_DATA_COLUMN = 2; + + public static final int METHODS_TYPE_COLUMN = 3; + + public static final int METHODS_LABEL_COLUMN = 4; + + public static final int METHODS_ISPRIMARY_COLUMN = 5; + + public static final int METHODS_AUX_DATA_COLUMN = 6; + + public static final int METHODS_STATUS_COLUMN = 7; + + public static final String[] ORGANIZATIONS_PROJECTION = new String[] { + Organizations._ID, // 0 + Organizations.TYPE, // 1 + Organizations.LABEL, // 2 + Organizations.COMPANY, // 3 + Organizations.TITLE, // 4 + Organizations.ISPRIMARY, // 5 + }; + + public static final int ORGANIZATIONS_ID_COLUMN = 0; + + public static final int ORGANIZATIONS_TYPE_COLUMN = 1; + + public static final int ORGANIZATIONS_LABEL_COLUMN = 2; + + public static final int ORGANIZATIONS_COMPANY_COLUMN = 3; + + public static final int ORGANIZATIONS_TITLE_COLUMN = 4; + + public static final int ORGANIZATIONS_ISPRIMARY_COLUMN = 5; + + public BluetoothPbapVcardManager(final Context context) { + mContext = context; + mResolver = mContext.getContentResolver(); + mDefaultName = context.getString(android.R.string.unknownName); + mDefaultNumber = context.getString(R.string.defaultnumber); + } + + private String getThisPhoneName() { + String name = ""; + name = BluetoothPbapService.getLocalPhoneName(); + if (name == null || name.trim().length() == 0) { + name = mDefaultName; + } + return name; + } + + private String getThisPhoneNumber() { + String number = ""; + number = BluetoothPbapService.getLocalPhoneNum(); + if (number == null || number.trim().length() == 0) { + number = mDefaultNumber; + } + return number; + } + + public final int getPhonebookSize() { + Uri myUri = Contacts.People.CONTENT_URI; + int mPhonebookSize = 0; + Cursor contactC = mResolver.query(myUri, null, null, null, null); + if (contactC != null) { + mPhonebookSize = contactC.getCount() + 1; // always has the 0.vcf + } + return mPhonebookSize; + } + + public final String getPhonebook(final int pos, final boolean vCard21) { + int size = getPhonebookSize(); + long id = 0; + try { + if (pos >= 0 && pos < size) { + Uri myUri = Contacts.People.CONTENT_URI; + // Individual Contact may be deleted,but Uri in database is also + // removed.So we need to calculate the actual Uri + if (pos > 0) { + Cursor personCursor = mResolver.query(myUri, CONTACT_PROJECTION, null, null, + null); + if (personCursor != null) { + personCursor.moveToPosition(pos - 1); + id = personCursor.getLong(CONTACT_ID_COLUMN); + personCursor.close(); + } + String sPos = String.valueOf(id); + myUri = Uri.withAppendedPath(myUri, sPos); + } + return loadPhonebook(myUri, pos, vCard21); + } else { + Log.w(TAG, "pos invalid"); + } + } catch (Exception e) { + Log.e(TAG, "catch exception" + e.toString()); + } + return null; + } + + public final int getCallHistorySize(final int type) { + int size = 0; + Uri myUri = CallLog.Calls.CONTENT_URI; + String selection = null; + switch (type) { + case BluetoothPbapObexServer.NEED_INCOMING_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.INCOMING_TYPE; + break; + case BluetoothPbapObexServer.NEED_OUTGOING_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.OUTGOING_TYPE; + break; + case BluetoothPbapObexServer.NEED_MISSED_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.MISSED_TYPE; + break; + default: + break; + } + Cursor callCursor = mResolver.query(myUri, null, selection, null, + CallLog.Calls.DEFAULT_SORT_ORDER); + if (callCursor != null) { + size = callCursor.getCount(); + callCursor.close(); + } + return size; + } + + public final String getCallHistory(final int pos, final int type, final boolean vCard21) { + int size = 0; + String selection = null; + switch (type) { + case BluetoothPbapObexServer.NEED_INCOMING_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.INCOMING_TYPE; + break; + case BluetoothPbapObexServer.NEED_OUTGOING_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.OUTGOING_TYPE; + break; + case BluetoothPbapObexServer.NEED_MISSED_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.MISSED_TYPE; + break; + default: + break; + } + size = getCallHistorySize(type); + try { + if (pos >= 0 && pos < size) { + Uri myUri = CallLog.Calls.CONTENT_URI; + return loadCallHistory(myUri, pos, selection, vCard21); + } else { + Log.w(TAG, "pos invalid"); + } + } catch (Exception e) { + Log.e(TAG, "catch exception e" + e.toString()); + } + + return null; + } + + public final ArrayList<String> loadNameList() { + ArrayList<String> nameList = new ArrayList<String>(); + int size = getPhonebookSize(); + Uri myUri = Contacts.People.CONTENT_URI; + Cursor contactC = mResolver.query(myUri, null, null, null, null); + for (int pos = 0; pos < size; pos++) { + if (pos == 0) { + nameList.add(getThisPhoneName()); + } else { + if (contactC != null) { + contactC.moveToPosition(pos - 1); + String name = contactC.getString(contactC + .getColumnIndexOrThrow(Contacts.People.NAME)); + if (name == null || name.trim().length() == 0) { + name = mDefaultName; + } + nameList.add(name); + } + } + } + if (contactC != null) { + contactC.close(); + } + return nameList; + } + + public final ArrayList<String> loadNumberList() { + ArrayList<String> numberList = new ArrayList<String>(); + if (numberList.size() > 0) { + numberList.clear(); + } + int size = getPhonebookSize(); + Uri myUri = Contacts.People.CONTENT_URI; + Cursor contactC = mResolver.query(myUri, null, null, null, null); + for (int pos = 0; pos < size; pos++) { + if (pos == 0) { + numberList.add(getThisPhoneNumber()); + } else { + contactC.moveToPosition(pos - 1); + String number = contactC.getString(contactC + .getColumnIndexOrThrow(Contacts.People.PRIMARY_PHONE_ID)); + if (number == null || number.trim().length() == 0) { + number = mDefaultNumber; + } + numberList.add(number); + } + } + return numberList; + } + + public final ArrayList<String> loadCallHistoryList(final int type) { + int size = 0; + String selection = null; + Uri myUri = CallLog.Calls.CONTENT_URI; + ArrayList<String> list = new ArrayList<String>(); + switch (type) { + case BluetoothPbapObexServer.NEED_INCOMING_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.INCOMING_TYPE; + break; + case BluetoothPbapObexServer.NEED_OUTGOING_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.OUTGOING_TYPE; + break; + case BluetoothPbapObexServer.NEED_MISSED_CALL_NUMBER: + selection = Calls.TYPE + "=" + CallLog.Calls.MISSED_TYPE; + break; + default: + break; + } + size = getCallHistorySize(type); + Cursor callCursor = mResolver.query(myUri, CALL_LOG_PROJECTION, selection, null, + CallLog.Calls.DEFAULT_SORT_ORDER); + if (callCursor != null) { + for (int pos = 0; pos < size; pos++) { + callCursor.moveToPosition(pos); + String name = callCursor.getString(CALLER_NAME_COLUMN_INDEX); + if (name == null || name.trim().length() == 0) { + // name not found,use number instead + name = callCursor.getString(NUMBER_COLUMN_INDEX); + } + list.add(name); + } + callCursor.close(); + } + return list; + } + + private final String loadCallHistory(final Uri mUri, final int pos, final String selection, + final boolean vCard21) { + ContactStruct contactStruct = new ContactStruct(); + Cursor callCursor = mResolver.query(mUri, CALL_LOG_PROJECTION, selection, null, + CallLog.Calls.DEFAULT_SORT_ORDER); + if (callCursor != null) { + if (callCursor.moveToPosition(pos)) { + contactStruct.name = callCursor.getString(CALLER_NAME_COLUMN_INDEX); + if (contactStruct.name == null || contactStruct.name.trim().length() == 0) { + contactStruct.name = callCursor.getString(NUMBER_COLUMN_INDEX); + } + String number = callCursor.getString(NUMBER_COLUMN_INDEX); + int type = callCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + String label = callCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); + if (label == null || label.trim().length() == 0) { + label = Integer.toString(type); + } + contactStruct.addPhone(type, number, label, true); + } + callCursor.close(); + } + try { + VCardComposer composer = new VCardComposer(); + if (vCard21) { + return composer.createVCard(contactStruct, VCardParser.VERSION_VCARD21_INT); + } else { + return composer.createVCard(contactStruct, VCardParser.VERSION_VCARD30_INT); + } + } catch (VCardException e) { + Log.e(TAG, "catch exception" + e.toString()); + return null; + } + } + + private final String loadPhonebook(final Uri mUri, final int pos, final boolean vCard21) { + // Build up the phone entries + ContactStruct contactStruct = new ContactStruct(); + Cursor personCursor = mResolver.query(mUri, CONTACT_PROJECTION, null, null, null); + if (personCursor == null) { + return null; + } + if (pos == 0) { + contactStruct.name = getThisPhoneName(); + contactStruct.addPhone(Contacts.PhonesColumns.TYPE_MOBILE, getThisPhoneNumber(), "", + true); + } else if (personCursor.moveToNext()) { + contactStruct.name = personCursor.getString(CONTACT_NAME_COLUMN); + if (contactStruct.name == null || contactStruct.name.trim().length() == 0) { + contactStruct.name = mDefaultName; + } + contactStruct.notes.add(checkStrEnd(personCursor.getString(CONTACT_NOTES_COLUMN), + vCard21)); + final Uri phonesUri = Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY); + final Cursor phonesCursor = mResolver.query(phonesUri, PHONES_PROJECTION, null, null, + Phones.ISPRIMARY + " DESC"); + if (phonesCursor != null) { + while (phonesCursor.moveToNext()) { + int type = phonesCursor.getInt(PHONES_TYPE_COLUMN); + String number = phonesCursor.getString(PHONES_NUMBER_COLUMN); + String label = phonesCursor.getString(PHONES_LABEL_COLUMN); + contactStruct.addPhone(type, number, label, true); + } + phonesCursor.close(); + } + // Build the contact method entries + final Uri methodsUri = Uri.withAppendedPath(mUri, + People.ContactMethods.CONTENT_DIRECTORY); + Cursor methodsCursor = mResolver + .query(methodsUri, METHODS_PROJECTION, null, null, null); + if (methodsCursor != null) { + while (methodsCursor.moveToNext()) { + int kind = methodsCursor.getInt(METHODS_KIND_COLUMN); + String label = methodsCursor.getString(METHODS_LABEL_COLUMN); + String data = methodsCursor.getString(METHODS_DATA_COLUMN); + int type = methodsCursor.getInt(METHODS_TYPE_COLUMN); + boolean isPrimary = methodsCursor.getInt(METHODS_ISPRIMARY_COLUMN) == 1 ? true + : false; + // TODO + // Below code is totally depend on the implementation of + // package android.syncml.pim.vcard + // VcardComposer shall throw null pointer exception when + // label is not set + // function appendContactMethodStr is weak and shall be + // improved in the future + if (kind == Contacts.KIND_EMAIL) { + if (type == Contacts.ContactMethodsColumns.TYPE_OTHER) { + if (label == null || label.trim().length() == 0) { + label = Integer.toString(type); + } + } + } + if (kind == Contacts.KIND_POSTAL) { + data = checkStrEnd(data, vCard21); + } + contactStruct.addContactmethod(kind, type, data, label, isPrimary); + } + methodsCursor.close(); + } + + // Build the organization entries + final Uri organizationsUri = Uri + .withAppendedPath(mUri, Organizations.CONTENT_DIRECTORY); + Cursor organizationsCursor = mResolver.query(organizationsUri, + ORGANIZATIONS_PROJECTION, "isprimary", null, null); + + if (organizationsCursor != null) { + while (organizationsCursor.moveToNext()) { + int type = organizationsCursor.getInt(ORGANIZATIONS_TYPE_COLUMN); + String company = organizationsCursor.getString(ORGANIZATIONS_COMPANY_COLUMN); + String title = checkStrEnd(organizationsCursor + .getString(ORGANIZATIONS_TITLE_COLUMN), vCard21); + boolean isPrimary = organizationsCursor.getInt(ORGANIZATIONS_ISPRIMARY_COLUMN) == 1 ? true + : false; + contactStruct.addOrganization(type, company, title, isPrimary); + } + organizationsCursor.close(); + } + } + personCursor.close(); + // Generate vCard data. + try { + VCardComposer composer = new VCardComposer(); + if (vCard21) { + return composer.createVCard(contactStruct, VCardParser.VERSION_VCARD21_INT); + } else { + return composer.createVCard(contactStruct, VCardParser.VERSION_VCARD30_INT); + } + } catch (VCardException e) { + Log.e(TAG, "catch exception in loadPhonebook" + e.toString()); + return null; + } + } + + /** + * This function is to check the string end to avoid function + * foldingString's returning null in package android.syncml.pim.vcard + */ + private final String checkStrEnd(String str, final boolean vCard21) { + if (str == null) { + return str; + } + if (str.charAt(str.length() - 1) != '\n') { + if (vCard21) { + str += "\r\n"; + } else { + str += "\n"; + } + } + return str; + } + +} |