MAP: Fix ANR for blocking operations on UI thread

UseCase:
    Stability Testing steps followed:
    1. Pair and connect with remote phone.
    2. Verify successful profile connection.
    3. Pair & Connect LE device.
    4. Disconnect & Unpair LE device.
    5. Reset BT multiple times.
    6. Repeat above steps 50 times

Failure:
    ANR(s) reported for long running operations perfromed on main thread.

Fix:
    -  Attach MAPService session handler to looper from a
       worker thread to avoid long running tasks handling
       for messages posted from UI thread.

    -  Move AppObserver Account onChange() handling
       to worker thread instead of UI thread.

    -  Avoid running contentobserver for SMS/MMS instance on
       main thread looper to  fix ANR reported while handling
       content changes for SMS and MMS listing. Add NPE checks
       hit during stability testing.

Change-Id: Ie146cd284fd191f7c7be7f0c1e7ed4877b36ae76
diff --git a/android/app/src/com/android/bluetooth/map/BluetoothMapAppObserver.java b/android/app/src/com/android/bluetooth/map/BluetoothMapAppObserver.java
index 8c69f15..6363415 100644
--- a/android/app/src/com/android/bluetooth/map/BluetoothMapAppObserver.java
+++ b/android/app/src/com/android/bluetooth/map/BluetoothMapAppObserver.java
@@ -160,7 +160,7 @@
     public void registerObserver(BluetoothMapAccountItem app) {
         Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
         if (V) Log.d(TAG, "registerObserver for URI "+uri.toString()+"\n");
-        ContentObserver observer = new ContentObserver(new Handler()) {
+        ContentObserver observer = new ContentObserver(null) {
             @Override
             public void onChange(boolean selfChange) {
                 onChange(selfChange, null);
@@ -179,7 +179,8 @@
             }
         };
         mObserverMap.put(uri.toString(), observer);
-        mResolver.registerContentObserver(uri, true, observer);
+        //False "notifyForDescendents" : Get notified whenever a change occurs to the exact URI.
+        mResolver.registerContentObserver(uri, false, observer);
     }
 
     public void unregisterObserver(BluetoothMapAccountItem app) {
diff --git a/android/app/src/com/android/bluetooth/map/BluetoothMapContentObserver.java b/android/app/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
index 335b728..2a125e7 100644
--- a/android/app/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
+++ b/android/app/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
@@ -469,8 +469,7 @@
         return smsType;
     }
 
-    private final ContentObserver mObserver = new ContentObserver(
-            new Handler(Looper.getMainLooper())) {
+    private final ContentObserver mObserver = new ContentObserver(new Handler()) {
         @Override
         public void onChange(boolean selfChange) {
             onChange(selfChange, null);
@@ -3401,7 +3400,9 @@
     };
 
     public void init() {
-        mSmsBroadcastReceiver.register();
+        if (mSmsBroadcastReceiver != null) {
+            mSmsBroadcastReceiver.register();
+        }
         registerPhoneServiceStateListener();
         mInitialized = true;
     }
@@ -3409,7 +3410,9 @@
     public void deinit() {
         mInitialized = false;
         unregisterObserver();
-        mSmsBroadcastReceiver.unregister();
+        if (mSmsBroadcastReceiver != null) {
+            mSmsBroadcastReceiver.unregister();
+        }
         unRegisterPhoneServiceStateListener();
         failPendingMessages();
         removeDeletedMessages();
diff --git a/android/app/src/com/android/bluetooth/map/BluetoothMapService.java b/android/app/src/com/android/bluetooth/map/BluetoothMapService.java
index e04f959..fa04202 100644
--- a/android/app/src/com/android/bluetooth/map/BluetoothMapService.java
+++ b/android/app/src/com/android/bluetooth/map/BluetoothMapService.java
@@ -30,6 +30,8 @@
 import android.content.IntentFilter;
 import android.content.IntentFilter.MalformedMimeTypeException;
 import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelUuid;
 import android.os.PowerManager;
@@ -146,6 +148,7 @@
     private boolean mAccountChanged = false;
     private boolean mSdpSearchInitiated = false;
     SdpMnsRecord mMnsRecord = null;
+    private MapServiceMessageHandler mSessionStatusHandler;
     private boolean mStartError = true;
 
     // package and class name to which we send intent to check phone book access permission
@@ -164,21 +167,18 @@
     }
 
 
-    private final void closeService() {
+    private synchronized void closeService() {
         if (DEBUG) Log.d(TAG, "MAP Service closeService in");
 
         if (mBluetoothMnsObexClient != null) {
             mBluetoothMnsObexClient.shutdown();
             mBluetoothMnsObexClient = null;
         }
-
-        for(int i=0, c=mMasInstances.size(); i < c; i++) {
-            mMasInstances.valueAt(i).shutdown();
-        }
-        mMasInstances.clear();
-
-        if (mSessionStatusHandler != null) {
-            mSessionStatusHandler.removeCallbacksAndMessages(null);
+        if (mMasInstances.size() > 0) {
+            for (int i=0, c=mMasInstances.size(); i < c; i++) {
+                mMasInstances.valueAt(i).shutdown();
+            }
+            mMasInstances.clear();
         }
 
         mIsWaitingAuthorization = false;
@@ -190,6 +190,19 @@
             if (VERBOSE) Log.v(TAG, "CloseService(): Release Wake Lock");
             mWakeLock = null;
         }
+        /* Only one SHUTDOWN message expected to closeService.
+         * Hence, quit looper and Handler on first SHUTDOWN message*/
+        if (mSessionStatusHandler != null) {
+            //Perform cleanup in Handler running on worker Thread
+            mSessionStatusHandler.removeCallbacksAndMessages(null);
+            Looper looper = mSessionStatusHandler.getLooper();
+            if (looper != null) {
+                looper.quit();
+                if(VERBOSE) Log.i(TAG, "Quit looper");
+            }
+            mSessionStatusHandler = null;
+            if(VERBOSE) Log.i(TAG, "Remove Handler");
+        }
         mRemoteDevice = null;
 
         if (VERBOSE) Log.v(TAG, "MAP Service closeService out");
@@ -294,7 +307,7 @@
         BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1
         if(masInst != null) {
             masInst.restartObexServerSession();
-        } else {
+        } else  if(masId == -1) {
             for(int i=0, c=mMasInstances.size(); i < c; i++) {
                 mMasInstances.valueAt(i).restartObexServerSession();
             }
@@ -318,7 +331,10 @@
         }
     }
 
-    private final Handler mSessionStatusHandler = new Handler() {
+    private final class MapServiceMessageHandler extends Handler {
+        private MapServiceMessageHandler(Looper looper) {
+            super(looper);
+        }
         @Override
         public void handleMessage(Message msg) {
             if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what);
@@ -496,7 +512,7 @@
     public boolean disconnectMap(BluetoothDevice device) {
         boolean result = false;
         if (DEBUG) Log.d(TAG, "disconnectMap");
-        if (getRemoteDevice().equals(device)) {
+        if (getRemoteDevice()!= null && getRemoteDevice().equals(device)) {
             switch (mState) {
                 case BluetoothMap.STATE_CONNECTED:
                     /* Disconnect all connections and restart all MAS instances */
@@ -578,6 +594,11 @@
             Log.w(TAG, "start received for already started, ignoring");
             return false;
         }
+        HandlerThread thread = new HandlerThread("BluetoothMapHandler");
+        thread.start();
+        Looper looper = thread.getLooper();
+        mSessionStatusHandler = new MapServiceMessageHandler(looper);
+
         IntentFilter filter = new IntentFilter();
         filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
@@ -791,6 +812,9 @@
         } else {
             if (VERBOSE) Log.d(TAG, "Service Stoping()");
         }
+        if (mSessionStatusHandler != null) {
+            sendShutdownMessage();
+        }
         mStartError = true;
         setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
         sendShutdownMessage();
@@ -800,8 +824,9 @@
     public boolean cleanup()  {
         if (DEBUG) Log.d(TAG, "cleanup()");
         setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
-        // TODO: Change to use message? - do we need to wait for completion?
-        closeService();
+        //Cleanup already handled in Stop().
+        //Move this  extra check to Handler.
+        sendShutdownMessage();
         return true;
     }
 
@@ -896,13 +921,16 @@
      * @param masId the MasID to start. Use -1 to start all listeners.
      */
     public void sendStartListenerMessage(int masId) {
-        if(mSessionStatusHandler != null) {
+        if (mSessionStatusHandler != null && ! mSessionStatusHandler.hasMessages(START_LISTENER)) {
             Message msg = mSessionStatusHandler.obtainMessage(START_LISTENER, masId, 0);
             /* We add a small delay here to ensure the call returns true before this message is
              * handled. It seems wrong to add a delay, but the alternative is to build a lock
              * system to handle synchronization, which isn't nice either... */
             mSessionStatusHandler.sendMessageDelayed(msg, 20);
-        } // Can only be null during shutdown
+        } else if (mSessionStatusHandler != null) {
+            if(DEBUG)
+                Log.w(TAG, "mSessionStatusHandler START_LISTENER message already in Queue");
+        }
     }
 
     private void sendConnectMessage(int masId) {
@@ -938,9 +966,23 @@
             mIsWaitingAuthorization = false;
             cancelUserTimeoutAlarm();
         }
-        mSessionStatusHandler.removeCallbacksAndMessages(null);
-        // Request release of all resources
-        mSessionStatusHandler.obtainMessage(SHUTDOWN).sendToTarget();
+        if (mSessionStatusHandler != null && !mSessionStatusHandler.hasMessages(SHUTDOWN)) {
+            mSessionStatusHandler.removeCallbacksAndMessages(null);
+            // Request release of all resources
+            Message msg = mSessionStatusHandler.obtainMessage(SHUTDOWN);
+            if( mSessionStatusHandler.sendMessage(msg) == false) {
+                /* most likely caused by shutdown being called from multiple sources - e.g.BT off
+                 * signaled through intent and a service shutdown simultaneously.
+                 * Intended behavior not documented, hence we need to be able to handle all cases*/
+            } else {
+                if(DEBUG)
+                    Log.e(TAG, "mSessionStatusHandler.sendMessage() dispatched shutdown message");
+            }
+        } else if (mSessionStatusHandler != null) {
+                if(DEBUG)
+                    Log.w(TAG, "mSessionStatusHandler shutdown message already in Queue");
+        }
+        if (VERBOSE) Log.d(TAG, "sendShutdownMessage() Out");
     }
 
     private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver();