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