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