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