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