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