Move insert share info operation to a thread to avoid Receiver timeout

In OppReceiver, the function mOppManager.startTransfer(remoteDevice), which
        is used to insert share records to DB,  can be really a time-consuming
        operation in case of 300 or more objects sharing case.
        For onReceive() function in BroadcastReceiver, there are below specification:
        The function  is normally called within the main thread of its process, so you should never
        perform long-running operations in it  (there is a timeout of 10 seconds that the system allows
        before considering the receiver to  be blocked and a candidate to be killed)

Add handle to concurrent sending case
        In Gellary, user can multiple select files (say 50files), and share via bluetooth to device1,
        and then right away share via bluetooth to device2, and also can share to device3. In such
        extream case, we need ensure all share info are saved to db.

Complete the TODO in OppManager
       Store application data to SharedPreferences and restore them when service restart.
diff --git a/android/app/res/values/strings.xml b/android/app/res/values/strings.xml
index 1c8c498..7a80cdb 100644
--- a/android/app/res/values/strings.xml
+++ b/android/app/res/values/strings.xml
@@ -173,6 +173,7 @@
     <string name="bt_tf_10">Locked memory card. You must unlock the memory card to save files.</string>
     <string name="bt_tf_12">There is a file being transferred to a different device. Please wait\u2026</string>
     <string name="bt_tf_13">Bluetooth is currently busy. Consider turning off another Bluetooth feature and trying again.</string>
+    <string name="ErrorTooManyRequests">Too many requests are being processed. Try again later.</string>
 
     <!-- Bluetooth Transfer Failure Reason -->
     <string name="status_pending">File transfer not started yet</string>
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
index d0f9ff3..413f896 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
@@ -68,7 +68,7 @@
              * an EXTRA_STREAM with the data to attach.
              */
             if (action.equals(Intent.ACTION_SEND)) {
-                // TODO(Moto): handle type == null case
+                // TODO: handle type == null case
                 String type = intent.getType();
                 Uri stream = (Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM);
                 if (stream != null && type != null) {
@@ -109,7 +109,7 @@
                 return;
             }
 
-            // TODO(Moto): In the future, we may send intent to DevicePickerActivity
+            // TODO: In the future, we may send intent to DevicePickerActivity
             // directly,
             // and let DevicePickerActivity to handle Bluetooth Enable.
             if (!BluetoothOppManager.getInstance(this).isEnabled()) {
@@ -120,7 +120,7 @@
             } else {
                 if (V) Log.v(TAG, "BT already enabled!! ");
                 Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
-                in1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                in1.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
                 in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);
                 in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
                         BluetoothDevicePicker.FILTER_TYPE_TRANSFER);
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppManager.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppManager.java
index 75e9fd9..2dd0acb 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppManager.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppManager.java
@@ -39,8 +39,11 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.content.SharedPreferences;
 import android.net.Uri;
+import android.os.Process;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.util.ArrayList;
@@ -74,8 +77,6 @@
 
     private ArrayList<Uri> mUrisOfSendingFiles;
 
-    private boolean mCanStartTransfer = false;
-
     private static final String OPP_PREFERENCE_FILE = "OPPMGR";
 
     private static final String SENDING_FLAG = "SENDINGFLAG";
@@ -90,7 +91,9 @@
 
     private static final String MULTIPLE_FLAG = "MULTIPLE_FLAG";
 
-    private static final String ARRAYLIST_ITEM_SEPERATOR = "!";
+    private static final String ARRAYLIST_ITEM_SEPERATOR = ";";
+
+    private static final int ALLOWED_INSERT_SHARE_THREAD_NUMBER = 3;
 
     // used to judge if need continue sending process after received a
     // ENABLED_ACTION
@@ -98,7 +101,9 @@
 
     public boolean mMultipleFlag;
 
