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