Enable Bluetooth sharing of downloaded files.

Change BT OPP to open the InputStreams of files to share in the
BluetoothOppLauncherActivity (or BluetoothOppHandoverReceiver
for NFC shares), while the process has grantUriPermission() on
the URI's to share.

InputStreams are saved inside the existing BluetoothOppSendFileInfo
objects, which no longer include the mDestination field because
this isn't known at the time the SendFileInfo object is now created
(before the user has chosen the destination BT device). These
objects are stored in a static ConcurrentHashMap in BluetoothOppUtility
and are removed when the file is closed (on success or failure).

If the user tries to share thousands of files in one batch, we may
not be able to open InputStreams for all of the files in the batch.
In this case, the open should fail gracefully.

Bug: 6808783
Change-Id: I3f3f86d2dc1a78a837aeb6a888f90b26434ba499
diff --git a/src/com/android/bluetooth/opp/BluetoothOppManager.java b/src/com/android/bluetooth/opp/BluetoothOppManager.java
index c996690..dd8efe0 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppManager.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppManager.java
@@ -246,12 +246,15 @@
         if (V) Log.v(TAG, "Application data stored to SharedPreference! ");
     }
 
-    public void saveSendingFileInfo(String mimeType, String uri, boolean isHandover) {
+    public void saveSendingFileInfo(String mimeType, String uriString, boolean isHandover) {
         synchronized (BluetoothOppManager.this) {
             mMultipleFlag = false;
             mMimeTypeOfSendingFile = mimeType;
-            mUriOfSendingFile = uri;
+            mUriOfSendingFile = uriString;
             mIsHandoverInitiated = isHandover;
+            Uri uri = Uri.parse(uriString);
+            BluetoothOppUtility.putSendFileInfo(uri,
+                    BluetoothOppSendFileInfo.generateFileInfo(mContext, uri, mimeType));
             storeApplicationData();
         }
     }
@@ -262,6 +265,10 @@
             mMimeTypeOfSendingFiles = mimeType;
             mUrisOfSendingFiles = uris;
             mIsHandoverInitiated = isHandover;
+            for (Uri uri : uris) {
+                BluetoothOppUtility.putSendFileInfo(uri,
+                        BluetoothOppSendFileInfo.generateFileInfo(mContext, uri, mimeType));
+            }
             storeApplicationData();
         }
     }