-    public int mfileNumInBatch;
+    private int mfileNumInBatch;
+
+    private int mInsertShareThreadNum = 0;
 
     /**
      * Get singleton instance.
@@ -142,6 +147,7 @@
     private void restoreApplicationData() {
         SharedPreferences settings = mContext.getSharedPreferences(OPP_PREFERENCE_FILE, 0);
 
+        // All member vars are not initialized till now
         mSendingFlag = settings.getBoolean(SENDING_FLAG, false);
         mMimeTypeOfSendigFile = settings.getString(MIME_TYPE, null);
         mUriOfSendingFile = settings.getString(FILE_URI, null);
@@ -152,57 +158,67 @@
                     + mMimeTypeOfSendigFile + mUriOfSendingFile);
 
         String strUris = settings.getString(FILE_URIS, null);
-        // TODO(Moto): restore mUrisOfSendingFiles from strUris.
+        mUrisOfSendingFiles = new ArrayList<Uri>();
+        if (strUris != null) {
+            String[] splitUri = strUris.split(ARRAYLIST_ITEM_SEPERATOR);
+            for (int i = 0; i < splitUri.length; i++) {
+                mUrisOfSendingFiles.add(Uri.parse(splitUri[i]));
+                if (V) Log.v(TAG, "Uri in batch:  " + Uri.parse(splitUri[i]));
+            }
+        }
+
+        mContext.getSharedPreferences(OPP_PREFERENCE_FILE, 0).edit().clear().commit();
     }
 
     /**
-     * Save application data to preference, need restore these data later
+     * Save application data to preference, need restore these data when service restart
      */
-    private void onDestroy() {
+    private void storeApplicationData() {
         SharedPreferences.Editor editor = mContext.getSharedPreferences(OPP_PREFERENCE_FILE, 0)
                 .edit();
         editor.putBoolean(SENDING_FLAG, mSendingFlag).commit();
-        editor.putString(MIME_TYPE, mMimeTypeOfSendigFile).commit();
-        editor.putString(FILE_URI, mUriOfSendingFile).commit();
-        editor.putString(MIME_TYPE_MULTIPLE, mMimeTypeOfSendigFiles).commit();
         editor.putBoolean(MULTIPLE_FLAG, mMultipleFlag).commit();
-        String strUris;
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0, count = mUrisOfSendingFiles.size(); i < count; i++) {
-            Uri uriContent = mUrisOfSendingFiles.get(i);
-            sb.append(uriContent);
-            sb.append(ARRAYLIST_ITEM_SEPERATOR);
-        }
-        strUris = sb.toString();
-        editor.putString(FILE_URIS, strUris).commit();
-        if (V) Log.v(TAG, "finalize is called and application data saved by SharedPreference! ");
-    }
+        if (mMultipleFlag) {
+            editor.putString(MIME_TYPE_MULTIPLE, mMimeTypeOfSendigFiles).commit();
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0, count = mUrisOfSendingFiles.size(); i < count; i++) {
+                Uri uriContent = mUrisOfSendingFiles.get(i);
+                sb.append(uriContent);
+                sb.append(ARRAYLIST_ITEM_SEPERATOR);
+            }
+            String strUris = sb.toString();
+            editor.putString(FILE_URIS, strUris).commit();
 
-    /**
-     * Save data to preference when this class is destroyed by system due to
-     * memory lack
-     */
-    protected void finalize() throws Throwable {
-        try {
-            onDestroy();
-        } finally {
-            super.finalize();
+            editor.remove(MIME_TYPE).commit();
+            editor.remove(FILE_URI).commit();
+        } else {
+            editor.putString(MIME_TYPE, mMimeTypeOfSendigFile).commit();
+            editor.putString(FILE_URI, mUriOfSendingFile).commit();
+
+            editor.remove(MIME_TYPE_MULTIPLE).commit();
+            editor.remove(FILE_URIS).commit();
         }
+        if (V) Log.v(TAG, "Application data stored to SharedPreference! ");
     }
 
     public void saveSendingFileInfo(String mimeType, String uri) {
-        mMultipleFlag = false;
-        mMimeTypeOfSendigFile = mimeType;
-        mUriOfSendingFile = uri;
-        mCanStartTransfer = true;
+        synchronized (BluetoothOppManager.this) {
+            mMultipleFlag = false;
+            mMimeTypeOfSendigFile = mimeType;
+            mUriOfSendingFile = uri;
+            storeApplicationData();
+        }
     }
 
     public void saveSendingFileInfo(String mimeType, ArrayList<Uri> uris) {
-        mMultipleFlag = true;
-        mMimeTypeOfSendigFiles = mimeType;
-        mUrisOfSendingFiles = uris;
-        mCanStartTransfer = true;
+        synchronized (BluetoothOppManager.this) {
+            mMultipleFlag = true;
+            mMimeTypeOfSendigFiles = mimeType;
+            mUrisOfSendingFiles = uris;
+            storeApplicationData();
+        }
     }
