From 3e0794418cfed9f2bc02a3f8644bb183b5999037 Mon Sep 17 00:00:00 2001 From: Ashwini Munigala Date: Wed, 12 Jun 2013 17:30:04 +0530 Subject: Bluetooth: Support OBEX FTP profile on Bluedroid. Porting changes for FTP on Bluedroid as a part of BluetoothExt APK. Support FTP 1.1 version features. Handle FTP Authorization from BluetoothExt APK instead from AOSP Settings proj. CRs-fixed: 504049 Change-Id: Iec2ee10b47599db8fa7f2b8e4da36dee25c577c9 --- .../bluetooth/ftp/BluetoothFtpActivity.java | 386 +++++++ .../bluetooth/ftp/BluetoothFtpAuthenticator.java | 103 ++ .../bluetooth/ftp/BluetoothFtpObexServer.java | 1096 ++++++++++++++++++++ .../bluetooth/ftp/BluetoothFtpReceiver.java | 82 ++ .../bluetooth/ftp/BluetoothFtpRfcommTransport.java | 86 ++ .../bluetooth/ftp/BluetoothFtpService.java | 909 ++++++++++++++++ .../bluetooth/ftp/BluetoothFtpTransport.java | 112 ++ src/org/codeaurora/bluetooth/ftp/FileUtils.java | 322 ++++++ 8 files changed, 3096 insertions(+) create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpActivity.java create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpAuthenticator.java create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpObexServer.java create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpReceiver.java create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpRfcommTransport.java create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpService.java create mode 100644 src/org/codeaurora/bluetooth/ftp/BluetoothFtpTransport.java create mode 100644 src/org/codeaurora/bluetooth/ftp/FileUtils.java (limited to 'src/org/codeaurora/bluetooth/ftp') diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpActivity.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpActivity.java new file mode 100644 index 0000000..a4ad2f8 --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpActivity.java @@ -0,0 +1,386 @@ + /* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codeaurora.bluetooth.ftp; + +import org.codeaurora.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.os.Handler; +import android.os.Message; +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; + +/** + * FtpActivity shows two dialogues: One for accepting incoming ftp request and + * the other prompts the user to enter a session key for authentication with a + * remote Bluetooth device. + */ +public class BluetoothFtpActivity extends AlertActivity implements + DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener, TextWatcher { + private static final String TAG = "BluetoothFtpActivity"; + + private static final boolean V = BluetoothFtpService.VERBOSE; + + 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 static final String KEY_USER_TIMEOUT = "user_timeout"; + + private View mView; + + private EditText mKeyView; + + private TextView messageView; + + private String mSessionKey = ""; + + private int mCurrentDialog; + + private Button mOkButton; + + private CheckBox mAlwaysAllowed; + + private boolean mTimeout = false; + + private boolean mAlwaysAllowedValue = false; + + private static final int DISMISS_TIMEOUT_DIALOG = 0; + + private static final int DISMISS_TIMEOUT_DIALOG_VALUE = 2000; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!BluetoothFtpService.USER_CONFIRM_TIMEOUT_ACTION.equals(intent.getAction())) { + return; + } + onTimeout(); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent i = getIntent(); + String action = i.getAction(); + if(V) Log.v(TAG,"onCreate action = "+ action); + if (action.equals(BluetoothFtpService.ACCESS_REQUEST_ACTION )) { + showFtpDialog(DIALOG_YES_NO_CONNECT); + mCurrentDialog = DIALOG_YES_NO_CONNECT; + } else if (action.equals(BluetoothFtpService.AUTH_CHALL_ACTION)){ + showFtpDialog(DIALOG_YES_NO_AUTH); + mCurrentDialog = DIALOG_YES_NO_AUTH; + } else { + Log.e(TAG, "Error: this activity may be started only with intent " + + "FTP_ACCESS_REQUEST or FTP_AUTH_CHALL "); + finish(); + } + Log.i(TAG,"onCreate"); + registerReceiver(mReceiver, new IntentFilter( + BluetoothFtpService.USER_CONFIRM_TIMEOUT_ACTION)); + } + /* + * Creates a Button with Yes/No dialog + */ + private void showFtpDialog(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.bluetooth_ftp_request); + 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: + if(V) Log.v(TAG,"showFtpDialog DIALOG_YES_NO_AUTH"); + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.ftp_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); + if (mOkButton != null) { + mOkButton.setEnabled(false); + } else { + Log.e(TAG, "Error! mOkButton is null"); + } + break; + default: + break; + } + } + /* + * Creates a Text window for the FTP acceptance or session key dialog + */ + private String createDisplayText(final int id) { + String mRemoteName = BluetoothFtpService.getRemoteDeviceName(); + if(V) Log.v(TAG,"createDisplayText" + id); + switch (id) { + case DIALOG_YES_NO_CONNECT: + String mMessage1 = getString(R.string.bluetooth_ftp_acceptance_dialog_text, mRemoteName, + mRemoteName); + return mMessage1; + case DIALOG_YES_NO_AUTH: + String mMessage2 = getString(R.string.ftp_session_key_dialog_title, mRemoteName); + return mMessage2; + default: + Log.e(TAG,"Display Text id ("+ id + ")not part of FTP resource"); + return null; + } + } + /* + * Creates a view for the dialog and text to get the user inputs + */ + private View createView(final int id) { + if(V) Log.v(TAG,"createView" + id); + switch (id) { + case DIALOG_YES_NO_CONNECT: + mView = getLayoutInflater().inflate(R.layout.bluetooth_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; + } else { + mAlwaysAllowedValue = false; + } + } + }); + 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: + Log.e(TAG,"Create view id ("+ id + ")not part of FTP resource"); + return null; + } + } + + private void onPositive() { + if(V) Log.v(TAG,"onPositive mtimeout = " + mTimeout + "mCurrentDialog = " + mCurrentDialog); + if (!mTimeout) { + if (mCurrentDialog == DIALOG_YES_NO_CONNECT) { + sendIntentToReceiver(BluetoothFtpService.ACCESS_ALLOWED_ACTION, + BluetoothFtpService.EXTRA_ALWAYS_ALLOWED, mAlwaysAllowedValue); + } else if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + sendIntentToReceiver(BluetoothFtpService.AUTH_RESPONSE_ACTION, + BluetoothFtpService.EXTRA_SESSION_KEY, mSessionKey); + mKeyView.removeTextChangedListener(this); + } + } + mTimeout = false; + finish(); + } + + private void onNegative() { + if(V) Log.v(TAG,"onNegative mtimeout = " + mTimeout + "mCurrentDialog = " + mCurrentDialog); + if (mCurrentDialog == DIALOG_YES_NO_CONNECT) { + sendIntentToReceiver(BluetoothFtpService.ACCESS_DISALLOWED_ACTION, + BluetoothFtpService.EXTRA_ALWAYS_ALLOWED, mAlwaysAllowedValue); + } else if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + sendIntentToReceiver(BluetoothFtpService.AUTH_CANCELLED_ACTION, null, null); + mKeyView.removeTextChangedListener(this); + } + finish(); + } + /* + * Sends an intent to the BluetoothFtpService class with a String parameter + * @param intentName the name of the intent to be broadcasted + * @param extraName the name of the extra parameter broadcasted + * @param extraValue the extra name parameter broadcasted + */ + private void sendIntentToReceiver(final String intentName, final String extraName, + final String extraValue) { + Intent intent = new Intent(intentName); + intent.setClassName(BluetoothFtpService.THIS_PACKAGE_NAME, BluetoothFtpReceiver.class + .getName()); + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + sendBroadcast(intent); + } + /* + * Sends an intent to the BluetoothFtpService class with a integer parameter + * @param intentName the name of the intent to be broadcasted + * @param extraName the name of the extra parameter broadcasted + * @param extraValue the extra name parameter broadcasted + + */ + private void sendIntentToReceiver(final String intentName, final String extraName, + final boolean extraValue) { + Intent intent = new Intent(intentName); + intent.setClassName(BluetoothFtpService.THIS_PACKAGE_NAME, BluetoothFtpReceiver.class + .getName()); + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + sendBroadcast(intent); + } + + public void onClick(DialogInterface dialog, int which) { + if(V) Log.v(TAG,"onClick which = " + 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; + default: + break; + } + } + + private void onTimeout() { + mTimeout = true; + Button mButton = mAlert.getButton(DialogInterface.BUTTON_NEGATIVE); + if(V) Log.v(TAG,"onTimeout mCurrentDialog = " + mCurrentDialog); + if (mCurrentDialog == DIALOG_YES_NO_CONNECT) { + messageView.setText(getString(R.string.ftp_acceptance_timeout_message, + BluetoothFtpService.getRemoteDeviceName())); + mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE); + mAlwaysAllowed.setVisibility(View.GONE); + mAlwaysAllowed.clearFocus(); + } else if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + /* Proceed to clear the view only if one created */ + if(mView != null) { + messageView.setText(getString(R.string.ftp_authentication_timeout_message, + BluetoothFtpService.getRemoteDeviceName())); + mKeyView.setVisibility(View.GONE); + mKeyView.clearFocus(); + mKeyView.removeTextChangedListener(this); + mOkButton.setEnabled(true); + if (mButton != null) { + mButton.setVisibility(View.GONE); + } else { + Log.e(TAG, "Error! mButton is null, can't setVisibility"); + } + } + } + + mTimeoutHandler.sendMessageDelayed(mTimeoutHandler.obtainMessage(DISMISS_TIMEOUT_DIALOG), + DISMISS_TIMEOUT_DIALOG_VALUE); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mTimeout = savedInstanceState.getBoolean(KEY_USER_TIMEOUT); + if (V) Log.v(TAG, "onRestoreInstanceState() mTimeout: " + mTimeout); + if (mTimeout) { + onTimeout(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_USER_TIMEOUT, mTimeout); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(mReceiver); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + return true; + } + + public void beforeTextChanged(CharSequence s, int start, int before, int after) { + } + + 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); + } + } + + private final Handler mTimeoutHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DISMISS_TIMEOUT_DIALOG: + if (V) Log.v(TAG, "Received DISMISS_TIMEOUT_DIALOG msg"); + finish(); + break; + default: + break; + } + } + }; +} diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpAuthenticator.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpAuthenticator.java new file mode 100644 index 0000000..8e9cc60 --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpAuthenticator.java @@ -0,0 +1,103 @@ + /* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package org.codeaurora.bluetooth.ftp; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import javax.obex.Authenticator; +import javax.obex.PasswordAuthentication; + +/** + * BluetoothFtpAuthenticator is a used by BluetoothObexServer for obex + * authentication procedure. + */ +public class BluetoothFtpAuthenticator implements Authenticator { + private static final String TAG = "BluetoothFtpAuthenticator"; + + private boolean mChallenged; + + private boolean mAuthCancelled; + + private String mSessionKey; + + private Handler mCallback; + + public BluetoothFtpAuthenticator(final Handler callback) { + mCallback = callback; + mChallenged = false; + mAuthCancelled = false; + mSessionKey = null; + } + + public final synchronized void setChallenged(final boolean bool) { + mChallenged = bool; + } + + public final synchronized void setCancelled(final boolean bool) { + mAuthCancelled = bool; + } + + public final synchronized void setSessionKey(final String string) { + mSessionKey = string; + } + + private void waitUserConfirmation() { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothFtpService.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) { + return null; + } +} diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpObexServer.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpObexServer.java new file mode 100644 index 0000000..f4e561b --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpObexServer.java @@ -0,0 +1,1096 @@ +/* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010-2012 The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codeaurora.bluetooth.ftp; + +import android.content.Context; +import android.os.Message; +import android.os.Handler; +import android.os.StatFs; +import android.text.TextUtils; +import android.util.Log; +import android.os.Bundle; +import android.webkit.MimeTypeMap; + + +import java.io.IOException; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.util.Date; +import java.text.SimpleDateFormat; +import java.util.List; +import java.lang.StringBuffer; + +import javax.obex.ServerRequestHandler; +import javax.obex.ResponseCodes; +import javax.obex.ApplicationParameter; +import javax.obex.ServerOperation; +import javax.obex.Operation; +import javax.obex.HeaderSet; +import javax.obex.ObexHelper; + +public class BluetoothFtpObexServer extends ServerRequestHandler { + + private static final String TAG = "BluetoothFtpObexServer"; + + private static final boolean D = BluetoothFtpService.DEBUG; + + private static final boolean V = BluetoothFtpService.VERBOSE; + + private static final int UUID_LENGTH = 16; + + /* To help parsing file attributes */ + private static final int INDEX_YEAR = 0; + + private static final int INDEX_MONTH = 1; + + private static final int INDEX_DATE = 2; + + private static final int INDEX_TIME = 3; + + private static final int INDEX_TIME_HOUR = 0; + + private static final int INDEX_TIME_MINUTE = 1; + + // type for list folder contents + private static final String TYPE_LISTING = "x-obex/folder-listing"; + + // record current path the client are browsing + private String mCurrentPath = ""; + + private long mConnectionId; + + private Handler mCallback = null; + + private Context mContext; + + public static boolean sIsAborted = false; + + public static final String ROOT_FOLDER_PATH = "/sdcard"; + + private static final String FOLDER_NAME_DOT = "."; + + private static final String FOLDER_NAME_DOTDOT = ".."; + + List filenames; + + List types; + + // 128 bit UUID for FTP + private static final byte[] FTP_TARGET = new byte[] { + (byte)0xF9, (byte)0xEC, (byte)0x7B, (byte)0xC4, (byte)0x95, + (byte)0x3c, (byte)0x11, (byte)0xD2, (byte)0x98, (byte)0x4E, + (byte)0x52, (byte)0x54, (byte)0x00, (byte)0xDc, (byte)0x9E, + (byte)0x09 + }; + + private static final String[] LEGAL_PATH = {"/sdcard"}; + + public BluetoothFtpObexServer(Handler callback, Context context) { + super(); + mConnectionId = -1; + mCallback = callback; + mContext = context; + // set initial value when ObexServer created + if (D) Log.d(TAG, "Initialize FtpObexServer"); + filenames = new ArrayList(); + types = new ArrayList(); + } + /** + * onConnect + * + * Called when a CONNECT request is received. + * + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + @Override + public int onConnect(final HeaderSet request, HeaderSet reply) { + if (D) Log.d(TAG, "onConnect()+"); + /* Extract the Target header */ + try { + byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET); + if (uuid == null) { + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + if (D) Log.d(TAG, "onConnect(): uuid=" + Arrays.toString(uuid)); + + if (uuid.length != UUID_LENGTH) { + Log.w(TAG, "Wrong UUID length"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + /* Compare the Uuid from target with FTP service uuid */ + for (int i = 0; i < UUID_LENGTH; i++) { + if (uuid[i] != FTP_TARGET[i]) { + Log.w(TAG, "Wrong UUID"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + } + /* Add the uuid into the WHO header part of connect reply */ + reply.setHeader(HeaderSet.WHO, uuid); + } catch (IOException e) { + Log.e(TAG,"onConnect "+ e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + /* Extract the remote WHO header and fill it in the Target header*/ + try { + byte[] remote = (byte[])request.getHeader(HeaderSet.WHO); + if (remote != null) { + if (D) Log.d(TAG, "onConnect(): remote=" + + Arrays.toString(remote)); + reply.setHeader(HeaderSet.TARGET, remote); + } + } catch (IOException e) { + Log.e(TAG,"onConnect "+ e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + if (V) Log.v(TAG, "onConnect(): uuid is ok, will send out " + + "MSG_SESSION_ESTABLISHED msg."); + /* Notify the FTP service about the session establishment */ + Message msg = Message.obtain(mCallback); + msg.what = BluetoothFtpService.MSG_SESSION_ESTABLISHED; + msg.sendToTarget(); + /* Initialise the mCurrentPath to ROOT path = /sdcard */ + mCurrentPath = ROOT_FOLDER_PATH; + if (D) Log.d(TAG, "onConnect() -"); + return ResponseCodes.OBEX_HTTP_OK; + } + /** + * onDisconnect + * + * Called when a DISCONNECT request is received. + * + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + */ + @Override + public void onDisconnect(final HeaderSet req, final HeaderSet resp) { + if (D) Log.d(TAG, "onDisconnect() +"); + + resp.responseCode = ResponseCodes.OBEX_HTTP_OK; + /* Send a message to the FTP service to close the Server session */ + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothFtpService.MSG_SESSION_DISCONNECTED; + msg.sendToTarget(); + if (V) Log.v(TAG, + "onDisconnect(): msg MSG_SESSION_DISCONNECTED sent out."); + } + if (D) Log.d(TAG, "onDisconnect() -"); + } + /** + * Called when a ABORT request is received. + */ + @Override + public int onAbort(HeaderSet request, HeaderSet reply) { + if (D) Log.d(TAG, "onAbort() +"); + sIsAborted = true; + if (D) Log.d(TAG, "onAbort() -"); + return ResponseCodes.OBEX_HTTP_OK; + } + + /** + * onDelete + * + * Called when a DELETE request is received. + * + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + @Override + public int onDelete(HeaderSet request, HeaderSet reply) { + if (D) Log.d(TAG, "onDelete() +"); + String name = ""; + /* Check if Card is mounted */ + if(FileUtils.checkMountedState() == false) { + Log.e(TAG,"SD card not Mounted"); + return ResponseCodes.OBEX_HTTP_NO_CONTENT; + } + /* 1. Extract the name header + * 2. Check if the file exists by appending the name to current path + * 3. Check if its read only + * 4. Check if the directory is read only + * 5. If 2 satisfies and 3 ,4 are not true proceed to delete the file + */ + try{ + name = (String)request.getHeader(HeaderSet.NAME); + /* Not allowed to delete a folder name with "." and ".." */ + if (TextUtils.equals(name, FOLDER_NAME_DOT) || + TextUtils.equals(name, FOLDER_NAME_DOTDOT) ) { + if(D) Log.d(TAG, "cannot delete the directory " + name); + return ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + } + if (D) Log.d(TAG,"OnDelete File = " + name + + "mCurrentPath = " + mCurrentPath ); + File deleteFile = new File(mCurrentPath + "/" + name); + if(deleteFile.exists() == true){ + if (D) Log.d(TAG, "onDelete(): Found File" + name + "in folder " + + mCurrentPath); + if(!deleteFile.canWrite()) { + return ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + } + + if(deleteFile.isDirectory()) { + if(!FileUtils.deleteDirectory(mCallback,deleteFile)) { + if (D) Log.d(TAG,"Directory delete unsuccessful"); + return ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + } + } else { + if(!deleteFile.delete()){ + if (D) Log.d(TAG,"File delete unsuccessful"); + return ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + } + FileUtils.sendMessage(mCallback,BluetoothFtpService.MSG_FILE_DELETED, + deleteFile.getAbsolutePath()); + } + } + else{ + if (D) Log.d(TAG,"File doesnot exist"); + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + }catch (IOException e) { + Log.e(TAG,"onDelete "+ e.toString()); + if (D) Log.d(TAG, "Delete operation failed"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (D) Log.d(TAG, "onDelete() -"); + return ResponseCodes.OBEX_HTTP_OK; + } + /** + * onPut + * + * Called when a PUT request is received. + * + * If an ABORT request is received during the processing of a PUT request, + * op will be closed by the implementation. + * @param operation contains the headers sent by the client and allows new + * headers to be sent in the reply; op will never be + * null + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + @Override + public int onPut(final Operation op) { + if (D) Log.d(TAG, "onPut() +"); + HeaderSet request = null; + long length; + String name = ""; + String filetype = ""; + int obexResponse = ResponseCodes.OBEX_HTTP_OK; + + if(FileUtils.checkMountedState() == false) { + Log.e(TAG,"SD card not Mounted"); + return ResponseCodes.OBEX_HTTP_NO_CONTENT; + } + /* 1. Extract the name,length and type header from operation object + * 2. check if name is null or empty + * 3. Check if disk has available space for the length of file + * 4. Open an input stream for the Bluetooth Socket and a file handle + * to the folder + * 5. Check if the file exists and can be overwritten + * 6. If 2,5 is false and 3 is satisfied proceed to read from the input + * stream and write to the new file + */ + try { + request = op.getReceivedHeader(); + length = extractLength(request); + name = (String)request.getHeader(HeaderSet.NAME); + /* Put with directory name "." and ".." is not allowed */ + if (TextUtils.equals(name, FOLDER_NAME_DOT) || + TextUtils.equals(name, FOLDER_NAME_DOTDOT) ) { + if(D) Log.d(TAG, "cannot put the directory " + name); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + filetype = (String)request.getHeader(HeaderSet.TYPE); + if (D) Log.d(TAG,"type = " + filetype + " name = " + name + + " Current Path = " + mCurrentPath + "length = " + length); + + if (length == 0) { + if (D) Log.d(TAG, "length is 0,proceeding with the transfer"); + } + if (name == null || name.equals("")) { + if (D) Log.d(TAG, "name is null or empty, reject the transfer"); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + if(FileUtils.checkAvailableSpace(length) == false) { + if (D) Log.d(TAG,"No Space Available"); + return ResponseCodes.OBEX_HTTP_ENTITY_TOO_LARGE; + } + BufferedOutputStream buff_op_stream = null; + InputStream in_stream = null; + + try { + in_stream = op.openInputStream(); + } catch (IOException e1) { + Log.e(TAG,"onPut open input stream "+ e1.toString()); + if (D) Log.d(TAG, "Error while openInputStream"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + int positioninfile = 0; + File fileinfo = new File(mCurrentPath+ "/" + name); + File parentFile = fileinfo.getParentFile(); + if (parentFile != null) { + if (parentFile.canWrite() == false) { + if (D) Log.d(TAG,"Dir "+ fileinfo.getParent() +"is read-only"); + return ResponseCodes.OBEX_DATABASE_LOCKED; + } + } else { + Log.e(TAG, "Error! Not able to get parent file name"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + /* If File exists we delete and proceed to take the rest of bytes */ + if(fileinfo.exists() == true) { + if(fileinfo.canWrite()) { + fileinfo.delete(); + fileinfo = null; + fileinfo = new File(mCurrentPath+ "/" + name); + } else { + /* if Readonly reject the replace */ + if (D) Log.d(TAG,"File is readonly"); + return ResponseCodes.OBEX_DATABASE_LOCKED; + } + } + + FileOutputStream fileOutputStream = new FileOutputStream(fileinfo); + buff_op_stream = new BufferedOutputStream(fileOutputStream, 0x4000); + int outputBufferSize = op.getMaxPacketSize(); + byte[] buff = new byte[outputBufferSize]; + int readLength = 0; + long timestamp = 0; + long starttimestamp = System.currentTimeMillis(); + try { + while ((positioninfile != length)) { + if (sIsAborted) { + ((ServerOperation)op).isAborted = true; + sIsAborted = false; + break; + } + timestamp = System.currentTimeMillis(); + if (V) Log.v(TAG,"Read Socket >"); + readLength = in_stream.read(buff); + if (V) Log.v(TAG,"Read Socket <"); + + if (readLength == -1) { + if (D) Log.d(TAG,"File reached end at position" + + positioninfile); + break; + } + + buff_op_stream.write(buff, 0, readLength); + positioninfile += readLength; + + if (V) { + Log.v(TAG, "Receive file position = " + positioninfile + + " readLength "+ readLength + " bytes took " + + (System.currentTimeMillis() - timestamp) + + " ms"); + } + } + }catch (IOException e1) { + Log.e(TAG, "onPut File receive"+ e1.toString()); + if (D) Log.d(TAG, "Error when receiving file"); + ((ServerOperation)op).isAborted = true; + /* If the transfer completed due to a + * abort from Ftp client, clean up the + * file in the Server + */ + fileinfo.delete(); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + long finishtimestamp = System.currentTimeMillis(); + Log.i(TAG,"Put Request TP analysis : Received "+ positioninfile + + " bytes in " + (finishtimestamp - starttimestamp)+"ms"); + if (buff_op_stream != null) { + try { + buff_op_stream.close(); + } catch (IOException e) { + Log.e(TAG,"onPut close stream "+ e.toString()); + if (D) Log.d(TAG, "Error when closing stream after send"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + if(D) Log.d(TAG,"close Stream >"); + if (!closeStream(in_stream, op)) { + if (D) Log.d(TAG,"Failed to close Input stream"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (D) Log.d(TAG,"close Stream <"); + + FileUtils.sendMessage(mCallback,BluetoothFtpService.MSG_FILE_RECEIVED, + fileinfo.getAbsolutePath()); + + }catch (IOException e) { + Log.e(TAG, "onPut headers error "+ e.toString()); + if (D) Log.d(TAG, "request headers error"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (D) Log.d(TAG, "onPut() -"); + return ResponseCodes.OBEX_HTTP_OK; + } + /** + * Called when a SETPATH request is received. + * + * @param request contains the headers sent by the client; + * request will never be null + * @param reply the headers that should be sent in the reply; + * reply will never be null + * @param backup true if the client requests that the server + * back up one directory before changing to the path described by + * name; false to apply the request to the + * present path + * @param create true if the path should be created if it does + * not already exist; false if the path should not be + * created if it does not exist and an error code should be returned + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + @Override + public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, + final boolean create) { + + if (D) Log.d(TAG, "onSetPath() +"); + + String current_path_tmp = mCurrentPath; + String tmp_path = null; + /* Check if Card is mounted */ + if(FileUtils.checkMountedState() == false) { + Log.e(TAG,"SD card not Mounted"); + return ResponseCodes.OBEX_HTTP_NO_CONTENT; + } + /* Extract the name header */ + try { + tmp_path = (String)request.getHeader(HeaderSet.NAME); + } catch (IOException e) { + Log.e(TAG,"onSetPath get header"+ e.toString()); + if (D) Log.d(TAG, "Get name header fail"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (D) Log.d(TAG, "backup=" + backup + " create=" + create + + " name=" + tmp_path +"mCurrentPath = " + mCurrentPath); + + /* If the name is "." or ".." do not allow to create or set the directory */ + if (TextUtils.equals(tmp_path, FOLDER_NAME_DOT) || + TextUtils.equals(tmp_path, FOLDER_NAME_DOTDOT)) { + if(D) Log.d(TAG, "cannot create or set the directory to " + tmp_path); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + + /* If backup flag is set then if the current path is not null then + * remove the substring till '/' in the current path For ex. if current + * path is "/sdcard/bluetooth" we will return a string "/sdcard" into + * current_path_tmp + * + * else we append the name to the current path if not null or else + * set the current path to ROOT Folder path + */ + if (backup) { + if (current_path_tmp.length() != 0) { + current_path_tmp = current_path_tmp.substring(0, + current_path_tmp.lastIndexOf("/")); + } + } else { + if (tmp_path == null) { + current_path_tmp = ROOT_FOLDER_PATH; + } else { + current_path_tmp = current_path_tmp + "/" + tmp_path; + } + } + + /* If the Path passed in the name doesnot exist and if the create flag + * is set we proceed towards creating the folder in the current folder + * + * else if the path doesnot exist and the create flag is not set we + * return ResponseCodes.OBEX_HTTP_NOT_FOUND + */ + if ((current_path_tmp.length() != 0) && + (!FileUtils.doesPathExist(current_path_tmp))) { + if (D) Log.d(TAG, "Current path has valid length "); + if (create) { + if (D) Log.d(TAG, "path create is not forbidden!"); + File filecreate = new File(current_path_tmp); + filecreate.mkdir(); + mCurrentPath = current_path_tmp; + return ResponseCodes.OBEX_HTTP_OK; + } else { + if (D) Log.d(TAG, "path not found error"); + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + } + /* We have already reached the root folder but user tries to press the + * back button + */ + if(current_path_tmp.length() == 0){ + current_path_tmp = ROOT_FOLDER_PATH; + } + + mCurrentPath = current_path_tmp; + if (V) Log.v(TAG, "after setPath, mCurrentPath == " + mCurrentPath); + + if (D) Log.d(TAG, "onSetPath() -"); + + return ResponseCodes.OBEX_HTTP_OK; + } + /** + * Called when session is closed. + */ + @Override + public void onClose() { + if (D) Log.d(TAG, "onClose() +"); + /* Send a message to the FTP service to close the Server session */ + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothFtpService.MSG_SERVERSESSION_CLOSE; + msg.sendToTarget(); + if (D) Log.d(TAG, + "onClose(): msg MSG_SERVERSESSION_CLOSE sent out."); + } + if (D) Log.d(TAG, "onClose() -"); + } + + @Override + public int onGet(Operation op) { + if (D) Log.d(TAG, "onGet() +"); + + sIsAborted = false; + HeaderSet request = null; + String type = ""; + String name = ""; + /* Check if Card is mounted */ + if(FileUtils.checkMountedState() == false) { + Log.e(TAG,"SD card not Mounted"); + return ResponseCodes.OBEX_HTTP_NO_CONTENT; + } + /*Extract the name and type header from operation object */ + try { + request = op.getReceivedHeader(); + type = (String)request.getHeader(HeaderSet.TYPE); + name = (String)request.getHeader(HeaderSet.NAME); + /* Get with folder name "." and ".." is not allowed */ + if (TextUtils.equals(name, FOLDER_NAME_DOT) || + TextUtils.equals(name, FOLDER_NAME_DOTDOT) ) { + if(D) Log.d(TAG, "cannot get the folder " + name); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + } catch (IOException e) { + Log.e(TAG,"onGet request headers "+ e.toString()); + if (D) Log.d(TAG, "request headers error"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + if (D) Log.d(TAG,"type = " + type + " name = " + name + + " Current Path = " + mCurrentPath); + + boolean validName = true; + + if (TextUtils.isEmpty(name)) { + validName = false; + } + if (D) Log.d(TAG,"validName = " + validName); + + + if(type != null) { + /* If type is folder listing then invoke the routine to package + * the folder listing contents in xml format + * + * Else call the routine to send the requested file names contents + */ + if(type.equals(TYPE_LISTING)){ + if(!validName){ + if (D) Log.d(TAG,"Not having a name"); + File rootfolder = new File(mCurrentPath); + File [] files = rootfolder.listFiles(); + if (files != null) { + for(int i = 0; i < files.length; i++) + if (D) Log.d(TAG,"Folder listing =" + files[i] ); + return sendFolderListingXml(0,op,files); + } else { + Log.e(TAG,"error in listing files"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } else { + if (D) Log.d(TAG,"Non Root Folder"); + if(type.equals(TYPE_LISTING)){ + File currentfolder = new File(mCurrentPath); + if (D) Log.d(TAG,"Current folder name = " + + currentfolder.getName() + + "Requested subFolder =" + name); + if(currentfolder.getName().compareTo(name) != 0) { + if (D) { + Log.d(TAG,"Not currently in this folder"); + } + File subFolder = new File(mCurrentPath +"/"+ name); + if(subFolder.exists()) { + File [] files = subFolder.listFiles(); + if (files != null) { + return sendFolderListingXml(0,op,files); + } else { + Log.e(TAG,"error in listing files"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } else { + Log.e(TAG, + "ResponseCodes.OBEX_HTTP_NO_CONTENT"); + return ResponseCodes.OBEX_HTTP_NO_CONTENT; + } + } + + File [] files = currentfolder.listFiles(); + if (files != null) { + for(int i = 0; i < files.length; i++) + if (D) Log.d(TAG,"Non Root Folder listing =" + files[i] ); + return sendFolderListingXml(0,op,files); + } else { + Log.e(TAG,"error in listing files"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + } + } + } else { + if (D) Log.d(TAG,"File get request"); + File fileinfo = new File (mCurrentPath + "/" + name); + return sendFileContents(op,fileinfo); + } + if (D) Log.d(TAG, "onGet() -"); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + /** + * sendFileContents + * + * Called when a GET request to get the contents of a File is received + * + * @param op provides the handle to the current server operation + * @param fileinfo provides the handle to the file to be sent + * @return a response code defined in ResponseCodes that will + * be returned to the client; if an invalid response code is + * provided, the OBEX_HTTP_INTERNAL_ERROR response code + * will be used + */ + private final int sendFileContents(Operation op,File fileinfo){ + + if (D) Log.d(TAG,"sendFile + = " + fileinfo.getName() ); + int position = 0; + int readLength = 0; + boolean isitokToProceed = false; + int outputBufferSize = op.getMaxPacketSize(); + long timestamp = 0; + int responseCode = -1; + FileInputStream fileInputStream = null; + OutputStream outputStream = null; + BufferedInputStream bis; + long finishtimestamp; + long starttimestamp; + long readbytesleft = 0; + long filelength = fileinfo.length(); + + byte[] buffer = new byte[outputBufferSize]; + + if(fileinfo.exists() != true) { + return ResponseCodes.OBEX_HTTP_NOT_FOUND; + } + + try { + fileInputStream = new FileInputStream(fileinfo); + outputStream = op.openOutputStream(); + } catch(IOException e) { + Log.e(TAG,"SendFilecontents open stream "+ e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } finally { + if (fileInputStream != null && outputStream == null) { + try { + fileInputStream.close(); + } catch (IOException ei) { + Log.e(TAG, "Error while closing stream"+ ei.toString()); + } + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + bis = new BufferedInputStream(fileInputStream, 0x4000); + starttimestamp = System.currentTimeMillis(); + /* + * Some Devices expect Length, send Legth Header also + */ + try { + HeaderSet reply = new HeaderSet(); + if (reply != null) { + reply.setHeader(HeaderSet.LENGTH, filelength); + op.sendHeaders (reply); + } + while ((position != filelength)) { + if (sIsAborted) { + ((ServerOperation)op).isAborted = true; + sIsAborted = false; + break; + } + timestamp = System.currentTimeMillis(); + + readbytesleft = filelength - position; + if(readbytesleft < outputBufferSize) { + outputBufferSize = (int) readbytesleft; + } + readLength = bis.read(buffer, 0, outputBufferSize); + if (D) Log.d(TAG,"Read File"); + + outputStream.write(buffer, 0, readLength); + position += readLength; + if (V) { + Log.v(TAG, "Sending file position = " + position + + " readLength " + readLength + " bytes took " + + (System.currentTimeMillis() - timestamp) + " ms"); + } + } + } catch (IOException e) { + Log.e(TAG,"Write aborted " + e.toString()); + if (D) Log.d(TAG,"Write Abort Received"); + ((ServerOperation)op).isAborted = true; + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + finishtimestamp = System.currentTimeMillis(); + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + Log.e(TAG,"input stream close" + e.toString()); + if (D) Log.d(TAG, "Error when closing stream after send"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + + if (!closeStream(outputStream, op)) { + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + if((position == filelength) || (((ServerOperation)op).isAborted == true)) { + Log.i(TAG,"Get Request TP analysis : Transmitted "+ position + + " bytes in" + (finishtimestamp - starttimestamp) + "ms"); + return ResponseCodes.OBEX_HTTP_OK; + }else { + return ResponseCodes.OBEX_HTTP_CONTINUE; + } + } + + /** + * scanDirectory + * + * Scans a directory recursively for files and their mimetypes + * and adds them into a global list of filenames and their + * corresponding mime type list. + * + * @param dir File handle to file/folder + * @return none + */ + + private final void scanDirectory(File dir) { + Log.d(TAG,"scanDirectory Dest "+dir); + if(dir.isFile()) { + String mimeType = null; + /* first we look for Mimetype in Android map */ + String extension = null, type = null; + String name = dir.getAbsolutePath(); + int dotIndex = name.lastIndexOf("."); + if (dotIndex < 0) { + if (D) Log.d(TAG, "There is no file extension"); + } else { + extension = name.substring(dotIndex + 1).toLowerCase(); + MimeTypeMap map = MimeTypeMap.getSingleton(); + type = map.getMimeTypeFromExtension(extension); + if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type); + if (type != null) { + mimeType = type; + } + if (mimeType != null) { + mimeType = mimeType.toLowerCase(); + if (D) Log.d(TAG, "Adding file path"+" /mnt" + dir.getAbsolutePath()); + filenames.add("/mnt" + dir.getAbsolutePath()); + if (D) Log.d(TAG, "Adding type" +mimeType); + types.add(mimeType); + } + } + return; + } + + File [] files = dir.listFiles(); + if (files == null) return; + for(int i = 0; i < files.length; i++) { + if (D) Log.d(TAG,"Files =" + files[i]); + if(files[i].isDirectory()) { + scanDirectory(files[i]); + } else if (files[i].isFile()) { + String mimeType = null; + /* first we look for Mimetype in Android map */ + String extension = null, type = null; + String name = files[i].getAbsolutePath(); + int dotIndex = name.lastIndexOf("."); + if (dotIndex < 0) { + if (D) Log.d(TAG, "There is no file extension"); + } else { + extension = name.substring(dotIndex + 1).toLowerCase(); + MimeTypeMap map = MimeTypeMap.getSingleton(); + type = map.getMimeTypeFromExtension(extension); + if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + + type); + if (type != null) { + mimeType = type; + } + if (mimeType != null) { + mimeType = mimeType.toLowerCase(); + if (D) Log.d(TAG, "Adding file path"+" /mnt" + files[i].getAbsolutePath()); + filenames.add("/mnt" + files[i].getAbsolutePath()); + if (D) Log.d(TAG, "Adding type" +mimeType); + types.add(mimeType); + } + } + } + } + } + + /* Extract the length from header */ + private final long extractLength(HeaderSet request) { + long len = 0; + if(request != null) { + try { + Object length = request.getHeader(HeaderSet.LENGTH); + /* Ensure that the length is not null before + * attempting a type cast to Long + */ + if(length != null) + len = (Long)length; + } catch(IOException e) {} + } + return len; + } + + /** Function to send folder listing data to client */ + private final int pushBytes(Operation op, final String folderlistString) { + if (D) Log.d(TAG,"pushBytes +"); + if (folderlistString == null) { + if (D) Log.d(TAG, "folderlistString is null!"); + return ResponseCodes.OBEX_HTTP_OK; + } + + byte [] folderListing = folderlistString.getBytes(); + int folderlistStringLen = folderListing.length; + if (D) Log.d(TAG, "Send Data: len=" + folderlistStringLen); + + OutputStream outputStream = null; + int pushResult = ResponseCodes.OBEX_HTTP_OK; + try { + outputStream = op.openOutputStream(); + } catch (IOException e) { + Log.e(TAG, "open outputstrem failed" + e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + int position = 0; + long timestamp = 0; + int outputBufferSize = op.getMaxPacketSize(); + if (V) Log.v(TAG, "outputBufferSize = " + outputBufferSize); + while (position != folderlistStringLen) { + if (sIsAborted) { + ((ServerOperation)op).isAborted = true; + sIsAborted = false; + break; + } + if (V) timestamp = System.currentTimeMillis(); + int readLength = outputBufferSize; + if (folderlistStringLen - position < outputBufferSize) { + readLength = folderlistStringLen - position; + } + + try { + outputStream.write(folderListing, position, readLength); + } catch (IOException e) { + Log.e(TAG, "write outputstrem failed" + e.toString()); + pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + break; + } + if (V) { + if (D) Log.d(TAG, "Sending folderlist String position = " + position + " readLength " + + readLength + " bytes took " + (System.currentTimeMillis() - timestamp) + + " ms"); + } + position += readLength; + } + + if (V) Log.v(TAG, "Send Data complete!"); + + if (!closeStream(outputStream, op)) { + pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (V) Log.v(TAG, "pushBytes - result = " + pushResult); + return pushResult; + } + + /** Form and Send an XML format String to client for Folder listing */ + private final int sendFolderListingXml(final int type,Operation op,final File[] files) { + if (V) Log.v(TAG, "sendFolderListingXml =" + files.length); + + StringBuilder result = new StringBuilder(); + result.append(""); + result.append('\r'); + result.append('\n'); + result.append(""); + result.append('\r'); + result.append('\n'); + result.append(""); + result.append('\r'); + result.append('\n'); + + /* For the purpose of parsing file attributes and to maintain the standard, + * enforce the format to be used + */ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy MM dd HH:mm"); + + String name = ""; + String permission = ""; + for(int i =0; i < files.length; i++) { + + if (files[i].isDirectory()) { + name = "folder name"; + } else { + name = "file name"; + } + + if (files[i].canRead() && files[i].canWrite()) { + permission = "RW"; + } else if(files[i].canRead()) { + permission = "R"; + } else if(files[i].canWrite()) { + permission = "W"; + } + + Date date = new Date(files[i].lastModified()); + String[] dateset = sdf.format(date).split(" "); + + StringBuffer xmldateformat = new StringBuffer(dateset[INDEX_YEAR]); + xmldateformat.append(dateset[INDEX_MONTH]); + xmldateformat.append(dateset[INDEX_DATE]); + + String[] timeset = dateset[INDEX_TIME].split(":"); + xmldateformat.append("T"); + xmldateformat.append(timeset[INDEX_TIME_HOUR]); + xmldateformat.append(timeset[INDEX_TIME_MINUTE]); + xmldateformat.append("00Z"); + + if (D) Log.d(TAG, name +"=" + files[i].getName()+ " size=" + + files[i].length() + " modified=" + date.toString() + + " dateformat to send=" + xmldateformat.toString()); + + result.append("<" + name + "=\"" + files[i].getName()+ "\"" + " size=\"" + + files[i].length() + "\"" + " user-perm=\"" + permission + "\"" + + " modified=\"" + xmldateformat.toString() + "\"" + "/>"); + result.append('\r'); + result.append('\n'); + } + result.append(""); + result.append('\r'); + result.append('\n'); + if (D) Log.d(TAG, "sendFolderListingXml -"); + return pushBytes(op, result.toString()); + } + /* Close the output stream */ + public static boolean closeStream(final OutputStream out, final Operation op) { + boolean returnvalue = true; + if (D) Log.d(TAG, "closeoutStream +"); + 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, "operation close failed" + e.toString()); + returnvalue = false; + } + + if (D) Log.d(TAG, "closeoutStream -"); + return returnvalue; + } + /* Close the input stream */ + public static boolean closeStream(final InputStream in, final Operation op) { + boolean returnvalue = true; + if (D) Log.d(TAG, "closeinStream +"); + + try { + if (in != null) { + in.close(); + } + } catch (IOException e) { + Log.e(TAG, "inputStream close failed" + e.toString()); + returnvalue = false; + } + try { + if (op != null) { + op.close(); + } + } catch (IOException e) { + Log.e(TAG, "operation close failed" + e.toString()); + returnvalue = false; + } + + if (D) Log.d(TAG, "closeinStream -"); + + return returnvalue; + } +}; diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpReceiver.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpReceiver.java new file mode 100644 index 0000000..eb57b4c --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpReceiver.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.codeaurora.bluetooth.ftp; +import android.bluetooth.BluetoothAdapter; +import android.content.Intent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.util.Log; +import android.os.SystemProperties; + +public class BluetoothFtpReceiver extends BroadcastReceiver { + + private static final String TAG = "BluetoothFtpReceiver"; + + private static final boolean V = BluetoothFtpService.VERBOSE; + + @Override + public void onReceive (Context context, Intent intent) { + + if(V) Log.v(TAG,"BluetoothFtpReceiver onReceive :" + intent.getAction()); + + Intent in = new Intent(); + in.putExtras(intent); + in.setClass(context, BluetoothFtpService.class); + String action = intent.getAction(); + in.putExtra("action",action); + + boolean startService = true; + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = in.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + //in.putExtra(BluetoothAdapter.EXTRA_STATE, state); + /* + * Other than Tranistioning state, start the FTP service whenever + * BT transitioned to OFF/ON, or Adapter returns error + */ + if ((state == BluetoothAdapter.STATE_TURNING_ON) + || (state == BluetoothAdapter.STATE_OFF)) { + startService = false; + } + } else { + // Don't forward intent unless device has bluetooth and bluetooth is enabled. + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null || !adapter.isEnabled()) { + startService = false; + } + } + + if (startService) { + /* start the FTP service only if ftp property is enabled in build */ + //if (SystemProperties.getBoolean("ro.qualcomm.bluetooth.ftp", false)) { + if(V) Log.v(TAG,"BluetoothFtpReceiver Start Service"); + context.startService(in); + //} + } + } +} diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpRfcommTransport.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpRfcommTransport.java new file mode 100644 index 0000000..82e1cb6 --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpRfcommTransport.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codeaurora.bluetooth.ftp; + +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 BluetoothFtpRfcommTransport implements ObexTransport { + private BluetoothSocket mSocket = null; + + public BluetoothFtpRfcommTransport(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; + } + +} diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpService.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpService.java new file mode 100644 index 0000000..9a18cc6 --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpService.java @@ -0,0 +1,909 @@ +/* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010-2012 The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.codeaurora.bluetooth.ftp; + +import android.app.Service; +import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.ContentProviderClient; +import android.content.Intent; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.os.Handler; +import android.text.TextUtils; +import android.content.res.Resources; +import android.os.IBinder; +import android.os.Message; +import android.os.Bundle; +import android.os.ParcelUuid; +import org.codeaurora.bluetooth.R; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import java.io.IOException; +import java.util.ArrayList; +import javax.obex.ServerSession; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.net.Uri; +import android.content.ContentResolver; +import android.os.RemoteException; +import android.provider.MediaStore; +import javax.obex.ObexHelper; +import android.bluetooth.BluetoothUuid; + + +import java.util.ArrayList; +import java.util.List; +import java.util.HashSet; + + +public class BluetoothFtpService extends Service { + private static final String TAG = "BluetoothFtpService"; + + /** + * To enable FTP DEBUG/VERBOSE logging - run below cmd in adb shell, and + * restart com.android.bluetooth process. only enable DEBUG log: + * "setprop log.tag.BluetoothFtpService DEBUG"; enable both VERBOSE and + * DEBUG log: "setprop log.tag.BluetoothFtpService VERBOSE" + */ + + //public static final boolean DEBUG = false; + + //public static final boolean VERBOSE = false; + + public static final boolean DEBUG = true; + public static final boolean VERBOSE = true; + private int mState; + + /** + * Intent indicating incoming connection request which is sent to + * BluetoothFtpActivity + */ + public static final String ACCESS_REQUEST_ACTION = "org.codeaurora.bluetooth.ftp.accessrequest"; + + /** + * Intent indicating incoming connection request accepted by user which is + * sent from BluetoothFtpActivity + */ + public static final String ACCESS_ALLOWED_ACTION = "org.codeaurora.bluetooth.ftp.accessallowed"; + + /** + * Intent indicating incoming connection request denied by user which is + * sent from BluetoothFtpActivity + */ + public static final String ACCESS_DISALLOWED_ACTION = + "org.codeaurora.bluetooth.ftp.accessdisallowed"; + + /** + * Intent indicating incoming obex authentication request which is from + * PCE(Carkit) + */ + public static final String AUTH_CHALL_ACTION = "org.codeaurora.bluetooth.ftp.authchall"; + + /** + * Intent indicating obex session key input complete by user which is sent + * from BluetoothFtpActivity + */ + public static final String AUTH_RESPONSE_ACTION = "org.codeaurora.bluetooth.ftp.authresponse"; + + /** + * Intent indicating user canceled obex authentication session key input + * which is sent from BluetoothFtpActivity + */ + public static final String AUTH_CANCELLED_ACTION = "org.codeaurora.bluetooth.ftp.authcancelled"; + + /** + * Intent indicating timeout for user confirmation, which is sent to + * BluetoothFtpActivity + */ + public static final String USER_CONFIRM_TIMEOUT_ACTION = + "org.codeaurora.bluetooth.ftp.userconfirmtimeout"; + + public static final String THIS_PACKAGE_NAME = "org.codeaurora.bluetooth"; + + /** + * Intent Extra name indicating always allowed which is sent from + * BluetoothFtpActivity + */ + public static final String EXTRA_ALWAYS_ALLOWED = "org.codeaurora.bluetooth.ftp.alwaysallowed"; + + /** + * Intent Extra name indicating session key which is sent from + * BluetoothFtpActivity + */ + public static final String EXTRA_SESSION_KEY = "org.codeaurora.bluetooth.ftp.sessionkey"; + + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + + public static final int MSG_SERVERSESSION_CLOSE = 5004; + + public static final int MSG_SESSION_ESTABLISHED = 5005; + + public static final int MSG_SESSION_DISCONNECTED = 5006; + + public static final int MSG_OBEX_AUTH_CHALL = 5007; + + public static final int MSG_FILE_RECEIVED = 5008; + + public static final int MSG_FILE_DELETED = 5009; + + public static final int MSG_FILES_RECEIVED = 5010; + + public static final int MSG_FILES_DELETED = 5011; + + private static final int MSG_INTERNAL_START_LISTENER = 1; + + private static final int MSG_INTERNAL_USER_TIMEOUT = 2; + + private static final int MSG_INTERNAL_AUTH_TIMEOUT = 3; + + private static final int MSG_INTERNAL_OBEX_RFCOMM_SESSION_UP = 10; + + private static final int MSG_INTERNAL_OBEX_L2CAP_SESSION_UP = 11; + + //Port number for FTP RFComm Socket + private static final int PORT_NUM = 20; + + private static final int DEFAULT_FTP_PSM = 5257; + + private static final int USER_CONFIRM_TIMEOUT_VALUE = 30000; + + public static final ParcelUuid FileTransfer = + ParcelUuid.fromString("00001106-0000-1000-8000-00805f9b34fb"); + + // Ensure not conflict with Opp notification ID + private static final int NOTIFICATION_ID_ACCESS = -1000005; + + private static final int NOTIFICATION_ID_AUTH = -1000006; + + private static final int FTP_MEDIA_SCANNED = 4; + + private static final int FTP_MEDIA_SCANNED_FAILED = 5; + + public static final int FTP_MEDIA_ADD = 6; + + public static final int FTP_MEDIA_DELETE = 7; + + public static final int FTP_MEDIA_FILES_ADD = 8; + + public static final int FTP_MEDIA_FILES_DELETE = 9; + + public static boolean isL2capSocket = false; + + private WakeLock mWakeLock; + + private BluetoothAdapter mAdapter; + + private RfcommSocketAcceptThread mRfcommAcceptThread = null; + + private BluetoothFtpAuthenticator mAuth = null; + + private BluetoothServerSocket mRfcommServerSocket = null; + + + private BluetoothSocket mConnSocket = null; + private static HashSet trustDevices = new HashSet(); + private BluetoothDevice mRemoteDevice = null; + + private static String sRemoteDeviceName = null; + + private boolean mHasStarted = false; + + private volatile boolean mInterrupted; + + private int mStartId = -1; + + private BluetoothFtpObexServer mFtpServer = null; + + private ServerSession mServerSession = null; + + private boolean isWaitingAuthorization = false; + + + public BluetoothFtpService() { + } + + @Override + public void onCreate() { + super.onCreate(); + if (VERBOSE) Log.v(TAG, "Ftp Service onCreate"); + Log.i(TAG, "FFFFFtp Service onCreate"); + + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (!mHasStarted) { + mHasStarted = true; + if (VERBOSE) Log.v(TAG, "Starting FTP service"); + + int state = mAdapter.getState(); + if (state == BluetoothAdapter.STATE_ON) { + mSessionStatusHandler.sendMessage(mSessionStatusHandler + .obtainMessage(MSG_INTERNAL_START_LISTENER)); + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (VERBOSE) Log.v(TAG, "Ftp Service onStartCommand"); + int retCode = super.onStartCommand(intent, flags, startId); + if (retCode == START_STICKY) { + mStartId = startId; + if (mAdapter == null) { + Log.w(TAG, "Stopping BluetoothFtpService: " + + "device does not have BT or device is not ready"); + // Release all resources + closeService(); + } else { + // No need to handle the null intent case, because we have + // all restart work done in onCreate() + if (intent != null) { + parseIntent(intent); + } + } + } + return retCode; + } + + // process the intent from receiver + private void parseIntent(final Intent intent) { + String action = (intent == null) ? null : intent.getStringExtra("action"); + if (action == null) { + Log.e(TAG, "Unexpected error! action is null"); + return; + } + if (VERBOSE) Log.v(TAG, "action: " + action); + + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + boolean removeTimeoutMsg = true; + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + removeTimeoutMsg = false; + if (state == BluetoothAdapter.STATE_TURNING_OFF) { + /** Terminate file copy operation if it is in progress */ + FileUtils.interruptFileCopy = true; + // Send any pending timeout now, as this service will be destroyed. + if (mSessionStatusHandler.hasMessages(MSG_INTERNAL_USER_TIMEOUT)) { + Intent i = new Intent(USER_CONFIRM_TIMEOUT_ACTION); + sendBroadcast(i); + removeFtpNotification(NOTIFICATION_ID_AUTH); + } + // Release all resources + closeService(); + } else { + removeTimeoutMsg = false; + } + } else if (action.equals(ACCESS_ALLOWED_ACTION)) { + if (!isWaitingAuthorization) { + // this reply is not for us + return; + } + + isWaitingAuthorization = false; + + if (intent.getBooleanExtra(BluetoothFtpService.EXTRA_ALWAYS_ALLOWED, false)) { + trustDevices.add(mRemoteDevice); + Log.v(TAG, "setTrust() D: " + mRemoteDevice.getName()+ "ADDED: " + trustDevices.contains(mRemoteDevice)); + } + try { + if (mConnSocket != null) { + startObexServerSession(); + } else { + stopObexServerSession(); + } + } catch (IOException ex) { + Log.e(TAG, "Caught the error: " + ex.toString()); + } + removeFtpNotification(NOTIFICATION_ID_ACCESS); + } else if (action.equals(ACCESS_DISALLOWED_ACTION)) { + stopObexServerSession(); + } else if (action.equals(AUTH_RESPONSE_ACTION)) { + String sessionkey = intent.getStringExtra(EXTRA_SESSION_KEY); + notifyAuthKeyInput(sessionkey); + removeAuthChallTimer(); + removeFtpNotification(NOTIFICATION_ID_ACCESS); + } else if (action.equals(AUTH_CANCELLED_ACTION)) { + notifyAuthCancelled(); + removeAuthChallTimer(); + removeFtpNotification(NOTIFICATION_ID_ACCESS); + } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { + BluetoothDevice device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (device != null && device.equals(mRemoteDevice)) { + /** Terminate file copy operation if it is in progress */ + FileUtils.interruptFileCopy = true; + if (mSessionStatusHandler != null) { + /* Let the user timeout handle this case as well */ + mSessionStatusHandler.sendMessage(mSessionStatusHandler + .obtainMessage(MSG_INTERNAL_USER_TIMEOUT)); + removeTimeoutMsg = false; + } + } + } else if ( BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { + + if (intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if(device != null) + Log.d(TAG,"device: "+ device.getName()); + if(mRemoteDevice != null) + Log.d(TAG," Remtedevie: "+mRemoteDevice.getName()); + if (device != null && trustDevices.contains(device) && + intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) == BluetoothDevice.BOND_NONE) { + Log.d(TAG,"BOND_STATE_CHANGED RFRSH trustDevices"+ device.getName()); + trustDevices.remove(device); + } + } + + } else { + removeTimeoutMsg = false; + } + + if (removeTimeoutMsg) { + mSessionStatusHandler.removeMessages(MSG_INTERNAL_USER_TIMEOUT); + } + } + + @Override + public void onDestroy() { + if (VERBOSE) Log.v(TAG, "Ftp Service onDestroy"); + + super.onDestroy(); + if (mWakeLock != null) { + mWakeLock.release(); + mWakeLock = null; + } + closeService(); + } + + @Override + public IBinder onBind(Intent intent) { + if (VERBOSE) Log.v(TAG, "Ftp Service onBind"); + return null; + } + + private void startRfcommSocketListener() { + if (VERBOSE) Log.v(TAG, "Ftp Service startRfcommSocketListener"); + + if (mRfcommServerSocket == null) { + if (!initRfcommSocket()) { + closeService(); + return; + } + } + if (mRfcommAcceptThread == null) { + mRfcommAcceptThread = new RfcommSocketAcceptThread(); + mRfcommAcceptThread.setName("BluetoothFtpRfcommAcceptThread"); + mRfcommAcceptThread.start(); + } + } + private final boolean initRfcommSocket() { + if (VERBOSE) Log.v(TAG, "Ftp Service initSocket"); + + boolean initSocketOK = false; + final int CREATE_RETRY_TIME = 10; + + // It's possible that create will fail in some cases. retry for 10 times + for (int i = 0; i < CREATE_RETRY_TIME && !mInterrupted; i++) { + try { + // It is mandatory for PSE to support initiation of bonding and + // encryption. + mRfcommServerSocket = mAdapter.listenUsingRfcommWithServiceRecord("OBEX File Transfer", FileTransfer.getUuid()); + initSocketOK = true; + } catch (IOException e) { + Log.e(TAG, "Error create RfcommServerSocket " + e.toString()); + initSocketOK = false; + } + if (!initSocketOK) { + synchronized (this) { + try { + if (VERBOSE) Log.v(TAG, "wait 3 seconds"); + Thread.sleep(3000); + } catch (InterruptedException e) { + Log.e(TAG, "socketAcceptThread thread was interrupted (3)"); + mInterrupted = true; + } + } + } else { + break; + } + } + + if (initSocketOK && (mRfcommServerSocket != null) ) { + if (VERBOSE) Log.v(TAG, "Succeed to create listening socket on channel " + PORT_NUM); + + } else { + Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try"); + } + return initSocketOK; + } + + private final void closeRfcommSocket(boolean server, boolean accept) throws IOException { + if (server == true) { + // Stop the possible trying to init serverSocket + mInterrupted = false; + + if (mRfcommServerSocket != null) { + mRfcommServerSocket.close(); + } + } + + if (accept == true) { + if (mConnSocket != null) { + mConnSocket.close(); + } + } + } + private final void closeService() { + if (VERBOSE) Log.v(TAG, "Ftp Service closeService"); + + if (mServerSession != null) { + mServerSession.close(); + mServerSession = null; + } + + try { + closeRfcommSocket(true, true); + } catch (IOException ex) { + Log.e(TAG, "CloseSocket error: " + ex); + } + + if (mRfcommAcceptThread != null) { + try { + mRfcommAcceptThread.shutdown(); + mRfcommAcceptThread.join(); + mRfcommAcceptThread = null; + } catch (InterruptedException ex) { + Log.w(TAG, "mAcceptThread close error" + ex); + } + } + + mRfcommServerSocket = null; + mConnSocket = null; + + mHasStarted = false; + if (stopSelfResult(mStartId)) { + if (VERBOSE) Log.v(TAG, "successfully stopped ftp service"); + } + } + + private final void startObexServerSession() throws IOException { + if (VERBOSE) Log.v(TAG, "Ftp Service startObexServerSession"); + + // 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, + "StartingObexFtpTransaction"); + mWakeLock.setReferenceCounted(false); + } + + if(!mWakeLock.isHeld()) { + Log.e(TAG,"Acquire partial wake lock"); + mWakeLock.acquire(); + } + + mFtpServer = new BluetoothFtpObexServer(mSessionStatusHandler, this); + synchronized (this) { + mAuth = new BluetoothFtpAuthenticator(mSessionStatusHandler); + mAuth.setChallenged(false); + mAuth.setCancelled(false); + } + BluetoothFtpTransport transport; + if(isL2capSocket == false) { + transport = new BluetoothFtpTransport(mConnSocket,BluetoothFtpTransport.TYPE_RFCOMM); + } else { + transport = new BluetoothFtpTransport(mConnSocket,BluetoothFtpTransport.TYPE_L2CAP); + } + + mServerSession = new ServerSession(transport, mFtpServer, mAuth); + + if (VERBOSE) { + Log.v(TAG, "startObexServerSession() success!"); + } + } + + private void stopObexServerSession() { + if (VERBOSE) Log.v(TAG, "Ftp Service stopObexServerSession"); + + // Release the wake lock if obex transaction is over + if(mWakeLock != null) { + if (mWakeLock.isHeld()) { + Log.e(TAG,"Release full wake lock"); + mWakeLock.release(); + mWakeLock = null; + } else { + mWakeLock = null; + } + } + if (mServerSession != null) { + mServerSession.close(); + mServerSession = null; + } + + mRfcommAcceptThread = null; + + try { + closeRfcommSocket(false, true); + mConnSocket = null; + } catch (IOException e) { + Log.e(TAG, "closeSocket error: " + e.toString()); + } + // Last obex transaction is finished, we start to listen for incoming + // connection again + if (mAdapter.isEnabled()) { + startRfcommSocketListener(); + } + } + + private void notifyAuthKeyInput(final String key) { + synchronized (mAuth) { + if (key != null) { + mAuth.setSessionKey(key); + } + mAuth.setChallenged(true); + mAuth.notify(); + } + } + + private void notifyAuthCancelled() { + synchronized (mAuth) { + mAuth.setCancelled(true); + mAuth.notify(); + } + } + + private void notifyMediaScanner(Bundle obj,int op) { + String[] mTypes = obj.getStringArray("mimetypes"); + String[] fPaths = obj.getStringArray("filepaths"); + if((op == FTP_MEDIA_ADD) || (op == FTP_MEDIA_DELETE)) { + new FtpMediaScannerNotifier(this,obj.getString("filepath"), + obj.getString("mimetype"),mSessionStatusHandler,op); + } else if (mTypes != null && fPaths != null) { + new FtpMediaScannerNotifier(this,fPaths, + mTypes,mSessionStatusHandler,op); + } else { + Log.e(TAG, "Unexpected error! mTypes or fPaths is null"); + return; + } + } + + private void notifyContentResolver(Uri uri) { + if (VERBOSE) Log.v(TAG,"FTP_MEDIA_SCANNED deleting uri "+uri); + ContentProviderClient client = getContentResolver() + .acquireContentProviderClient(MediaStore.AUTHORITY); + if (client == null) { + Log.e(TAG, "Unexpected error! mTypes is null"); + return; + } + try { + client.delete(uri, null, null); + } catch(RemoteException e){ + Log.e(TAG,e.toString()); + } + if (VERBOSE) Log.v(TAG,"FTP_MEDIA_SCANNED deleted uri "+uri); + } + + /** + * 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 RfcommSocketAcceptThread extends Thread { + + private boolean stopped = false; + + private static final String RTAG = "BluetoothFtpService:RfcommSocketAcceptThread"; + + @Override + public void run() { + while (!stopped) { + try { + Log.v(RTAG,"Run Accept thread"); + mConnSocket = mRfcommServerSocket.accept(); + isL2capSocket = false; + mRemoteDevice = mConnSocket.getRemoteDevice(); + if (mRemoteDevice == null) { + Log.i(RTAG, "getRemoteDevice() = null"); + break; + } + sRemoteDeviceName = mRemoteDevice.getName(); + // In case getRemoteName failed and return null + if (TextUtils.isEmpty(sRemoteDeviceName)) { + sRemoteDeviceName = getString(R.string.defaultname); + } + mSessionStatusHandler.sendMessage(mSessionStatusHandler + .obtainMessage(MSG_INTERNAL_OBEX_RFCOMM_SESSION_UP)); + boolean trust = false; + if (trustDevices != null) + trust = trustDevices.contains(mRemoteDevice); + + if (VERBOSE) Log.v(RTAG, "GetTrustState() = " + trust); + + if (trust) { + try { + Log.i(RTAG, "incomming connection accepted from: " + + sRemoteDeviceName + " automatically as trusted device"); + startObexServerSession(); + } catch (IOException ex) { + Log.e(RTAG, "catch exception starting obex server session" + + ex.toString()); + } + } else { + isWaitingAuthorization = true; + createFtpNotification(ACCESS_REQUEST_ACTION); + Log.i(RTAG, "waiting for authorization for connection from: " + + sRemoteDeviceName); + mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler + .obtainMessage(MSG_INTERNAL_USER_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE); + } + stopped = true; // job done ,close this thread; + } catch (IOException ex) { + stopped = true; //IO exception, close the thread + //Assign socket handle to null. + mRfcommServerSocket = null; + if (VERBOSE) Log.v(RTAG, "Accept exception: " + ex.toString()); + } + } + } + + void shutdown() { + Log.e(RTAG,"Shutdown"); + stopped = true; + interrupt(); + } + } + + private final Handler mSessionStatusHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what); + + switch (msg.what) { + case MSG_INTERNAL_START_LISTENER: + if (mAdapter.isEnabled()) { + startRfcommSocketListener(); + } else { + closeService();// release all resources + } + break; + case MSG_INTERNAL_USER_TIMEOUT: + Intent intent = new Intent(USER_CONFIRM_TIMEOUT_ACTION); + sendBroadcast(intent); + removeFtpNotification(NOTIFICATION_ID_ACCESS); + isWaitingAuthorization = false; + stopObexServerSession(); + break; + case MSG_INTERNAL_AUTH_TIMEOUT: + Intent i = new Intent(USER_CONFIRM_TIMEOUT_ACTION); + sendBroadcast(i); + removeFtpNotification(NOTIFICATION_ID_AUTH); + notifyAuthCancelled(); + stopObexServerSession(); + break; + case MSG_SERVERSESSION_CLOSE: + stopObexServerSession(); + break; + case MSG_SESSION_ESTABLISHED: + break; + case MSG_SESSION_DISCONNECTED: + break; + case MSG_FILE_RECEIVED: + if (VERBOSE) Log.v(TAG,"MSG_FILE_RECEIVED"); + Bundle arguments = (Bundle) msg.obj; + notifyMediaScanner(arguments,FTP_MEDIA_ADD); + break; + case MSG_FILE_DELETED: + if (VERBOSE) Log.v(TAG,"MSG_FILE_DELETED"); + Bundle delarguments = (Bundle) msg.obj; + notifyMediaScanner(delarguments,FTP_MEDIA_DELETE); + break; + case MSG_FILES_DELETED: + if (VERBOSE) Log.v(TAG,"MSG_FILES_DELETED"); + Bundle delfilesarguments = (Bundle) msg.obj; + notifyMediaScanner(delfilesarguments,FTP_MEDIA_FILES_DELETE); + break; + case MSG_FILES_RECEIVED: + if (VERBOSE) Log.v(TAG,"MSG_FILES_RECEIVED"); + Bundle newfilearguments = (Bundle) msg.obj; + notifyMediaScanner(newfilearguments,FTP_MEDIA_FILES_ADD); + break; + + case FTP_MEDIA_SCANNED: + if (VERBOSE) Log.v(TAG,"FTP_MEDIA_SCANNED arg1 "+msg.arg1); + Uri uri = (Uri)msg.obj; + /* If the media scan was for a + * Deleted file Delete the entry + * from content resolver + */ + if((msg.arg1 == FTP_MEDIA_DELETE) || (msg.arg1 == FTP_MEDIA_FILES_DELETE)) { + notifyContentResolver(uri); + } + break; + case MSG_INTERNAL_OBEX_RFCOMM_SESSION_UP: + if (VERBOSE) Log.v(TAG,"MSG_INTERNAL_OBEX_RFCOMM_SESSION_UP"); + try { + closeRfcommSocket(true, false); + mRfcommServerSocket = null; + } catch (IOException ex) { + Log.e(TAG, "CloseSocket error: " + ex); + } + break; + case MSG_OBEX_AUTH_CHALL: + createFtpNotification(AUTH_CHALL_ACTION); + mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler + .obtainMessage(MSG_INTERNAL_AUTH_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE); + break; + default: + break; + } + } + }; + private void createFtpNotification(String action) { + + NotificationManager nm = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + + // Create an intent triggered by clicking on the status icon. + Intent clickIntent = new Intent(); + clickIntent.setClass(this, BluetoothFtpActivity.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(this, BluetoothFtpReceiver.class); + + Log.v(TAG,"createFtpNotification: action: "+action); + Notification notification = null; + String name = getRemoteDeviceName(); + if (action.equals(ACCESS_REQUEST_ACTION)) { + deleteIntent.setAction(ACCESS_ALLOWED_ACTION); + notification = new Notification(android.R.drawable.stat_sys_data_bluetooth, + getString(R.string.ftp_notif_ticker), System.currentTimeMillis()); + notification.setLatestEventInfo(this, getString(R.string.ftp_notif_ticker), + getString(R.string.ftp_notif_message, name), PendingIntent + .getActivity(this, 0, clickIntent, 0)); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; + notification.defaults = Notification.DEFAULT_SOUND; + notification.deleteIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, 0); + nm.notify(NOTIFICATION_ID_ACCESS, notification); + } else if (action.equals(AUTH_CHALL_ACTION)) { + deleteIntent.setAction(AUTH_CANCELLED_ACTION); + notification = new Notification(android.R.drawable.stat_sys_data_bluetooth, + getString(R.string.ftp_notif_ticker), System.currentTimeMillis()); + notification.setLatestEventInfo(this, getString(R.string.ftp_notif_title), + getString(R.string.ftp_notif_message, name), PendingIntent + .getActivity(this, 0, clickIntent, 0)); + + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; + notification.defaults = Notification.DEFAULT_SOUND; + notification.deleteIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, 0); + nm.notify(NOTIFICATION_ID_AUTH, notification); + } + } + + private void removeFtpNotification(int id) { + Context context = getApplicationContext(); + NotificationManager nm = (NotificationManager)context + .getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(id); + } + + private void removeAuthChallTimer() { + if (mSessionStatusHandler != null) + mSessionStatusHandler.removeMessages(MSG_INTERNAL_AUTH_TIMEOUT); + } + + public static String getRemoteDeviceName() { + return sRemoteDeviceName; + } + + public static class FtpMediaScannerNotifier implements MediaScannerConnectionClient { + + private MediaScannerConnection mConnection; + + private Context mContext; + + private Handler mCallback; + + private int mOp; + + public FtpMediaScannerNotifier(Context context,final String filename, + final String mimetype,Handler handler,int op) { + mContext = context; + mCallback = handler; + mOp = op; + if (VERBOSE) Log.v(TAG, "FTP MediaScannerConnection FtpMediaScannerNotifier mFilename =" + + filename + " mMimetype = " + mimetype +"operation " + mOp); + List filenames = new ArrayList(); + List types = new ArrayList(); + + filenames.add(filename); + types.add(mimetype); + MediaScannerConnection.scanFile(context,filenames.toArray(new String[filenames.size()]), + types.toArray(new String[types.size()]), + this); + } + + public FtpMediaScannerNotifier(Context context,final String[] filenames, + final String[] mimetypes,Handler handler,int op) { + mContext = context; + mCallback = handler; + mOp = op; + if (VERBOSE) Log.v(TAG, "FtpMediaScannerNotifier scan for multiple files " + + filenames.length +" " +mimetypes.length ); + MediaScannerConnection.scanFile(context,filenames,mimetypes, + this); + } + + public void onMediaScannerConnected() { + if (VERBOSE) Log.v(TAG, "FTP MediaScannerConnection onMediaScannerConnected"); + } + + public void onScanCompleted(String path, Uri uri) { + try { + if (VERBOSE) { + Log.v(TAG, "FTP MediaScannerConnection onScanCompleted"); + Log.v(TAG, "FTP MediaScannerConnection path is " + path); + Log.v(TAG, "FTP MediaScannerConnection Uri is " + uri); + Log.v(TAG, "FTP MediaScannerConnection mOp is " + mOp); + } + if (uri != null) { + Message msg = Message.obtain(); + msg.setTarget(mCallback); + msg.what = FTP_MEDIA_SCANNED; + msg.arg1 = mOp; + msg.obj = uri; + msg.sendToTarget(); + } else { + Message msg = Message.obtain(); + msg.setTarget(mCallback); + msg.what = FTP_MEDIA_SCANNED_FAILED; + msg.arg1 = mOp; + msg.sendToTarget(); + } + } catch (Exception ex) { + Log.e(TAG, "FTP !!!MediaScannerConnection exception: " + ex); + } finally { + if (VERBOSE) Log.v(TAG, "FTP MediaScannerConnection disconnect"); + } + } + }; +}; diff --git a/src/org/codeaurora/bluetooth/ftp/BluetoothFtpTransport.java b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpTransport.java new file mode 100644 index 0000000..f84b53d --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/BluetoothFtpTransport.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2008-2009, Motorola, Inc. + * Copyright (c) 2010-2011, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codeaurora.bluetooth.ftp; + +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 BluetoothFtpTransport implements ObexTransport { + private BluetoothSocket mSocket = null; + + private final int mType; + + public static final int TYPE_RFCOMM = 0; + + public static final int TYPE_L2CAP = 1; + + public BluetoothFtpTransport(BluetoothSocket rfs, int type) { + super(); + this.mSocket = rfs; + this.mType = type; + } + + 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; + } + + public String getRemoteAddress() { + if (mSocket == null) + return null; + return mSocket.getRemoteDevice().getAddress(); + } + + public boolean isAmpCapable() { + return mType == TYPE_L2CAP; + } + + public boolean isSrmCapable() { + return mType == TYPE_L2CAP; + } +/* + public boolean setDesiredAmpPolicy(int policy) { + if (mSocket == null || mType != TYPE_L2CAP) + return false; + return mSocket.setDesiredAmpPolicy(policy); + }*/ +} diff --git a/src/org/codeaurora/bluetooth/ftp/FileUtils.java b/src/org/codeaurora/bluetooth/ftp/FileUtils.java new file mode 100644 index 0000000..364331b --- /dev/null +++ b/src/org/codeaurora/bluetooth/ftp/FileUtils.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2011 The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of The Linux Foundation nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.codeaurora.bluetooth.ftp; + +import android.os.Message; +import android.os.Handler; +import android.os.StatFs; +import android.os.Environment; +import android.util.Log; +import android.os.Bundle; +import android.webkit.MimeTypeMap; + +import java.io.IOException; +import java.io.FileNotFoundException; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; + +import javax.obex.ResponseCodes; +import javax.obex.Operation; + +public class FileUtils { + + private static final String TAG = "FileUtils"; + + private static final boolean D = BluetoothFtpService.DEBUG; + + private static final boolean V = BluetoothFtpService.VERBOSE; + + public static boolean interruptFileCopy = false; + + /** + * deleteDirectory + * + * Called when a PUT request is received to delete a non empty folder + * + * @param mCallback handler for sending message + * @param dir provides the handle to the directory to be deleted + * @return a TRUE if operation was succesful or false otherwise + */ + public static final boolean deleteDirectory(Handler mCallback,File dir) { + if (D) Log.d(TAG, "deleteDirectory() +"); + if(dir.exists()) { + File [] files = dir.listFiles(); + if (files == null) { + Log.e(TAG, "error in listing directory "); + return false; + } + for(int i = 0; i < files.length;i++) { + if(files[i].isDirectory()) { + deleteDirectory(mCallback,files[i]); + if (D) Log.d(TAG,"Dir Delete =" + files[i].getName()); + } else { + if (D) Log.d(TAG,"File Delete =" + files[i].getName()); + files[i].delete(); + sendMessage(mCallback,BluetoothFtpService.MSG_FILE_DELETED,files[i].getAbsolutePath()); + } + } + } + + if (D) Log.d(TAG, "deleteDirectory() -"); + return( dir.delete() ); + } + + /** + * copyFolders + * + * Called when a Copy action is to be performed from a source + * folder to destination folder + * + * @param mCallback handler for sending message + * @param src File handle to source directory + * @param dest File handle to destination directory + * @return a ResponseCodes.OBEX_HTTP_OK if operation was succesful + * or ResponseCodes.OBEX_HTTP_INTERNAL_ERROR otherwise + */ + + public static final int copyFolders(Handler mCallback,File src, File dest) { + Log.d(TAG,"copyFolders src "+src+"dest "+dest); + int ret = 0; + dest.mkdir(); + File [] files = src.listFiles(); + if (files == null) { + Log.e(TAG, "error in listing directory"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + for(int i = 0; i < files.length; i++) { + if (D) Log.d(TAG,"Files =" + files[i]); + if(files[i].isDirectory()) { + File recdest = new File(dest.getAbsolutePath() + "/" + files[i].getName()); + ret = copyFolders(mCallback,files[i],recdest); + if(ret != ResponseCodes.OBEX_HTTP_OK) + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } else if(files[i].isFile()) { + File recdest = new File(dest.getAbsolutePath() + "/" + files[i].getName()); + ret = copyFile(mCallback,files[i],recdest); + if(ret != ResponseCodes.OBEX_HTTP_OK) + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + return ResponseCodes.OBEX_HTTP_OK; + } + /** + * copyFile + * + * Called when a Copy action is to be performed from a source + * file to destination file + * + * @param mCallback handler for sending message + * @param src File handle to source file + * @param dest File handle to destination file + * @return a ResponseCodes.OBEX_HTTP_OK if operation was succesful + * or ResponseCodes.OBEX_HTTP_INTERNAL_ERROR otherwise + */ + + public static final int copyFile(Handler mCallback,File src, File dest) { + if (D) Log.d(TAG,"copyFile src "+ src +"dest "+dest); + if (dest == null) + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + FileInputStream reader = null; + FileOutputStream writer = null; + interruptFileCopy = false; + + try { + reader = new FileInputStream(src); + writer = new FileOutputStream(dest); + } catch(FileNotFoundException e) { + Log.e(TAG,"copyFile file not found "+ e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } catch(IOException e) { + Log.e(TAG,"copyFile open stream failed "+ e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } finally { + if (null != reader && null == writer) { + try { + reader.close(); + } catch (IOException e) { + Log.e(TAG, "copyFile close stream failed"+ e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + } + + BufferedInputStream ins = new BufferedInputStream(reader, 0x40000); + BufferedOutputStream os = new BufferedOutputStream(writer, 0x40000); + byte[] buff = new byte[0x40000]; + long position = 0; + int readLength = 0; + long timestamp = System.currentTimeMillis(); + try { + if(V) Log.v(TAG,"position = "+position + "src.filelength = "+src.length()); + while ((position != src.length()) && !interruptFileCopy) { + if (BluetoothFtpObexServer.sIsAborted) { + BluetoothFtpObexServer.sIsAborted = false; + break; + } + + readLength = ins.read(buff, 0, 0x40000); + if (D) Log.d(TAG,"Read File"); + os.write(buff, 0, readLength); + position += readLength; + if (V) { + Log.v(TAG, "Copying file position = " + position + + " readLength " + readLength); + } + } + } catch (IOException e) { + Log.e(TAG,"copyFile "+ e.toString()); + if (D) Log.d(TAG, "File Copy failed"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + if (ins != null) { + try { + ins.close(); + os.close(); + } catch (IOException e) { + Log.e(TAG,"input/output stream close" + e.toString()); + if (D) Log.d(TAG, "Error when closing stream after send"); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + + if (position != src.length()) { + Log.i(TAG, "Copy is aborted "); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } else { + Log.i(TAG,"copyFile completed in "+ + (System.currentTimeMillis() - timestamp) + "ms"); + } + + sendMessage(mCallback,BluetoothFtpService.MSG_FILE_RECEIVED,dest.getAbsolutePath()); + return ResponseCodes.OBEX_HTTP_OK; + } + + /** check whether path is legal */ + public static final boolean doesPathExist(final String str) { + if (D) Log.d(TAG,"doesPathExist + = " + str ); + File searchfolder = new File(str); + if(searchfolder.exists()) + return true; + return false; + } + + /** Check the Mounted State of External Storage */ + public static final boolean checkMountedState() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return true; + } else { + if (D) Log.d(TAG,"SD card Media not mounted"); + return false; + } + } + + /** Check the Available Space on External Storage */ + public static final boolean checkAvailableSpace(long filelength) { + StatFs stat = new StatFs(BluetoothFtpObexServer.ROOT_FOLDER_PATH); + if (D) Log.d(TAG,"stat.getAvailableBlocks() "+ stat.getAvailableBlocks()); + if (D) Log.d(TAG,"stat.getBlockSize() ="+ stat.getBlockSize()); + long availabledisksize = stat.getBlockSize() * ((long)stat.getAvailableBlocks() - 4); + if (D) Log.d(TAG,"Disk size = " + availabledisksize + "File length = " + filelength); + if (stat.getBlockSize() * ((long)stat.getAvailableBlocks() - 4) < filelength) { + if (D) Log.d(TAG,"Not Enough Space hence can't receive the file"); + return false; + } else { + return true; + } + } + + /* Send message to FTP Service */ + public static final void sendMessage(Handler mCallback,int msgtype, String name) { + /* Send a message to the FTP service to initiate a Media scanner connection */ + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = msgtype; + Bundle args = new Bundle(); + if(V) Log.e(TAG,"sendMessage "+name); + String path = "/" + "mnt"+ name; + String mimeType = null; + + /* first we look for Mimetype in Android map */ + String extension = null, type = null; + int dotIndex = name.lastIndexOf("."); + if (dotIndex < 0) { + if (D) Log.d(TAG, "There is no file extension"); + return; + } else { + extension = name.substring(dotIndex + 1).toLowerCase(); + MimeTypeMap map = MimeTypeMap.getSingleton(); + type = map.getMimeTypeFromExtension(extension); + if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type); + if (type != null) { + mimeType = type; + } + } + if (mimeType != null) { + mimeType = mimeType.toLowerCase(); + } else { + //Mimetype is unknown hence we dont need a media scan + return; + } + + args.putString("filepath", path); + args.putString("mimetype", mimeType); + msg.obj = args; + msg.sendToTarget(); + if (V) Log.v(TAG,"msg" + msgtype + "sent out."); + } + } + + /* Send custom message to FTP Service */ + public static final void sendCustomMessage(Handler mCallback,int msgtype, String[] files, + String[] types) { + /* Send a message to the FTP service to initiate a Media scanner connection */ + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = msgtype; + Bundle args = new Bundle(); + Log.e(TAG,"sendCustomMessage "); + + args.putStringArray("filepaths", files); + args.putStringArray("mimetypes", types); + msg.obj = args; + msg.sendToTarget(); + if (V) Log.v(TAG,"msg" + msgtype + "sent out."); + } + } + + +}; -- cgit v1.2.3