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