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 
     66         void onTransferComplete(HandoverTransfer transfer, boolean success);
     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 
     75     static final int STATE_IN_PROGRESS = 1;
     76     static final int STATE_W4_NEXT_TRANSFER = 2;
     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     static final int STATE_CANCELLING = 7;
     83     static final int MSG_NEXT_TRANSFER_TIMER = 0;
     84 
     85     static final int MSG_TRANSFER_TIMEOUT = 1;
     86     static final int DEVICE_TYPE_BLUETOOTH = 1;
     87 
     88     public static final int DEVICE_TYPE_WIFI = 2;
     89     // We need to receive an update within this time period
     90     // to still consider this transfer to be "alive" (ie
     91     // a reason to keep the handover transport enabled).
     92     static final int ALIVE_CHECK_MS = 20000;
     93 
     94     // The amount of time to wait for a new transfer
     95     // once the current one completes.
     96     static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000;
     97 
     98     static final String BEAM_DIR = "beam";
     99 
    100     final boolean mIncoming;  // whether this is an incoming transfer
    101 
    102     final int mTransferId; // Unique ID of this transfer used for notifications
    103     int mBluetoothTransferId; // ID of this transfer in Bluetooth namespace
    104 
    105     final PendingIntent mCancelIntent;
    106     final Context mContext;
    107     final Handler mHandler;
    108     final NotificationManager mNotificationManager;
    109     final BluetoothDevice mRemoteDevice;
    110     final String mRemoteMac;
    111     final Callback mCallback;
    112     final Long mStartTime;
    113 
    114     // Variables below are only accessed on the main thread
    115     int mState;
    116     int mCurrentCount;
    117     int mSuccessCount;
    118     int mTotalCount;
    119     int mDeviceType;
    120     boolean mCalledBack;
    121     Long mLastUpdate; // Last time an event occurred for this transfer
    122     float mProgress; // Progress in range [0..1]
    123     ArrayList<Uri> mUris; // Received uris from transport
    124     ArrayList<String> mTransferMimeTypes; // Mime-types received from transport
    125     Uri[] mOutgoingUris; // URIs to send via Wifi Direct
    126 
    127     ArrayList<String> mPaths; // Raw paths on the filesystem for Beam-stored files
    128     HashMap<String, String> mMimeTypes; // Mime-types associated with each path
    129     HashMap<String, Uri> mMediaUris; // URIs found by the media scanner for each path
    130     int mUrisScanned;
    131 
    132     public HandoverTransfer(Context context, Callback callback,
    133             PendingHandoverTransfer pendingTransfer) {
    134         mContext = context;
    135         mCallback = callback;
    136         mRemoteDevice = pendingTransfer.remoteDevice;
    137         mRemoteMac = pendingTransfer.remoteMacAddress;
    138         mIncoming = pendingTransfer.incoming;
    139         mTransferId = pendingTransfer.id;
    140         mBluetoothTransferId = -1;
    141         mDeviceType = pendingTransfer.deviceType;
    142         // For incoming transfers, count can be set later
    143         mTotalCount = (pendingTransfer.uris != null) ? pendingTransfer.uris.length : 0;
    144         mLastUpdate = SystemClock.elapsedRealtime();
    145         mProgress = 0.0f;
    146         mState = STATE_NEW;
    147         mUris = new ArrayList<Uri>();
    148         mTransferMimeTypes = new ArrayList<String>();
    149         mMimeTypes = new HashMap<String, String>();
    150         mPaths = new ArrayList<String>();
    151         mMediaUris = new HashMap<String, Uri>();
    152         mCancelIntent = buildCancelIntent(mIncoming);
    153         mUrisScanned = 0;
    154         mCurrentCount = 0;
    155         mSuccessCount = 0;
    156         mOutgoingUris = pendingTransfer.uris;
    157         mHandler = new Handler(Looper.getMainLooper(), this);
    158         mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
    159         mNotificationManager = (NotificationManager) mContext.getSystemService(
    160                 Context.NOTIFICATION_SERVICE);
    161 
    162         mStartTime = System.currentTimeMillis();
    163     }
    164 
    165     void whitelistOppDevice(BluetoothDevice device) {
    166         if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
    167         Intent intent = new Intent(HandoverManager.ACTION_WHITELIST_DEVICE);
    168         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
    169         mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
    170     }
    171 
    172     public void updateFileProgress(float progress) {
    173         if (!isRunning()) return; // Ignore when we're no longer running
    174 
    175         mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
    176 
    177         this.mProgress = progress;
    178 
    179         // We're still receiving data from this device - keep it in
    180         // the whitelist for a while longer
    181         if (mIncoming && mRemoteDevice != null) whitelistOppDevice(mRemoteDevice);
    182 
    183         updateStateAndNotification(STATE_IN_PROGRESS);
    184     }
    185 
    186     public synchronized void setBluetoothTransferId(int id) {
    187         if (mBluetoothTransferId == -1 && id != -1) {
    188             mBluetoothTransferId = id;
    189             if (mState == STATE_CANCELLING) {
    190                 sendBluetoothCancelIntentAndUpdateState();
    191             }
    192         }
    193     }
    194 
    195     public void finishTransfer(boolean success, Uri uri, String mimeType) {
    196         if (!isRunning()) return; // Ignore when we're no longer running
    197 
    198         mCurrentCount++;
    199         if (success && uri != null) {
    200             mSuccessCount++;
    201             if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
    202             mProgress = 0.0f;
    203             if (mimeType == null) {
    204                 mimeType = MimeTypeUtil.getMimeTypeForUri(mContext, uri);
    205             }
    206             if (mimeType != null) {
    207                 mUris.add(uri);
    208                 mTransferMimeTypes.add(mimeType);
    209             } else {
    210                 if (DBG) Log.d(TAG, "Could not get mimeType for file.");
    211             }
    212         } else {
    213             Log.e(TAG, "Handover transfer failed");
    214             // Do wait to see if there's another file coming.
    215         }
    216         mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
    217         if (mCurrentCount == mTotalCount) {
    218             if (mIncoming) {
    219                 processFiles();
    220             } else {
    221                 Log.i(TAG, "Updating state!");
    222                 updateStateAndNotification(mSuccessCount > 0 ? STATE_SUCCESS : STATE_FAILED);
    223             }
    224         } else {
    225             mHandler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
    226             updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
    227         }
    228     }
    229 
    230     public boolean isRunning() {
    231         if (mState != STATE_NEW && mState != STATE_IN_PROGRESS && mState != STATE_W4_NEXT_TRANSFER) {
    232             return false;
    233         } else {
    234             return true;
    235         }
    236     }
    237 
    238     public void setObjectCount(int objectCount) {
    239         mTotalCount = objectCount;
    240     }
    241 
    242     void cancel() {
    243         if (!isRunning()) return;
    244 
    245         // Delete all files received so far
    246         for (Uri uri : mUris) {
    247             File file = new File(uri.getPath());
    248             if (file.exists()) file.delete();
    249         }
    250 
    251         if (mBluetoothTransferId != -1) {
    252             // we know the ID, we can cancel immediately
    253             sendBluetoothCancelIntentAndUpdateState();
    254         } else {
    255             updateStateAndNotification(STATE_CANCELLING);
    256         }
    257 
    258     }
    259 
    260     private void sendBluetoothCancelIntentAndUpdateState() {
    261         Intent cancelIntent = new Intent(
    262                 "android.btopp.intent.action.STOP_HANDOVER_TRANSFER");
    263         cancelIntent.putExtra(HandoverService.EXTRA_TRANSFER_ID, mBluetoothTransferId);
    264         mContext.sendBroadcast(cancelIntent);
    265         updateStateAndNotification(STATE_CANCELLED);
    266     }
    267 
    268     void updateNotification() {
    269         Builder notBuilder = new Notification.Builder(mContext);
    270         notBuilder.setColor(mContext.getResources().getColor(
    271                 com.android.internal.R.color.system_notification_accent_color));
    272         notBuilder.setWhen(mStartTime);
    273         notBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);
    274         String beamString;
    275         if (mIncoming) {
    276             beamString = mContext.getString(R.string.beam_progress);
    277         } else {
    278             beamString = mContext.getString(R.string.beam_outgoing);
    279         }
    280         if (mState == STATE_NEW || mState == STATE_IN_PROGRESS ||
    281                 mState == STATE_W4_NEXT_TRANSFER || mState == STATE_W4_MEDIA_SCANNER) {
    282             notBuilder.setAutoCancel(false);
    283             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download :
    284                     android.R.drawable.stat_sys_upload);
    285             notBuilder.setTicker(beamString);
    286             notBuilder.setContentTitle(beamString);
    287             notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark,
    288                     mContext.getString(R.string.cancel), mCancelIntent);
    289             float progress = 0;
    290             if (mTotalCount > 0) {
    291                 float progressUnit = 1.0f / mTotalCount;
    292                 progress = (float) mCurrentCount * progressUnit + mProgress * progressUnit;
    293             }
    294             if (mTotalCount > 0 && progress > 0) {
    295                 notBuilder.setProgress(100, (int) (100 * progress), false);
    296             } else {
    297                 notBuilder.setProgress(100, 0, true);
    298             }
    299         } else if (mState == STATE_SUCCESS) {
    300             notBuilder.setAutoCancel(true);
    301             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download_done :
    302                     android.R.drawable.stat_sys_upload_done);
    303             notBuilder.setTicker(mContext.getString(R.string.beam_complete));
    304             notBuilder.setContentTitle(mContext.getString(R.string.beam_complete));
    305 
    306             if (mIncoming) {
    307                 notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view));
    308                 Intent viewIntent = buildViewIntent();
    309                 PendingIntent contentIntent = PendingIntent.getActivity(
    310                         mContext, mTransferId, viewIntent, 0, null);
    311 
    312                 notBuilder.setContentIntent(contentIntent);
    313             }
    314         } else if (mState == STATE_FAILED) {
    315             notBuilder.setAutoCancel(false);
    316             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download_done :
    317                     android.R.drawable.stat_sys_upload_done);
    318             notBuilder.setTicker(mContext.getString(R.string.beam_failed));
    319             notBuilder.setContentTitle(mContext.getString(R.string.beam_failed));
    320         } else if (mState == STATE_CANCELLED || mState == STATE_CANCELLING) {
    321             notBuilder.setAutoCancel(false);
    322             notBuilder.setSmallIcon(mIncoming ? android.R.drawable.stat_sys_download_done :
    323                     android.R.drawable.stat_sys_upload_done);
    324             notBuilder.setTicker(mContext.getString(R.string.beam_canceled));
    325             notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled));
    326         } else {
    327             return;
    328         }
    329 
    330         mNotificationManager.notify(null, mTransferId, notBuilder.build());
    331     }
    332 
    333     void updateStateAndNotification(int newState) {
    334         this.mState = newState;
    335         this.mLastUpdate = SystemClock.elapsedRealtime();
    336 
    337         mHandler.removeMessages(MSG_TRANSFER_TIMEOUT);
    338         if (isRunning()) {
    339             // Update timeout timer if we're still running
    340             mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
    341         }
    342 
    343         updateNotification();
    344 
    345         if ((mState == STATE_SUCCESS || mState == STATE_FAILED || mState == STATE_CANCELLED)
    346                 && !mCalledBack) {
    347             mCalledBack = true;
    348             // Notify that we're done with this transfer
    349             mCallback.onTransferComplete(this, mState == STATE_SUCCESS);
    350         }
    351     }
    352 
    353     void processFiles() {
    354         // Check the amount of files we received in this transfer;
    355         // If more than one, create a separate directory for it.
    356         String extRoot = Environment.getExternalStorageDirectory().getPath();
    357         File beamPath = new File(extRoot + "/" + BEAM_DIR);
    358 
    359         if (!checkMediaStorage(beamPath) || mUris.size() == 0) {
    360             Log.e(TAG, "Media storage not valid or no uris received.");
    361             updateStateAndNotification(STATE_FAILED);
    362             return;
    363         }
    364 
    365         if (mUris.size() > 1) {
    366             beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/");
    367             if (!beamPath.isDirectory() && !beamPath.mkdir()) {
    368                 Log.e(TAG, "Failed to create multiple path " + beamPath.toString());
    369                 updateStateAndNotification(STATE_FAILED);
    370                 return;
    371             }
    372         }
    373 
    374         for (int i = 0; i < mUris.size(); i++) {
    375             Uri uri = mUris.get(i);
    376             String mimeType = mTransferMimeTypes.get(i);
    377 
    378             File srcFile = new File(uri.getPath());
    379 
    380             File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(),
    381                     uri.getLastPathSegment());
    382             Log.d(TAG, "Renaming from " + srcFile);
    383             if (!srcFile.renameTo(dstFile)) {
    384                 if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
    385                 srcFile.delete();
    386                 return;
    387             } else {
    388                 mPaths.add(dstFile.getAbsolutePath());
    389                 mMimeTypes.put(dstFile.getAbsolutePath(), mimeType);
    390                 if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile);
    391             }
    392         }
    393 
    394         // We can either add files to the media provider, or provide an ACTION_VIEW
    395         // intent to the file directly. We base this decision on the mime type
    396         // of the first file; if it's media the platform can deal with,
    397         // use the media provider, if it's something else, just launch an ACTION_VIEW
    398         // on the file.
    399         String mimeType = mMimeTypes.get(mPaths.get(0));
    400         if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
    401                 mimeType.startsWith("audio/")) {
    402             String[] arrayPaths = new String[mPaths.size()];
    403             MediaScannerConnection.scanFile(mContext, mPaths.toArray(arrayPaths), null, this);
    404             updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
    405         } else {
    406             // We're done.
    407             updateStateAndNotification(STATE_SUCCESS);
    408         }
    409 
    410     }
    411 
    412     public int getTransferId() {
    413         return mTransferId;
    414     }
    415 
    416     public boolean handleMessage(Message msg) {
    417         if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
    418             // We didn't receive a new transfer in time, finalize this one
    419             if (mIncoming) {
    420                 processFiles();
    421             } else {
    422                 updateStateAndNotification(mSuccessCount > 0 ? STATE_SUCCESS : STATE_FAILED);
    423             }
    424             return true;
    425         } else if (msg.what == MSG_TRANSFER_TIMEOUT) {
    426             // No update on this transfer for a while, fail it.
    427             if (DBG) Log.d(TAG, "Transfer timed out for id: " + Integer.toString(mTransferId));
    428             updateStateAndNotification(STATE_FAILED);
    429         }
    430         return false;
    431     }
    432 
    433     public synchronized void onScanCompleted(String path, Uri uri) {
    434         if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
    435         if (uri != null) {
    436             mMediaUris.put(path, uri);
    437         }
    438         mUrisScanned++;
    439         if (mUrisScanned == mPaths.size()) {
    440             // We're done
    441             updateStateAndNotification(STATE_SUCCESS);
    442         }
    443     }
    444 
    445     boolean checkMediaStorage(File path) {
    446         if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    447             if (!path.isDirectory() && !path.mkdir()) {
    448                 Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath());
    449                 return false;
    450             }
    451             return true;
    452         } else {
    453             Log.e(TAG, "External storage not mounted, can't store file.");
    454             return false;
    455         }
    456     }
    457 
    458     Intent buildViewIntent() {
    459         if (mPaths.size() == 0) return null;
    460 
    461         Intent viewIntent = new Intent(Intent.ACTION_VIEW);
    462 
    463         String filePath = mPaths.get(0);
    464         Uri mediaUri = mMediaUris.get(filePath);
    465         Uri uri =  mediaUri != null ? mediaUri :
    466             Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
    467         viewIntent.setDataAndTypeAndNormalize(uri, mMimeTypes.get(filePath));
    468         viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    469         return viewIntent;
    470     }
    471 
    472     PendingIntent buildCancelIntent(boolean incoming) {
    473         Intent intent = new Intent(HandoverService.ACTION_CANCEL_HANDOVER_TRANSFER);
    474         intent.putExtra(HandoverService.EXTRA_ADDRESS, mDeviceType == DEVICE_TYPE_BLUETOOTH
    475                 ? mRemoteDevice.getAddress() : mRemoteMac);
    476         intent.putExtra(HandoverService.EXTRA_INCOMING, incoming ?
    477                 HandoverService.DIRECTION_INCOMING : HandoverService.DIRECTION_OUTGOING);
    478         PendingIntent pi = PendingIntent.getBroadcast(mContext, mTransferId, intent,
    479                 PendingIntent.FLAG_ONE_SHOT);
    480 
    481         return pi;
    482     }
    483 
    484     File generateUniqueDestination(String path, String fileName) {
    485         int dotIndex = fileName.lastIndexOf(".");
    486         String extension = null;
    487         String fileNameWithoutExtension = null;
    488         if (dotIndex < 0) {
    489             extension = "";
    490             fileNameWithoutExtension = fileName;
    491         } else {
    492             extension = fileName.substring(dotIndex);
    493             fileNameWithoutExtension = fileName.substring(0, dotIndex);
    494         }
    495         File dstFile = new File(path + File.separator + fileName);
    496         int count = 0;
    497         while (dstFile.exists()) {
    498             dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" +
    499                     Integer.toString(count) + extension);
    500             count++;
    501         }
    502         return dstFile;
    503     }
    504 
    505     File generateMultiplePath(String beamRoot) {
    506         // Generate a unique directory with the date
    507         String format = "yyyy-MM-dd";
    508         SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
    509         String newPath = beamRoot + "beam-" + sdf.format(new Date());
    510         File newFile = new File(newPath);
    511         int count = 0;
    512         while (newFile.exists()) {
    513             newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" +
    514                     Integer.toString(count);
    515             newFile = new File(newPath);
    516             count++;
    517         }
    518         return newFile;
    519     }
    520 }
    521 
    522