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