+
     /**
      * Get the current status of Bluetooth hardware.
      * @return true if Bluetooth enabled, false otherwise.
@@ -253,59 +269,138 @@
         return deviceName;
     }
 
+    public int getBatchSize() {
+        synchronized (BluetoothOppManager.this) {
+            return mfileNumInBatch;
+        }
+    }
+
     /**
-     * insert sending sessions to db, only used by Opp application.
+     * Fork a thread to insert share info to db.
      */
     public void startTransfer(BluetoothDevice device) {
-        if (device == null) {
-            Log.e(TAG, "Target bt device is null!");
-            return;
+        if (V) Log.v(TAG, "Active InsertShareThread number is : " + mInsertShareThreadNum);
+        InsertShareInfoThread insertThread;
+        synchronized (BluetoothOppManager.this) {
+            if (mInsertShareThreadNum > ALLOWED_INSERT_SHARE_THREAD_NUMBER) {
+                Log.e(TAG, "Too many shares user triggered concurrently!");
+
+                // Notice user
+                Intent in = new Intent(mContext, BluetoothOppBtErrorActivity.class);
+                in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                in.putExtra("title", mContext.getString(R.string.enabling_progress_title));
+                in.putExtra("content", mContext.getString(R.string.ErrorTooManyRequests));
+                mContext.startActivity(in);
+
+                return;
+            }
+            insertThread = new InsertShareInfoThread(device, mMultipleFlag, mMimeTypeOfSendigFile,
+                    mUriOfSendingFile, mMimeTypeOfSendigFiles, mUrisOfSendingFiles);
+            if (mMultipleFlag) {
+                mfileNumInBatch = mUrisOfSendingFiles.size();
+            }
         }
 
-        if (!mCanStartTransfer) {
-            if (V) Log.v(TAG, "No transfer info restored: fileType&fileName");
-            return;
+        insertThread.start();
+    }
+
+    /**
+     * Thread to insert share info to db. In multiple files (say 100 files)
+     * share case, the inserting share info to db operation would be a time
+     * consuming operation, so need a thread to handle it. This thread allows
+     * multiple instances to support below case: User select multiple files to
+     * share to one device (say device 1), and then right away share to second
+     * device (device 2), we need insert all these share info to db.
+     */
+    private class InsertShareInfoThread extends Thread {
+        private final BluetoothDevice mRemoteDevice;
+
+        private final String mTypeOfSingleFile;
+
+        private final String mUri;
+
+        private final String mTypeOfMultipleFiles;
+
+        private final ArrayList<Uri> mUris;
+
+        private final boolean mIsMultiple;
+
+        public InsertShareInfoThread(BluetoothDevice device, boolean multiple,
+                String typeOfSingleFile, String uri, String typeOfMultipleFiles, ArrayList<Uri> uris) {
+            super("Insert ShareInfo Thread");
+            this.mRemoteDevice = device;
+            this.mIsMultiple = multiple;
+            this.mTypeOfSingleFile = typeOfSingleFile;
+            this.mUri = uri;
+            this.mTypeOfMultipleFiles = typeOfMultipleFiles;
+            this.mUris = uris;
+
+            synchronized (BluetoothOppManager.this) {
+                mInsertShareThreadNum++;
+            }
+
+            if (V) Log.v(TAG, "Thread id is: " + this.getId());
         }
 
-        if (mMultipleFlag == true) {
-            int count = mUrisOfSendingFiles.size();
-            mfileNumInBatch = count;
+        @Override
+        public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            if (mRemoteDevice == null) {
+                Log.e(TAG, "Target bt device is null!");
+                return;
+            }
+            if (mIsMultiple) {
+                insertMultipleShare();
+            } else {
+                insertSingleShare();
+            }
+            synchronized (BluetoothOppManager.this) {
+                mInsertShareThreadNum--;
+            }
+        }
 
+        /**
+         * Insert multiple sending sessions to db, only used by Opp application.
+         */
+        private void insertMultipleShare() {
+            int count = mUris.size();
             Long ts = System.currentTimeMillis();
             for (int i = 0; i < count; i++) {
-                Uri fileUri = mUrisOfSendingFiles.get(i);
+                Uri fileUri = mUris.get(i);
                 ContentResolver contentResolver = mContext.getContentResolver();
                 String contentType = contentResolver.getType(fileUri);
                 if (V) Log.v(TAG, "Got mimetype: " + contentType + "  Got uri: " + fileUri);
+                if (TextUtils.isEmpty(contentType)) {
+                    contentType = mTypeOfMultipleFiles;
+                }
 
                 ContentValues values = new ContentValues();
                 values.put(BluetoothShare.URI, fileUri.toString());
                 values.put(BluetoothShare.MIMETYPE, contentType);
-                values.put(BluetoothShare.DESTINATION, device.getAddress());
+                values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress());
                 values.put(BluetoothShare.TIMESTAMP, ts);
 
                 final Uri contentUri = mContext.getContentResolver().insert(
                         BluetoothShare.CONTENT_URI, values);
                 if (V) Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: "
-                            + getDeviceName(device));
+                            + getDeviceName(mRemoteDevice));
             }
-        } else {
+        }
+
+         /**
+         * Insert single sending session to db, only used by Opp application.
+         */
+        private void insertSingleShare() {
             ContentValues values = new ContentValues();
-            values.put(BluetoothShare.URI, mUriOfSendingFile);
-            values.put(BluetoothShare.MIMETYPE, mMimeTypeOfSendigFile);
-            values.put(BluetoothShare.DESTINATION, device.getAddress());
+            values.put(BluetoothShare.URI, mUri);
+            values.put(BluetoothShare.MIMETYPE, mTypeOfSingleFile);
+            values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress());
 
             final Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI,
                     values);
             if (V) Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: "
