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