Home | History | Annotate | Download | only in beam
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.nfc.beam;
     18 
     19 import com.android.nfc.R;
     20 
     21 import android.app.Notification;
     22 import android.app.NotificationManager;
     23 import android.app.PendingIntent;
     24 import android.app.Notification.Builder;
     25 import android.bluetooth.BluetoothDevice;
     26 import android.content.ContentResolver;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.media.MediaScannerConnection;
     30 import android.net.Uri;
     31 import android.os.Environment;
     32 import android.os.Handler;
     33 import android.os.Looper;
     34 import android.os.Message;
     35 import android.os.SystemClock;
     36 import android.os.UserHandle;
     37 import android.util.Log;
     38 
     39 import java.io.File;
     40 import java.text.SimpleDateFormat;
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.Date;
     44 import java.util.HashMap;
     45 import java.util.Locale;
     46 
     47 /**
     48  * A BeamTransferManager object represents a set of files
     49  * that were received through NFC connection handover
     50  * from the same source address.
     51  *
     52  * It manages starting, stopping, and processing the transfer, as well
     53  * as the user visible notification.
     54  *
     55  * For Bluetooth, files are received through OPP, and
     56  * we have no knowledge how many files will be transferred
     57  * as part of a single transaction.
     58  * Hence, a transfer has a notion of being "alive": if
     59  * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS
     60  * milliseconds, we consider a new file transfer from the
     61  * same source address as part of the same transfer.
     62  * The corresponding URIs will be grouped in a single folder.
     63  *
     64  * @hide
     65  */
     66 
     67 public class BeamTransferManager implements Handler.Callback,
     68         MediaScannerConnection.OnScanCompletedListener {
     69     interface Callback {
     70 
     71         void onTransferComplete(BeamTransferManager transfer, boolean success);
     72     };
     73     static final String TAG = "BeamTransferManager";
     74 
     75     static final Boolean DBG = true;
     76 
     77     // In the states below we still accept new file transfer
     78     static final int STATE_NEW = 0;
     79     static final int STATE_IN_PROGRESS = 1;
     80     static final int STATE_W4_NEXT_TRANSFER = 2;
     81     // In the states below no new files are accepted.
     82     static final int STATE_W4_MEDIA_SCANNER = 3;
     83     static final int STATE_FAILED = 4;
     84     static final int STATE_SUCCESS = 5;
     85     static final int STATE_CANCELLED = 6;
     86     static final int STATE_CANCELLING = 7;
     87     static final int MSG_NEXT_TRANSFER_TIMER = 0;
     88 
     89     static final int MSG_TRANSFER_TIMEOUT = 1;
     90     static final int DATA_LINK_TYPE_BLUETOOTH = 1;
     91 
     92     // We need to receive an update within this time period
     93     // to still consider this transfer to be "alive" (ie
     94     // a reason to keep the handover transport enabled).
     95     static final int ALIVE_CHECK_MS = 20000;
     96 
     97     // The amount of time to wait for a new transfer
     98     // once the current one completes.
     99     static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000;
    100 
    101     static final String BEAM_DIR = "beam";
    102 
    103     static final String ACTION_WHITELIST_DEVICE =
    104             "android.btopp.intent.action.WHITELIST_DEVICE";
    105 
    106     static final String ACTION_STOP_BLUETOOTH_TRANSFER =
    107             "android.btopp.intent.action.STOP_HANDOVER_TRANSFER";
    108 
    109     final boolean mIncoming;  // whether this is an incoming transfer
    110 
    111     final int mTransferId; // Unique ID of this transfer used for notifications
    112     int mBluetoothTransferId; // ID of this transfer in Bluetooth namespace
    113 
    114     final PendingIntent mCancelIntent;
    115     final Context mContext;
    116     final Handler mHandler;
    117     final NotificationManager mNotificationManager;
    118     final BluetoothDevice mRemoteDevice;
    119     final Callback mCallback;
    120     final boolean mRemoteActivating;
    121 
    122     // Variables below are only accessed on the main thread
    123     int mState;
    124     int mCurrentCount;
    125     int mSuccessCount;
    126     int mTotalCount;
    127     int mDataLinkType;
    128     boolean mCalledBack;
    129     Long mLastUpdate; // Last time an event occurred for this transfer
    130     float mProgress; // Progress in range [0..1]
    131     ArrayList<Uri> mUris; // Received uris from transport
    132     ArrayList<String> mTransferMimeTypes; // Mime-types received from transport
    133     Uri[] mOutgoingUris; // URIs to send
    134     ArrayList<String> mPaths; // Raw paths on the filesystem for Beam-stored files
    135     HashMap<String, String> mMimeTypes; // Mime-types associated with each path
    136     HashMap<String, Uri> mMediaUris; // URIs found by the media scanner for each path
    137     int mUrisScanned;
    138     Long mStartTime;
    139 
    140     public BeamTransferManager(Context context, Callback callback,
    141                                BeamTransferRecord pendingTransfer, boolean incoming) {
    142         mContext = context;
    143         mCallback = callback;
    144         mRemoteDevice = pendingTransfer.remoteDevice;
    145         mIncoming = incoming;
    146         mTransferId = pendingTransfer.id;
    147         mBluetoothTransferId = -1;
    148         mDataLinkType = pendingTransfer.dataLinkType;
    149         mRemoteActivating = pendingTransfer.remoteActivating;
    150         mStartTime = 0L;
    151         // For incoming transfers, count can be set later
    152         mTotalCount = (pendingTransfer.uris != null) ? pendingTransfer.uris.length : 0;
    153         mLastUpdate = SystemClock.elapsedRealtime();
    154         mProgress = 0.0f;
    155         mState = STATE_NEW;
    156         mUris = pendingTransfer.uris == null
    157                 ? new ArrayList<Uri>()
    158                 : new ArrayList<Uri>(Arrays.asList(pendingTransfer.uris));
    159         mTransferMimeTypes = new ArrayList<String>();
    160         mMimeTypes = new HashMap<String, String>();
    161         mPaths = new ArrayList<String>();
    162         mMediaUris = new HashMap<String, Uri>();
    163         mCancelIntent = buildCancelIntent();
    164         mUrisScanned = 0;
    165         mCurrentCount = 0;
    166         mSuccessCount = 0;
    167         mOutgoingUris = pendingTransfer.uris;
    168         mHandler = new Handler(Looper.getMainLooper(), this);
    169         mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
    170         mNotificationManager = (NotificationManager) mContext.getSystemService(
    171                 Context.NOTIFICATION_SERVICE);
    172     }
    173 
    174     void whitelistOppDevice(BluetoothDevice device) {
    175         if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
    176         Intent intent = new Intent(ACTION_WHITELIST_DEVICE);
    177         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
    178         mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
    179     }
    180 
    181     public void start() {
    182         if (mStartTime > 0) {
    183             // already started
    184             return;
    185         }
    186 
    187         mStartTime = System.currentTimeMillis();
    188 
    189         if (!mIncoming) {
    190             if (mDataLinkType == BeamTransferRecord.DATA_LINK_TYPE_BLUETOOTH) {
    191                 new BluetoothOppHandover(mContext, mRemoteDevice, mUris, mRemoteActivating).start();
    192             }
    193         }
    194     }
    195 
    196     public void updateFileProgress(float progress) {
    197         if (!isRunning()) return; // Ignore when we're no longer running
    198 
    199         mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
    200 
    201         this.mProgress = progress;
    202 
    203         // We're still receiving data from this device - keep it in
    204         // the whitelist for a while longer
    205         if (mIncoming && mRemoteDevice != null) whitelistOppDevice(mRemoteDevice);
    206 
    207         updateStateAndNotification(STATE_IN_PROGRESS);
    208     }
    209 
    210     public synchronized void setBluetoothTransferId(int id) {
    211         if (mBluetoothTransferId == -1 && id != -1) {
    212             mBluetoothTransferId = id;
    213             if (mState == STATE_CANCELLING) {
    214                 sendBluetoothCancelIntentAndUpdateState();
    215             }
    216         }
    217     }
    218 
    219     public void finishTransfer(boolean success, Uri uri, String mimeType) {
    220         if (!isRunning()) return; // Ignore when we're no longer running
    221 
    222         mCurrentCount++;
    223         if (success && uri != null) {
    224             mSuccessCount++;
    225             if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
    226             mProgress = 0.0f;
    227             if (mimeType == null) {
    228                 mimeType = MimeTypeUtil.getMimeTypeForUri(mContext, uri);
    229             }
    230             if (mimeType != null) {
    231                 mUris.add(uri);
    232                 mTransferMimeTypes.add(mimeType);
    233             } else {
    234                 if (DBG) Log.d(TAG, "Could not get mimeType for file.");
    235             }
    236         } else {
    237             Log.e(TAG, "Handover transfer failed");
    238             // Do wait to see if there's another file coming.
    239         }
    240         mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
    241         if (mCurrentCount == mTotalCount) {
    242             if (mIncoming) {
    243                 processFiles();
    244             } else {
    245                 updateStateAndNotification(mSuccessCount > 0 ? STATE_SUCCESS : STATE_FAILED);
    246             }
    247         } else {
    248             mHandler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
    249             updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
    250         }
    251     }
    252 
    253     public boolean isRunning() {
    254         if (mState != STATE_NEW && mState != STATE_IN_PROGRESS && mState != STATE_W4_NEXT_TRANSFER) {
    255             return false;
    256         } else {
    257             return true;
    258         }
    259     }
    260 
    261     public void setObjectCount(int objectCount) {
    262         mTotalCount = objectCount;
    263     }
    264 
    265     void cancel() {
    266         if (!isRunning()) return;
    267 
    268         // Delete all files received so far
    269         for (Uri uri : mUris) {
    270             File file = new File(uri.getPath());
    271             if (file.exists()) file.delete();
    272         }
    273 
    274         if (mBluetoothTransferId != -1) {
    275             // we know the ID, we can cancel immediately
    276             sendBluetoothCancelIntentAndUpdateState();
    277         } else {
    278             updateStateAndNotification(STATE_CANCELLING);
    279         }
    280 
    281     }
    282 
    283     private void sendBluetoothCancelIntentAndUpdateState() {
    284         Intent cancelIntent = new Intent(ACTION_STOP_BLUETOOTH_TRANSFER);
    285         cancelIntent.putExtra(BeamStatusReceiver.EXTRA_TRANSFER_ID, mBluetoothTransferId);
    286         mContext.sendBroadcast(cancelIntent);
    287         updateStateAndNotification(STATE_CANCELLED);
    288     }
    289 
    290     void updateNotification() {
    291         Builder notBuilder = new Notification.Builder(mContext);
    292         notBuilder.setColor(mContext.getResources().getColor(
    293                 com.android.internal.R.color.system_notification_accent_color));
    294         notBuilder.setWhen(mStartTime);
    295         notBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);
    296         String beamString;
    297         if (mIncoming) {
    298             beamString = mContext.getString(R.string.beam_progress);
    299         } else {
    300             beamString = mContext.getString(R.string.beam_outgoing);
    301         }
    302         if (mState == STATE_NEW || mState == STATE_IN_PROGRESS ||
    303                 mState == STATE_W4_NEXT_TRANSFER || mState == STATE_W4_MEDIA_SCANNER) {
    304             notBuilder.setAutoCancel(false);
    305             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download :
    306                     android.R.drawable.stat_sys_upload);
    307             notBuilder.setTicker(beamString);
    308             notBuilder.setContentTitle(beamString);
    309             notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark,
    310                     mContext.getString(R.string.cancel), mCancelIntent);
    311             float progress = 0;
    312             if (mTotalCount > 0) {
    313                 float progressUnit = 1.0f / mTotalCount;
    314                 progress = (float) mCurrentCount * progressUnit + mProgress * progressUnit;
    315             }
    316             if (mTotalCount > 0 && progress > 0) {
    317                 notBuilder.setProgress(100, (int) (100 * progress), false);
    318             } else {
    319                 notBuilder.setProgress(100, 0, true);
    320             }
    321         } else if (mState == STATE_SUCCESS) {
    322             notBuilder.setAutoCancel(true);
    323             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download_done :
    324                     android.R.drawable.stat_sys_upload_done);
    325             notBuilder.setTicker(mContext.getString(R.string.beam_complete));
    326             notBuilder.setContentTitle(mContext.getString(R.string.beam_complete));
    327 
    328             if (mIncoming) {
    329                 notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view));
    330                 Intent viewIntent = buildViewIntent();
    331                 PendingIntent contentIntent = PendingIntent.getActivity(
    332                         mContext, mTransferId, viewIntent, 0, null);
    333 
    334                 notBuilder.setContentIntent(contentIntent);
    335             }
    336         } else if (mState == STATE_FAILED) {
    337             notBuilder.setAutoCancel(false);
    338             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download_done :
    339                     android.R.drawable.stat_sys_upload_done);
    340             notBuilder.setTicker(mContext.getString(R.string.beam_failed));
    341             notBuilder.setContentTitle(mContext.getString(R.string.beam_failed));
    342         } else if (mState == STATE_CANCELLED || mState == STATE_CANCELLING) {
    343             notBuilder.setAutoCancel(false);
    344             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download_done :
    345                     android.R.drawable.stat_sys_upload_done);
    346             notBuilder.setTicker(mContext.getString(R.string.beam_canceled));
    347             notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled));
    348         } else {
    349             return;
    350         }
    351 
    352         mNotificationManager.notify(null, mTransferId, notBuilder.build());
    353     }
    354 
    355     void updateStateAndNotification(int newState) {
    356         this.mState = newState;
    357         this.mLastUpdate = SystemClock.elapsedRealtime();
    358 
    359         mHandler.removeMessages(MSG_TRANSFER_TIMEOUT);
    360         if (isRunning()) {
    361             // Update timeout timer if we're still running
    362             mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
    363         }
    364 
    365         updateNotification();
    366 
    367         if ((mState == STATE_SUCCESS || mState == STATE_FAILED || mState == STATE_CANCELLED)
    368                 && !mCalledBack) {
    369             mCalledBack = true;
    370             // Notify that we're done with this transfer
    371             mCallback.onTransferComplete(this, mState == STATE_SUCCESS);
    372         }
    373     }
    374 
    375     void processFiles() {
    376         // Check the amount of files we received in this transfer;
    377         // If more than one, create a separate directory for it.
    378         String extRoot = Environment.getExternalStorageDirectory().getPath();
    379         File beamPath = new File(extRoot + "/" + BEAM_DIR);
    380 
    381         if (!checkMediaStorage(beamPath) || mUris.size() == 0) {
    382             Log.e(TAG, "Media storage not valid or no uris received.");
    383             updateStateAndNotification(STATE_FAILED);
    384             return;
    385         }
    386 
    387         if (mUris.size() > 1) {
    388             beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/");
    389             if (!beamPath.isDirectory() && !beamPath.mkdir()) {
    390                 Log.e(TAG, "Failed to create multiple path " + beamPath.toString());
    391                 updateStateAndNotification(STATE_FAILED);
    392                 return;
    393             }
    394         }
    395 
    396         for (int i = 0; i < mUris.size(); i++) {
    397             Uri uri = mUris.get(i);
    398             String mimeType = mTransferMimeTypes.get(i);
    399 
    400             File srcFile = new File(uri.getPath());
    401 
    402             File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(),
    403                     uri.getLastPathSegment());
    404             Log.d(TAG, "Renaming from " + srcFile);
    405             if (!srcFile.renameTo(dstFile)) {
    406                 if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
    407                 srcFile.delete();
    408                 return;
    409             } else {
    410                 mPaths.add(dstFile.getAbsolutePath());
    411                 mMimeTypes.put(dstFile.getAbsolutePath(), mimeType);
    412                 if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile);
    413             }
    414         }
    415 
    416         // We can either add files to the media provider, or provide an ACTION_VIEW
    417         // intent to the file directly. We base this decision on the mime type
    418         // of the first file; if it's media the platform can deal with,
    419         // use the media provider, if it's something else, just launch an ACTION_VIEW
    420         // on the file.
    421         String mimeType = mMimeTypes.get(mPaths.get(0));
    422         if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
    423                 mimeType.startsWith("audio/")) {
    424             String[] arrayPaths = new String[mPaths.size()];
    425             MediaScannerConnection.scanFile(mContext, mPaths.toArray(arrayPaths), null, this);
    426             updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
    427         } else {
    428             // We're done.
    429             updateStateAndNotification(STATE_SUCCESS);
    430         }
    431 
    432     }
    433 
    434     public boolean handleMessage(Message msg) {
    435         if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
    436             // We didn't receive a new transfer in time, finalize this one
    437             if (mIncoming) {
    438                 processFiles();
    439             } else {
    440                 updateStateAndNotification(mSuccessCount > 0 ? STATE_SUCCESS : STATE_FAILED);
    441             }
    442             return true;
    443         } else if (msg.what == MSG_TRANSFER_TIMEOUT) {
    444             // No update on this transfer for a while, fail it.
    445             if (DBG) Log.d(TAG, "Transfer timed out for id: " + Integer.toString(mTransferId));
    446             updateStateAndNotification(STATE_FAILED);
    447         }
    448         return false;
    449     }
    450 
    451     public synchronized void onScanCompleted(String path, Uri uri) {
    452         if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
    453         if (uri != null) {
    454             mMediaUris.put(path, uri);
    455         }
    456         mUrisScanned++;
    457         if (mUrisScanned == mPaths.size()) {
    458             // We're done
    459             updateStateAndNotification(STATE_SUCCESS);
    460         }
    461     }
    462 
    463 
    464     Intent buildViewIntent() {
    465         if (mPaths.size() == 0) return null;
    466 
    467         Intent viewIntent = new Intent(Intent.ACTION_VIEW);
    468 
    469         String filePath = mPaths.get(0);
    470         Uri mediaUri = mMediaUris.get(filePath);
    471         Uri uri =  mediaUri != null ? mediaUri :
    472             Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
    473         viewIntent.setDataAndTypeAndNormalize(uri, mMimeTypes.get(filePath));
    474         viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    475         return viewIntent;
    476     }
    477 
    478     PendingIntent buildCancelIntent() {
    479         Intent intent = new Intent(BeamStatusReceiver.ACTION_CANCEL_HANDOVER_TRANSFER);
    480         intent.putExtra(BeamStatusReceiver.EXTRA_ADDRESS, mRemoteDevice.getAddress());
    481         intent.putExtra(BeamStatusReceiver.EXTRA_INCOMING, mIncoming ?
    482                 BeamStatusReceiver.DIRECTION_INCOMING : BeamStatusReceiver.DIRECTION_OUTGOING);
    483         PendingIntent pi = PendingIntent.getBroadcast(mContext, mTransferId, intent,
    484                 PendingIntent.FLAG_ONE_SHOT);
    485 
    486         return pi;
    487     }
    488 
    489     static boolean checkMediaStorage(File path) {
    490         if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    491             if (!path.isDirectory() && !path.mkdir()) {
    492                 Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath());
    493                 return false;
    494             }
    495             return true;
    496         } else {
    497             Log.e(TAG, "External storage not mounted, can't store file.");
    498             return false;
    499         }
    500     }
    501 
    502     static File generateUniqueDestination(String path, String fileName) {
    503         int dotIndex = fileName.lastIndexOf(".");
    504         String extension = null;
    505         String fileNameWithoutExtension = null;
    506         if (dotIndex < 0) {
    507             extension = "";
    508             fileNameWithoutExtension = fileName;
    509         } else {
    510             extension = fileName.substring(dotIndex);
    511             fileNameWithoutExtension = fileName.substring(0, dotIndex);
    512         }
    513         File dstFile = new File(path + File.separator + fileName);
    514         int count = 0;
    515         while (dstFile.exists()) {
    516             dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" +
    517                     Integer.toString(count) + extension);
    518             count++;
    519         }
    520         return dstFile;
    521     }
    522 
    523     static File generateMultiplePath(String beamRoot) {
    524         // Generate a unique directory with the date
    525         String format = "yyyy-MM-dd";
    526         SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
    527         String newPath = beamRoot + "beam-" + sdf.format(new Date());
    528         File newFile = new File(newPath);
    529         int count = 0;
    530         while (newFile.exists()) {
    531             newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" +
    532                     Integer.toString(count);
    533             newFile = new File(newPath);
    534             count++;
    535         }
    536         return newFile;
    537     }
    538 }
    539 
    540