summaryrefslogtreecommitdiffstats
path: root/src/com/android/nfc/handover/HandoverTransfer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/nfc/handover/HandoverTransfer.java')
-rw-r--r--src/com/android/nfc/handover/HandoverTransfer.java423
1 files changed, 423 insertions, 0 deletions
diff --git a/src/com/android/nfc/handover/HandoverTransfer.java b/src/com/android/nfc/handover/HandoverTransfer.java
new file mode 100644
index 0000000..98b59a6
--- /dev/null
+++ b/src/com/android/nfc/handover/HandoverTransfer.java
@@ -0,0 +1,423 @@
+package com.android.nfc.handover;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Notification.Builder;
+import android.bluetooth.BluetoothDevice;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.nfc.R;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+
+/**
+ * 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.
+ *
+ */
+public class HandoverTransfer implements Handler.Callback,
+ MediaScannerConnection.OnScanCompletedListener {
+
+ interface Callback {
+ void onTransferComplete(HandoverTransfer transfer, boolean success);
+ };
+
+ static final String TAG = "HandoverTransfer";
+
+ static final Boolean DBG = true;
+
+ // In the states below we still accept new file transfer
+ static final int STATE_NEW = 0;
+ static final int STATE_IN_PROGRESS = 1;
+ static final int STATE_W4_NEXT_TRANSFER = 2;
+
+ // In the states below no new files are accepted.
+ 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;
+ static final int MSG_TRANSFER_TIMEOUT = 1;
+
+ // 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;
+
+ // 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 boolean mIncoming; // whether this is an incoming transfer
+ final int mTransferId; // Unique ID of this transfer used for notifications
+ final PendingIntent mCancelIntent;
+ final Context mContext;
+ final Handler mHandler;
+ final NotificationManager mNotificationManager;
+ final BluetoothDevice mRemoteDevice;
+ final Callback mCallback;
+
+ // Variables below are only accessed on the main thread
+ int mState;
+ boolean mCalledBack;
+ Long mLastUpdate; // Last time an event occurred for this transfer
+ float mProgress; // Progress in range [0..1]
+ ArrayList<Uri> mBtUris; // Received uris from Bluetooth OPP
+ ArrayList<String> mBtMimeTypes; // Mime-types received from Bluetooth OPP
+
+ ArrayList<String> mPaths; // Raw paths on the filesystem for Beam-stored files
+ HashMap<String, String> mMimeTypes; // Mime-types associated with each path
+ HashMap<String, Uri> mMediaUris; // URIs found by the media scanner for each path
+ int mUrisScanned;
+
+ public HandoverTransfer(Context context, Callback callback,
+ PendingHandoverTransfer pendingTransfer) {
+ mContext = context;
+ mCallback = callback;
+ mRemoteDevice = pendingTransfer.remoteDevice;
+ mIncoming = pendingTransfer.incoming;
+ mTransferId = pendingTransfer.id;
+ mLastUpdate = SystemClock.elapsedRealtime();
+ mProgress = 0.0f;
+ mState = STATE_NEW;
+ mBtUris = new ArrayList<Uri>();
+ mBtMimeTypes = new ArrayList<String>();
+ mPaths = new ArrayList<String>();
+ mMimeTypes = new HashMap<String, String>();
+ mMediaUris = new HashMap<String, Uri>();
+ mCancelIntent = buildCancelIntent();
+ mUrisScanned = 0;
+
+ mHandler = new Handler(Looper.getMainLooper(), this);
+ mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
+ mNotificationManager = (NotificationManager) mContext.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ }
+
+ void whitelistOppDevice(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
+ Intent intent = new Intent(HandoverManager.ACTION_WHITELIST_DEVICE);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+ }
+
+ public void updateFileProgress(float progress) {
+ if (!isRunning()) return; // Ignore when we're no longer running
+
+ mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
+
+ this.mProgress = progress;
+
+ // We're still receiving data from this device - keep it in
+ // the whitelist for a while longer
+ if (mIncoming) whitelistOppDevice(mRemoteDevice);
+
+ updateStateAndNotification(STATE_IN_PROGRESS);
+ }
+
+ public void finishTransfer(boolean success, Uri uri, String mimeType) {
+ if (!isRunning()) return; // Ignore when we're no longer running
+
+ if (success && uri != null) {
+ if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
+ this.mProgress = 1.0f;
+ if (mimeType == null) {
+ mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri);
+ }
+ if (mimeType != null) {
+ mBtUris.add(uri);
+ mBtMimeTypes.add(mimeType);
+ } else {
+ if (DBG) Log.d(TAG, "Could not get mimeType for file.");
+ }
+ } else {
+ Log.e(TAG, "Handover transfer failed");
+ // Do wait to see if there's another file coming.
+ }
+ mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
+ mHandler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
+ updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
+ }
+
+ public boolean isRunning() {
+ if (mState != STATE_NEW && mState != STATE_IN_PROGRESS && mState != STATE_W4_NEXT_TRANSFER) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ void cancel() {
+ if (!isRunning()) return;
+
+ // Delete all files received so far
+ for (Uri uri : mBtUris) {
+ File file = new File(uri.getPath());
+ if (file.exists()) file.delete();
+ }
+
+ updateStateAndNotification(STATE_CANCELLED);
+ }
+
+ void updateNotification() {
+ if (!mIncoming) return; // No notifications for outgoing transfers
+
+ Builder notBuilder = new Notification.Builder(mContext);
+
+ if (mState == STATE_NEW || mState == STATE_IN_PROGRESS ||
+ mState == STATE_W4_NEXT_TRANSFER || mState == 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.addAction(R.drawable.ic_menu_cancel_holo_dark,
+ mContext.getString(R.string.cancel), mCancelIntent);
+ notBuilder.setDeleteIntent(mCancelIntent);
+ // 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 (mState == STATE_SUCCESS) {
+ notBuilder.setAutoCancel(true);
+ notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+ notBuilder.setTicker(mContext.getString(R.string.beam_complete));
+ notBuilder.setContentTitle(mContext.getString(R.string.beam_complete));
+ notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view));
+
+ Intent viewIntent = buildViewIntent();
+ PendingIntent contentIntent = PendingIntent.getActivity(
+ mContext, 0, viewIntent, 0, null);
+
+ notBuilder.setContentIntent(contentIntent);
+ } else if (mState == STATE_FAILED) {
+ notBuilder.setAutoCancel(false);
+ notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+ notBuilder.setTicker(mContext.getString(R.string.beam_failed));
+ notBuilder.setContentTitle(mContext.getString(R.string.beam_failed));
+ } else if (mState == STATE_CANCELLED) {
+ notBuilder.setAutoCancel(false);
+ notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+ notBuilder.setTicker(mContext.getString(R.string.beam_canceled));
+ notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled));
+ } else {
+ return;
+ }
+
+ mNotificationManager.notify(null, mTransferId, notBuilder.build());
+ }
+
+ void updateStateAndNotification(int newState) {
+ this.mState = newState;
+ this.mLastUpdate = SystemClock.elapsedRealtime();
+
+ if (mHandler.hasMessages(MSG_TRANSFER_TIMEOUT)) {
+ // Update timeout timer
+ mHandler.removeMessages(MSG_TRANSFER_TIMEOUT);
+ mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
+ }
+
+ updateNotification();
+
+ if ((mState == STATE_SUCCESS || mState == STATE_FAILED || mState == STATE_CANCELLED)
+ && !mCalledBack) {
+ mCalledBack = true;
+ // Notify that we're done with this transfer
+ mCallback.onTransferComplete(this, mState == STATE_SUCCESS);
+ }
+ }
+
+ 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) || mBtUris.size() == 0) {
+ Log.e(TAG, "Media storage not valid or no uris received.");
+ updateStateAndNotification(STATE_FAILED);
+ return;
+ }
+
+ if (mBtUris.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 < mBtUris.size(); i++) {
+ Uri uri = mBtUris.get(i);
+ String mimeType = mBtMimeTypes.get(i);
+
+ File srcFile = new File(uri.getPath());
+
+ File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(),
+ uri.getLastPathSegment());
+ if (!srcFile.renameTo(dstFile)) {
+ if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
+ srcFile.delete();
+ return;
+ } else {
+ mPaths.add(dstFile.getAbsolutePath());
+ mMimeTypes.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 = mMimeTypes.get(mPaths.get(0));
+ if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
+ mimeType.startsWith("audio/")) {
+ String[] arrayPaths = new String[mPaths.size()];
+ MediaScannerConnection.scanFile(mContext, mPaths.toArray(arrayPaths), null, this);
+ updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
+ } else {
+ // We're done.
+ updateStateAndNotification(STATE_SUCCESS);
+ }
+
+ }
+
+ public int getTransferId() {
+ return mTransferId;
+ }
+
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
+ // We didn't receive a new transfer in time, finalize this one
+ if (mIncoming) {
+ processFiles();
+ } else {
+ updateStateAndNotification(STATE_SUCCESS);
+ }
+ return true;
+ } else if (msg.what == MSG_TRANSFER_TIMEOUT) {
+ // No update on this transfer for a while, check
+ // to see if it's still running, and fail it if it is.
+ if (isRunning()) {
+ updateStateAndNotification(STATE_FAILED);
+ }
+ }
+ return false;
+ }
+
+ public synchronized void onScanCompleted(String path, Uri uri) {
+ if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
+ if (uri != null) {
+ mMediaUris.put(path, uri);
+ }
+ mUrisScanned++;
+ if (mUrisScanned == mPaths.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;
+ }
+ }
+
+ Intent buildViewIntent() {
+ if (mPaths.size() == 0) return null;
+
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW);
+
+ String filePath = mPaths.get(0);
+ Uri mediaUri = mMediaUris.get(filePath);
+ Uri uri = mediaUri != null ? mediaUri :
+ Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
+ viewIntent.setDataAndTypeAndNormalize(uri, mMimeTypes.get(filePath));
+ viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return viewIntent;
+ }
+
+ PendingIntent buildCancelIntent() {
+ Intent intent = new Intent(HandoverService.ACTION_CANCEL_HANDOVER_TRANSFER);
+ intent.putExtra(HandoverService.EXTRA_SOURCE_ADDRESS, mRemoteDevice.getAddress());
+ PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+
+ return pi;
+ }
+
+ File generateUniqueDestination(String path, String fileName) {
+ int dotIndex = fileName.lastIndexOf(".");
+ String extension = null;
+ String fileNameWithoutExtension = null;
+ if (dotIndex < 0) {
+ extension = "";
+ fileNameWithoutExtension = fileName;
+ } else {
+ extension = fileName.substring(dotIndex);
+ fileNameWithoutExtension = fileName.substring(0, dotIndex);
+ }
+ File dstFile = new File(path + File.separator + fileName);
+ int count = 0;
+ while (dstFile.exists()) {
+ dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" +
+ Integer.toString(count) + extension);
+ count++;
+ }
+ return dstFile;
+ }
+
+ 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;
+ }
+}
+