diff --git a/src/com/android/bluetooth/opp/BluetoothOppObexClientSession.java b/src/com/android/bluetooth/opp/BluetoothOppObexClientSession.java
index f234203..dce7fa3 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppObexClientSession.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppObexClientSession.java
@@ -305,8 +305,7 @@
         private BluetoothOppSendFileInfo processShareInfo() {
             if (V) Log.v(TAG, "Client thread processShareInfo() " + mInfo.mId);
 
-            BluetoothOppSendFileInfo fileInfo = BluetoothOppSendFileInfo.generateFileInfo(
-                    mContext1, mInfo.mUri, mInfo.mMimetype, mInfo.mDestination);
+            BluetoothOppSendFileInfo fileInfo = BluetoothOppUtility.getSendFileInfo(mInfo.mUri);
             if (fileInfo.mFileName == null || fileInfo.mLength == 0) {
                 if (V) Log.v(TAG, "BluetoothOppSendFileInfo get invalid file");
                     Constants.updateShareStatus(mContext1, mInfo.mId, fileInfo.mStatus);
@@ -343,7 +342,7 @@
             request.setHeader(HeaderSet.NAME, fileInfo.mFileName);
             request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype);
 
-            applyRemoteDeviceQuirks(request, fileInfo);
+            applyRemoteDeviceQuirks(request, mInfo.mDestination, fileInfo.mFileName);
 
             Constants.updateShareStatus(mContext1, mInfo.mId, BluetoothShare.STATUS_RUNNING);
 
@@ -500,7 +499,8 @@
                 handleSendException(e.toString());
             } finally {
                 try {
-                    fileInfo.mInputStream.close();
+                    // Close InputStream and remove SendFileInfo from map
+                    BluetoothOppUtility.closeSendFileInfo(mInfo.mUri);
                     if (!error) {
                         responseCode = putOperation.getResponseCode();
                         if (responseCode != -1) {
@@ -566,8 +566,7 @@
         }
     }
 
-    public static void applyRemoteDeviceQuirks(HeaderSet request, BluetoothOppSendFileInfo info) {
-        String address = info.mDestAddr;
+    public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) {
         if (address == null) {
             return;
         }
@@ -576,8 +575,6 @@
             // Rejects filenames with more than one '.'. Rename to '_'.
             // for example: 'a.b.jpg' -> 'a_b.jpg'
             //              'abc.jpg' NOT CHANGED
-            String filename = info.mFileName;
-
             char[] c = filename.toCharArray();
             boolean firstDot = true;
             boolean modified = false;
diff --git a/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java b/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java
index ec879f0..81c3c92 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppSendFileInfo.java
@@ -32,19 +32,18 @@
 
 package com.android.bluetooth.opp;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-
-import android.util.Log;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.ParcelFileDescriptor;
 import android.provider.OpenableColumns;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 
 /**
  * This class stores information about a single sending file It will only be
@@ -57,6 +56,10 @@
 
     private static final boolean V = Constants.VERBOSE;
 
+    /** Reusable SendFileInfo for error status. */
+    static final BluetoothOppSendFileInfo SEND_FILE_INFO_ERROR = new BluetoothOppSendFileInfo(
+            null, null, 0, null, BluetoothShare.STATUS_FILE_ERROR);
+
     /** readable media file name */
     public final String mFileName;
 
@@ -72,46 +75,40 @@
 
     public final long mLength;
 
-    public final String mDestAddr;
-
     /** for media file */
     public BluetoothOppSendFileInfo(String fileName, String type, long length,
-            FileInputStream inputStream, int status, String dest) {
+            FileInputStream inputStream, int status) {
         mFileName = fileName;
         mMimetype = type;
         mLength = length;
         mInputStream = inputStream;
         mStatus = status;
-        mDestAddr = dest;
         mData = null;
     }
 
     /** for vCard, or later for vCal, vNote. Not used currently */
-    public BluetoothOppSendFileInfo(String data, String type, long length, int status,
-            String dest) {
+    public BluetoothOppSendFileInfo(String data, String type, long length, int status) {
         mFileName = null;
         mInputStream = null;
         mData = data;
         mMimetype = type;
         mLength = length;
         mStatus = status;
-        mDestAddr = dest;
     }
 
-    public static BluetoothOppSendFileInfo generateFileInfo(Context context, String uri,
-            String type, String dest) {
+    public static BluetoothOppSendFileInfo generateFileInfo(Context context, Uri uri,
+            String type) {
         ContentResolver contentResolver = context.getContentResolver();
-        Uri u = Uri.parse(uri);
-        String scheme = u.getScheme();
+        String scheme = uri.getScheme();
         String fileName = null;
-        String contentType = null;
+        String contentType;
         long length = 0;
         // Support all Uri with "content" scheme
         // This will allow more 3rd party applications to share files via
         // bluetooth
-        if (scheme.equals("content")) {
-            contentType = contentResolver.getType(u);
-            Cursor metadataCursor = contentResolver.query(u, new String[] {
+        if ("content".equals(scheme)) {
+            contentType = contentResolver.getType(uri);
+            Cursor metadataCursor = contentResolver.query(uri, new String[] {
                     OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
             }, null, null, null);
             if (metadataCursor != null) {
@@ -125,25 +122,23 @@
                     metadataCursor.close();
                 }
             }
-        } else if (scheme.equals("file")) {
-            fileName = u.getLastPathSegment();
+        } else if ("file".equals(scheme)) {
+            fileName = uri.getLastPathSegment();
             contentType = type;
-            File f = new File(u.getPath());
+            File f = new File(uri.getPath());
             length = f.length();
         } else {
             // currently don't accept other scheme
-            return new BluetoothOppSendFileInfo(null, null, 0, null,
-                    BluetoothShare.STATUS_FILE_ERROR, dest);
+            return SEND_FILE_INFO_ERROR;
         }
         FileInputStream is = null;
         if (scheme.equals("content")) {
-            AssetFileDescriptor fd = null;
             try {
                 // We've found that content providers don't always have the
                 // right size in _OpenableColumns.SIZE
                 // As a second source of getting the correct file length,
                 // get a file descriptor and get the stat length
-                fd = contentResolver.openAssetFileDescriptor(u, "r");
+                AssetFileDescriptor fd = contentResolver.openAssetFileDescriptor(uri, "r");
                 long statLength = fd.getLength();
                 if (length != statLength && statLength > 0) {
                     Log.e(TAG, "Content provider length is wrong (" + Long.toString(length) +
@@ -154,7 +149,7 @@
                     // This creates an auto-closing input-stream, so
                     // the file descriptor will be closed whenever the InputStream
                     // is closed.
-                    is = (FileInputStream)fd.createInputStream();
+                    is = fd.createInputStream();
                 } catch (IOException e) {
                     try {
                         fd.close();
@@ -168,10 +163,9 @@
         }
         if (is == null) {
             try {
-                is = (FileInputStream)contentResolver.openInputStream(u);
+                is = (FileInputStream) contentResolver.openInputStream(uri);
             } catch (FileNotFoundException e) {
-                return new BluetoothOppSendFileInfo(null, null, 0, null,
-                        BluetoothShare.STATUS_FILE_ERROR, dest);
+                return SEND_FILE_INFO_ERROR;
             }
         }
         // If we can not get file length from content provider, we can try to
@@ -182,11 +176,10 @@
                 if (V) Log.v(TAG, "file length is " + length);
             } catch (IOException e) {
                 Log.e(TAG, "Read stream exception: ", e);
-                return new BluetoothOppSendFileInfo(null, null, 0, null,
-                        BluetoothShare.STATUS_FILE_ERROR, dest);
+                return SEND_FILE_INFO_ERROR;
             }
         }
 
-        return new BluetoothOppSendFileInfo(fileName, contentType, length, is, 0, dest);
+        return new BluetoothOppSendFileInfo(fileName, contentType, length, is, 0);
     }
 }
diff --git a/src/com/android/bluetooth/opp/BluetoothOppService.java b/src/com/android/bluetooth/opp/BluetoothOppService.java
index 5a30433..9020006 100755
--- a/src/com/android/bluetooth/opp/BluetoothOppService.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppService.java
@@ -543,9 +543,18 @@
     }
 
     private void insertShare(Cursor cursor, int arrayPos) {
+        String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
+        Uri uri;
+        if (uriString != null) {
+            uri = Uri.parse(uriString);
+            Log.d(TAG, "insertShare parsed URI: " + uri);
+        } else {
+            uri = null;
+            Log.e(TAG, "insertShare found null URI at cursor!");
+        }
         BluetoothOppShareInfo info = new BluetoothOppShareInfo(
                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)),
-                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI)),
+                uri,
                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
@@ -597,23 +606,12 @@
         if (info.isReadyToStart()) {
             if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
                 /* check if the file exists */
-                InputStream i;
-                try {
-                    i = getContentResolver().openInputStream(Uri.parse(info.mUri));
-                } catch (FileNotFoundException e) {
+                BluetoothOppSendFileInfo sendFileInfo = BluetoothOppUtility.getSendFileInfo(
+                        info.mUri);
+                if (sendFileInfo == null || sendFileInfo.mInputStream == null) {
                     Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId);
                     Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
-                    return;
-                } catch (SecurityException e) {
-                    Log.e(TAG, "Exception:" + e.toString() + " for OUTBOUND info " + info.mId);
-                    Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
-                    return;
-                }
-
-                try {
-                    i.close();
-                } catch (IOException ex) {
-                    Log.e(TAG, "IO error when close file for OUTBOUND info " + info.mId);
+                    BluetoothOppUtility.closeSendFileInfo(info.mUri);
                     return;
                 }
             }
@@ -678,7 +676,12 @@
         int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
 
         info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
-        info.mUri = stringFromCursor(info.mUri, cursor, BluetoothShare.URI);
+        if (info.mUri != null) {
+            info.mUri = Uri.parse(stringFromCursor(info.mUri.toString(), cursor,
+                    BluetoothShare.URI));
+        } else {
+            Log.d(TAG, "updateShare() called for ID " + info.mId + " with null URI");
+        }
         info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
         info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
         info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
diff --git a/src/com/android/bluetooth/opp/BluetoothOppShareInfo.java b/src/com/android/bluetooth/opp/BluetoothOppShareInfo.java
index da57bd2..32f6b3c 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppShareInfo.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppShareInfo.java
@@ -32,6 +32,8 @@
 
 package com.android.bluetooth.opp;
 
+import android.net.Uri;
+
 /**
  * This class stores information about a single OBEX share, e.g. one object
  * send/receive to a destination address.
@@ -40,7 +42,7 @@
 
     public int mId;
 
-    public String mUri;
+    public Uri mUri;
 
     public String mHint;
 
@@ -66,7 +68,7 @@
 
     public boolean mMediaScanned;
 
-    public BluetoothOppShareInfo(int id, String uri, String hint, String filename, String mimetype,
+    public BluetoothOppShareInfo(int id, Uri uri, String hint, String filename, String mimetype,
             int direction, String destination, int visibility, int confirm, int status,
             int totalBytes, int currentBytes, int timestamp, boolean mediaScanned) {
         mId = id;
diff --git a/src/com/android/bluetooth/opp/BluetoothOppTransfer.java b/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
index 44d9bb6..2be679c 100755
--- a/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
@@ -315,9 +315,9 @@
                 updateValues.put(BluetoothShare.STATUS, info.mStatus);
                 /* Update un-processed outbound transfer to show some info */
                 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
-                    BluetoothOppSendFileInfo fileInfo = null;
-                    fileInfo = BluetoothOppSendFileInfo.generateFileInfo(mContext, info.mUri,
-                            info.mMimetype, info.mDestination);
+                    BluetoothOppSendFileInfo fileInfo
+                            = BluetoothOppUtility.getSendFileInfo(info.mUri);
+                    BluetoothOppUtility.closeSendFileInfo(info.mUri);
                     if (fileInfo.mFileName != null) {
                         updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
                         updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength);
diff --git a/src/com/android/bluetooth/opp/BluetoothOppUtility.java b/src/com/android/bluetooth/opp/BluetoothOppUtility.java
index df26bd2..7e03281 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppUtility.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppUtility.java
@@ -49,8 +49,10 @@
 import android.util.Log;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * This class has some utilities for Opp application;
@@ -60,6 +62,9 @@
     private static final boolean D = Constants.DEBUG;
     private static final boolean V = Constants.VERBOSE;
 
+    private static final ConcurrentHashMap<Uri, BluetoothOppSendFileInfo> sSendFileMap
+            = new ConcurrentHashMap<Uri, BluetoothOppSendFileInfo>();
+
     public static BluetoothOppTransferInfo queryRecord(Context context, Uri uri) {
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
@@ -303,4 +308,25 @@
                 transInfo.mDeviceName);
     }
 
+    static void putSendFileInfo(Uri uri, BluetoothOppSendFileInfo sendFileInfo) {
+        if (D) Log.d(TAG, "putSendFileInfo: uri=" + uri + " sendFileInfo=" + sendFileInfo);
+        sSendFileMap.put(uri, sendFileInfo);
+    }
+
+    static BluetoothOppSendFileInfo getSendFileInfo(Uri uri) {
+        if (D) Log.d(TAG, "getSendFileInfo: uri=" + uri);
+        BluetoothOppSendFileInfo info = sSendFileMap.get(uri);
+        return (info != null) ? info : BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR;
+    }
+
+    static void closeSendFileInfo(Uri uri) {
+        if (D) Log.d(TAG, "closeSendFileInfo: uri=" + uri);
+        BluetoothOppSendFileInfo info = sSendFileMap.remove(uri);
+        if (info != null) {
+            try {
+                info.mInputStream.close();
+            } catch (IOException ignored) {
+            }
+        }
+    }
 }