-                        + getDeviceName(device));
+                                + getDeviceName(mRemoteDevice));
         }
-
-        // reset vars
-        mMimeTypeOfSendigFile = null;
-        mUriOfSendingFile = null;
-        mUrisOfSendingFiles = null;
-        mMultipleFlag = false;
-        mCanStartTransfer = false;
     }
+
 }
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppNotification.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppNotification.java
index 345ec89..0076c8e 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppNotification.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppNotification.java
@@ -114,7 +114,7 @@
     }
 
     public void finishNotification() {
-        synchronized (this) {
+        synchronized (BluetoothOppNotification.this) {
             mFinised = true;
         }
     }
@@ -123,7 +123,7 @@
      * Update the notification ui.
      */
     public void updateNotification() {
-        synchronized (this) {
+        synchronized (BluetoothOppNotification.this) {
             mPendingUpdate = true;
             if (mUpdateNotificationThread == null) {
                 mUpdateNotificationThread = new NotificationUpdateThread();
@@ -143,7 +143,7 @@
         public void run() {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
             for (;;) {
-                synchronized (this) {
+                synchronized (BluetoothOppNotification.this) {
                     if (mUpdateNotificationThread != this) {
                         throw new IllegalStateException(
                                 "multiple UpdateThreads in BluetoothOppNotification");
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppReceiver.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppReceiver.java
index 7ce5cbc..a0735d0 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppReceiver.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppReceiver.java
@@ -104,9 +104,10 @@
             // Display toast message
             String deviceName = mOppManager.getDeviceName(remoteDevice);
             String toastMsg;
+            int batchSize = mOppManager.getBatchSize();
             if (mOppManager.mMultipleFlag) {
-                toastMsg = context.getString(R.string.bt_toast_5, Integer
-                        .toString(mOppManager.mfileNumInBatch), deviceName);
+                toastMsg = context.getString(R.string.bt_toast_5, Integer.toString(batchSize),
+                        deviceName);
             } else {
                 toastMsg = context.getString(R.string.bt_toast_4, deviceName);
             }