summaryrefslogtreecommitdiffstats
path: root/src/org/codeaurora/bluetooth/ftp
diff options
context:
space:
mode:
authorLinux Build Service Account <lnxbuild@localhost>2013-07-08 13:45:03 -0700
committerGerrit - the friendly Code Review server <code-review@localhost>2013-07-08 13:45:03 -0700
commit77b4192fe6764babf541c23bd9260c8ce5430581 (patch)
tree431830ee9ebc418e058e7df50a97711c0166ac3f /src/org/codeaurora/bluetooth/ftp
parent791a5e91f10ec0e103377634f35cc7df30f48310 (diff)
parent3e0794418cfed9f2bc02a3f8644bb183b5999037 (diff)
downloadandroid_packages_apps_BluetoothExt-77b4192fe6764babf541c23bd9260c8ce5430581.tar.gz
android_packages_apps_BluetoothExt-77b4192fe6764babf541c23bd9260c8ce5430581.tar.bz2
android_packages_apps_BluetoothExt-77b4192fe6764babf541c23bd9260c8ce5430581.zip
Merge "Bluetooth: Support OBEX FTP profile on Bluedroid."
Diffstat (limited to 'src/org/codeaurora/bluetooth/ftp')
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpActivity.java386
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpAuthenticator.java103
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpObexServer.java1096
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpReceiver.java82
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpRfcommTransport.java86
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpService.java909
-rw-r--r--src/org/codeaurora/bluetooth/ftp/BluetoothFtpTransport.java112
-rw-r--r--src/org/codeaurora/bluetooth/ftp/FileUtils.java322
8 files changed, 3096 insertions, 0 deletions
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<String> filenames;
+
+ List<String> 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<String>();
+ types = new ArrayList<String>();
+ }
+ /**
+ * 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("<?xml version=\"1.0\"?>");
+ result.append('\r');
+ result.append('\n');
+ result.append("<!DOCTYPE folder-listing SYSTEM \"obex-folder-listing.dtd\">");
+ result.append('\r');
+ result.append('\n');
+ result.append("<folder-listing version=\"1.0\">");
+ 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("</folder-listing>");
+ 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<BluetoothDevice> trustDevices = new HashSet<BluetoothDevice>();
+ 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<String> filenames = new ArrayList<String>();
+ List<String> types = new ArrayList<String>();
+
+ 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.");
+ }
+ }
+
+
+};