Merge "Bluetooth Beam handover: deal with multi-file transfers." into jb-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3d65a2a..f0c027f 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.VIBRATE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.permission.WHITELIST_BLUETOOTH_DEVICE" />
<uses-permission android:name="com.android.permission.HANDOVER_STATUS" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8d25943..06cc01c 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -19,7 +19,7 @@
<string name="accessibility_nfc_enabled">NFC enabled.</string>
<string name="touch">Touch to beam</string>
- <string name="beam_progress">Beam in progress</string>
+ <string name="beam_progress">Incoming beam...</string>
<string name="beam_complete">Beam complete</string>
<string name="beam_failed">Beam failed</string>
<string name="beam_touch_to_view">Touch to view</string>
diff --git a/src/com/android/nfc/handover/BluetoothOppHandover.java b/src/com/android/nfc/handover/BluetoothOppHandover.java
index 325691d..1835aa8 100644
--- a/src/com/android/nfc/handover/BluetoothOppHandover.java
+++ b/src/com/android/nfc/handover/BluetoothOppHandover.java
@@ -60,7 +60,9 @@
mState = STATE_INIT;
}
- public static String getMimeTypeForUri(Context context, Uri uri) {
+ public static String getMimeTypeForUri(Context context, Uri uri) {
+ if (uri.getScheme() == null) return null;
+
if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
ContentResolver cr = context.getContentResolver();
return cr.getType(uri);
diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java
index 6ebfc2d..79bcef1 100644
--- a/src/com/android/nfc/handover/HandoverManager.java
+++ b/src/com/android/nfc/handover/HandoverManager.java
@@ -20,7 +20,10 @@
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@@ -36,20 +39,21 @@
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.media.MediaScannerConnection;
import android.net.Uri;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
+import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
-import android.widget.Toast;
-
import com.android.nfc.NfcService;
import com.android.nfc.R;
@@ -75,6 +79,12 @@
static final String EXTRA_BT_OPP_TRANSFER_STATUS =
"android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS";
+ static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE =
+ "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE";
+
+ static final String EXTRA_BT_OPP_ADDRESS =
+ "android.btopp.intent.extra.BT_OPP_ADDRESS";
+
static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0;
static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1;
@@ -129,7 +139,7 @@
String mLocalBluetoothAddress;
int mNotificationId;
- HashMap<Pair<Integer, Integer>, HandoverTransfer> mTransfers;
+ HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers;
static class BluetoothHandoverData {
public boolean valid = false;
@@ -152,7 +162,7 @@
* when there is no Bluetooth activity intitiated by NFC
* anymore.
*/
- boolean enableBluetooth() {
+ synchronized boolean enableBluetooth() {
// Enable BT
boolean result = mBluetoothAdapter.enableNoAutoConnect();
@@ -164,10 +174,17 @@
return result;
}
- boolean isBluetoothEnabled() {
+ synchronized boolean isBluetoothEnabled() {
return mBluetoothAdapter.isEnabled();
}
+ synchronized void resetTimer() {
+ if (handler.hasMessages(MSG_HANDOVER_POWER_CHECK)) {
+ handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
+ handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS);
+ }
+ }
+
void stopMonitoring() {
handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
}
@@ -198,57 +215,122 @@
}
}
- class HandoverTransfer {
+ /**
+ * A HandoverTransfer object represents a set of files
+ * that were received through NFC connection handover
+ * from the same source address.
+ *
+ * For Bluetooth, files are received through OPP, and
+ * we have no knowledge how many files will be transferred
+ * as part of a single transaction.
+ * Hence, a transfer has a notion of being "alive": if
+ * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS
+ * milliseconds, we consider a new file transfer from the
+ * same source address as part of the same transfer.
+ * The corresponding URIs will be grouped in a single folder.
+ *
+ */
+ class HandoverTransfer implements Handler.Callback,
+ MediaScannerConnection.OnScanCompletedListener {
static final int STATE_NEW = 0;
static final int STATE_IN_PROGRESS = 1;
- static final int STATE_FAILED = 2;
- static final int STATE_SUCCESS = 3;
+ static final int STATE_W4_NEXT_TRANSFER = 2;
+ static final int STATE_W4_MEDIA_SCANNER = 3;
+ static final int STATE_FAILED = 4;
+ static final int STATE_SUCCESS = 5;
+ static final int STATE_CANCELLED = 6;
+
+ static final int MSG_NEXT_TRANSFER_TIMER = 0;
// We need to receive an update within this time period
// to still consider this transfer to be "alive" (ie
// a reason to keep the handover transport enabled).
static final int ALIVE_CHECK_MS = 20000;
- int notificationId; // Unique ID of this transfer used for notifications
+ // The amount of time to wait for a new transfer
+ // once the current one completes.
+ static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000;
+
+ static final String BEAM_DIR = "beam";
+
+ final BluetoothDevice device;
+ final String sourceAddress;
+ final boolean incoming; // whether this is an incoming transfer
+ final int notificationId; // Unique ID of this transfer used for notifications
+ final Handler handler;
+
+ int state;
Long lastUpdate; // Last time an event occurred for this transfer
float progress; // Progress in range [0..1]
- int state;
- Uri uri;
- boolean incoming; // whether this is an incoming transfer
+ ArrayList<Uri> btUris; // Received uris from Bluetooth OPP
+ ArrayList<String> btMimeTypes; // Mime-types received from Bluetooth OPP
- public HandoverTransfer(boolean incoming) {
+ ArrayList<String> paths; // Raw paths on the filesystem for Beam-stored files
+ HashMap<String, String> mimeTypes; // Mime-types associated with each path
+ HashMap<String, Uri> mediaUris; // URIs found by the media scanner for each path
+ int urisScanned;
+
+ public HandoverTransfer(String sourceAddress, boolean incoming) {
synchronized (HandoverManager.this) {
this.notificationId = mNotificationId++;
}
this.lastUpdate = SystemClock.elapsedRealtime();
this.progress = 0.0f;
this.state = STATE_NEW;
- this.uri = null;
+ this.btUris = new ArrayList<Uri>();
+ this.btMimeTypes = new ArrayList<String>();
+ this.paths = new ArrayList<String>();
+ this.mimeTypes = new HashMap<String, String>();
+ this.mediaUris = new HashMap<String, Uri>();
+ this.sourceAddress = sourceAddress;
this.incoming = incoming;
+ this.handler = new Handler(mContext.getMainLooper(), this);
+ this.urisScanned = 0;
+ this.device = mBluetoothAdapter.getRemoteDevice(sourceAddress);
}
- synchronized void updateTransferProgress(float progress) {
- this.state = STATE_IN_PROGRESS;
+ public synchronized void updateFileProgress(float progress) {
+ if (!isRunning()) return; // Ignore when we're no longer running
+
+ handler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
+
this.progress = progress;
- this.lastUpdate = SystemClock.elapsedRealtime();
- updateNotification();
+ // We're still receiving data from this device - keep it in
+ // the whitelist for a while longer
+ if (incoming) whitelistOppDevice(device);
+
+ updateStateAndNotification(STATE_IN_PROGRESS);
}
- synchronized void finishTransfer(boolean success, Uri uri) {
+ public synchronized void finishTransfer(boolean success, Uri uri, String mimeType) {
+ if (!isRunning()) return; // Ignore when we're no longer running
+
if (success && uri != null) {
- this.state = STATE_SUCCESS;
- this.uri = uri;
+ if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
+ this.progress = 1.0f;
+ if (mimeType == null) {
+ mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri);
+ }
+ if (mimeType != null) {
+ btUris.add(uri);
+ btMimeTypes.add(mimeType);
+ } else {
+ if (DBG) Log.d(TAG, "Could not get mimeType for file.");
+ }
} else {
- this.state = STATE_FAILED;
+ Log.e(TAG, "Handover transfer failed");
+ // Do wait to see if there's another file coming.
}
- this.lastUpdate = SystemClock.elapsedRealtime();
-
- updateNotification();
+ handler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
+ handler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
+ updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
}
- synchronized boolean isRunning() {
- if (state != STATE_IN_PROGRESS) return false;
+ public synchronized boolean isRunning() {
+ if (state != STATE_NEW && state != STATE_IN_PROGRESS && state != STATE_W4_NEXT_TRANSFER) {
+ return false;
+ }
// Check that we've made progress
Long currentTime = SystemClock.elapsedRealtime();
@@ -264,13 +346,16 @@
Builder notBuilder = new Notification.Builder(mContext);
- if (state == STATE_IN_PROGRESS) {
- int progressInt = (int) (progress * 100);
+ if (state == STATE_NEW || state == STATE_IN_PROGRESS ||
+ state == STATE_W4_NEXT_TRANSFER || state == STATE_W4_MEDIA_SCANNER) {
notBuilder.setAutoCancel(false);
notBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
notBuilder.setTicker(mContext.getString(R.string.beam_progress));
notBuilder.setContentTitle(mContext.getString(R.string.beam_progress));
- notBuilder.setProgress(100, progressInt, progress == -1);
+ // We do have progress indication on a per-file basis, but in a multi-file
+ // transfer we don't know the total progress. So for now, just show an
+ // indeterminate progress bar.
+ notBuilder.setProgress(100, 0, true);
} else if (state == STATE_SUCCESS) {
notBuilder.setAutoCancel(true);
notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
@@ -278,10 +363,8 @@
notBuilder.setContentTitle(mContext.getString(R.string.beam_complete));
notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view));
- Intent notificationIntent = new Intent(Intent.ACTION_VIEW);
- String mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri);
- notificationIntent.setDataAndType(uri, mimeType);
- PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, notificationIntent, 0);
+ Intent viewIntent = buildViewIntent();
+ PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, viewIntent, 0);
notBuilder.setContentIntent(contentIntent);
@@ -298,21 +381,151 @@
mNotificationManager.notify(mNotificationId, notBuilder.build());
}
+
+ synchronized void updateStateAndNotification(int newState) {
+ this.state = newState;
+ this.lastUpdate = SystemClock.elapsedRealtime();
+ updateNotification();
+ }
+
+ synchronized void processFiles() {
+ // Check the amount of files we received in this transfer;
+ // If more than one, create a separate directory for it.
+ String extRoot = Environment.getExternalStorageDirectory().getPath();
+ File beamPath = new File(extRoot + "/" + BEAM_DIR);
+
+ if (!checkMediaStorage(beamPath) || btUris.size() == 0) {
+ Log.e(TAG, "Media storage not valid or no uris received.");
+ updateStateAndNotification(STATE_FAILED);
+ return;
+ }
+
+ if (btUris.size() > 1) {
+ beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/");
+ if (!beamPath.isDirectory() && !beamPath.mkdir()) {
+ Log.e(TAG, "Failed to create multiple path " + beamPath.toString());
+ updateStateAndNotification(STATE_FAILED);
+ return;
+ }
+ }
+
+ for (int i = 0; i < btUris.size(); i++) {
+ Uri uri = btUris.get(i);
+ String mimeType = btMimeTypes.get(i);
+
+ File srcFile = new File(uri.getPath());
+ File dstFile = new File(beamPath + "/" + uri.getLastPathSegment());
+ if (!srcFile.renameTo(dstFile)) {
+ if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
+ srcFile.delete();
+ return;
+ } else {
+ paths.add(dstFile.getAbsolutePath());
+ mimeTypes.put(dstFile.getAbsolutePath(), mimeType);
+ if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile);
+ }
+ }
+
+ // We can either add files to the media provider, or provide an ACTION_VIEW
+ // intent to the file directly. We base this decision on the mime type
+ // of the first file; if it's media the platform can deal with,
+ // use the media provider, if it's something else, just launch an ACTION_VIEW
+ // on the file.
+ String mimeType = mimeTypes.get(paths.get(0));
+ if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
+ mimeType.startsWith("audio/")) {
+ String[] arrayPaths = new String[paths.size()];
+ MediaScannerConnection.scanFile(mContext, paths.toArray(arrayPaths), null, this);
+ updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
+ } else {
+ // We're done.
+ updateStateAndNotification(STATE_SUCCESS);
+ }
+
+ }
+
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
+ // We didn't receive a new transfer in time, finalize this one
+ processFiles();
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized void onScanCompleted(String path, Uri uri) {
+ if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
+ if (uri != null) {
+ mediaUris.put(path, uri);
+ }
+ urisScanned++;
+ if (urisScanned == paths.size()) {
+ // We're done
+ updateStateAndNotification(STATE_SUCCESS);
+ }
+ }
+
+ boolean checkMediaStorage(File path) {
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ if (!path.isDirectory() && !path.mkdir()) {
+ Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath());
+ return false;
+ }
+ return true;
+ } else {
+ Log.e(TAG, "External storage not mounted, can't store file.");
+ return false;
+ }
+ }
+
+ synchronized Intent buildViewIntent() {
+ if (paths.size() == 0) return null;
+
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW);
+
+ String filePath = paths.get(0);
+ Uri mediaUri = mediaUris.get(filePath);
+ Uri uri = mediaUri != null ? mediaUri :
+ Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
+ viewIntent.setDataAndTypeAndNormalize(uri, mimeTypes.get(filePath));
+
+ return viewIntent;
+ }
+
+ synchronized File generateMultiplePath(String beamRoot) {
+ // Generate a unique directory with the date
+ String format = "yyyy-MM-dd";
+ SimpleDateFormat sdf = new SimpleDateFormat(format);
+ String newPath = beamRoot + "beam-" + sdf.format(new Date());
+ File newFile = new File(newPath);
+ int count = 0;
+ while (newFile.exists()) {
+ newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" +
+ Integer.toString(count);
+ newFile = new File(newPath);
+ count++;
+ }
+
+ return newFile;
+ }
+
+
}
- synchronized HandoverTransfer getHandoverTransfer(int source, int id) {
- Pair<Integer, Integer> key = new Pair<Integer, Integer>(source,id);
- if (!mTransfers.containsKey(key)) {
- boolean incoming = false;
- if (source == SOURCE_BLUETOOTH_INCOMING) {
- incoming = true;
+ synchronized HandoverTransfer getHandoverTransfer(String sourceAddress, boolean incoming) {
+ Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming);
+ if (mTransfers.containsKey(key)) {
+ HandoverTransfer transfer = mTransfers.get(key);
+ if (transfer.isRunning()) {
+ return transfer;
+ } else {
+ // Remove old transfer; new one will be created below
+ mTransfers.remove(key);
}
- HandoverTransfer transfer = new HandoverTransfer(incoming);
- mTransfers.put(key, transfer);
- return transfer;
- } else {
- return mTransfers.get(key);
}
+ HandoverTransfer transfer = new HandoverTransfer(sourceAddress, incoming);
+ mTransfers.put(key, transfer);
+ return transfer;
}
public HandoverManager(Context context) {
@@ -324,7 +537,7 @@
mNotificationManager = (NotificationManager) mContext.getSystemService(
Context.NOTIFICATION_SERVICE);
- mTransfers = new HashMap<Pair<Integer, Integer>, HandoverTransfer>();
+ mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>();
mHandoverPowerManager = new HandoverPowerManager(context);
IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE);
@@ -334,9 +547,9 @@
}
synchronized void cleanupTransfers() {
- Iterator<Map.Entry<Pair<Integer, Integer>, HandoverTransfer>> it = mTransfers.entrySet().iterator();
+ Iterator<Map.Entry<Pair<String, Boolean>, HandoverTransfer>> it = mTransfers.entrySet().iterator();
while (it.hasNext()) {
- Map.Entry<Pair<Integer, Integer>, HandoverTransfer> pair = it.next();
+ Map.Entry<Pair<String, Boolean>, HandoverTransfer> pair = it.next();
HandoverTransfer transfer = pair.getValue();
if (!transfer.isRunning()) {
it.remove();
@@ -442,12 +655,20 @@
boolean bluetoothActivating = false;
- if (!mHandoverPowerManager.isBluetoothEnabled()) {
- if (!mHandoverPowerManager.enableBluetooth()) {
- toast(mContext.getString(R.string.beam_failed));
- return null;
+ synchronized(HandoverManager.this) {
+ if (!mHandoverPowerManager.isBluetoothEnabled()) {
+ if (!mHandoverPowerManager.enableBluetooth()) {
+ return null;
+ }
+ bluetoothActivating = true;
+ } else {
+ mHandoverPowerManager.resetTimer();
}
- bluetoothActivating = true;
+
+ // Create the initial transfer object
+ HandoverTransfer transfer = getHandoverTransfer(bluetoothData.device.getAddress(),
+ true);
+ transfer.updateNotification();
}
// BT OOB found, whitelist it for incoming OPP data
@@ -695,10 +916,6 @@
}
}
- void toast(CharSequence text) {
- Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
- }
-
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -718,10 +935,11 @@
int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1);
int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1);
- if (direction == -1 || id == -1) return;
- int source = (direction == DIRECTION_BLUETOOTH_INCOMING) ?
- SOURCE_BLUETOOTH_INCOMING : SOURCE_BLUETOOTH_OUTGOING;
- HandoverTransfer transfer = getHandoverTransfer(source, id);
+ String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS);
+
+ if (direction == -1 || id == -1 || sourceAddress == null) return;
+ boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING);
+ HandoverTransfer transfer = getHandoverTransfer(sourceAddress, incoming);
if (transfer == null) return;
if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) {
@@ -730,17 +948,18 @@
if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) {
String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI);
+ String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE);
Uri uri = Uri.parse(uriString);
if (uri.getScheme() == null) {
uri = Uri.fromFile(new File(uri.getPath()));
}
- transfer.finishTransfer(true, uri);
+ transfer.finishTransfer(true, uri, mimeType);
} else {
- transfer.finishTransfer(false, null);
+ transfer.finishTransfer(false, null, null);
}
} else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) {
float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f);
- transfer.updateTransferProgress(progress);
+ transfer.updateFileProgress(progress);
}
}
}