Loading android/app/src/com/android/bluetooth/mapclient/MapClientService.java +253 −36 Original line number Diff line number Diff line Loading @@ -21,9 +21,17 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothMapClient; import android.bluetooth.SdpMasRecord; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.ParcelUuid; import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.bluetooth.Utils; Loading @@ -31,8 +39,11 @@ import com.android.bluetooth.btservice.ProfileService; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class MapClientService extends ProfileService { private static final String TAG = "MapClientService"; Loading @@ -40,15 +51,15 @@ public class MapClientService extends ProfileService { static final boolean DBG = false; static final boolean VDBG = false; private static final int MAXIMUM_CONNECTED_DEVICES = 1; static final int MAXIMUM_CONNECTED_DEVICES = 4; private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; MceStateMachine mMceStateMachine; private Map<BluetoothDevice, MceStateMachine> mMapInstanceMap = new ConcurrentHashMap<>(1); private MnsService mMnsServer; private BluetoothAdapter mAdapter; private static MapClientService sMapClientService; private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); public static synchronized MapClientService getMapClientService() { if (sMapClientService != null && sMapClientService.isAvailable()) { Loading Loading @@ -76,28 +87,126 @@ public class MapClientService extends ProfileService { } else { if (DBG) { if (sMapClientService == null) { Log.d(TAG, "setA2dpService(): service not available"); Log.d(TAG, "MapClientService service not available"); } else if (!sMapClientService.isAvailable()) { Log.d(TAG, "setA2dpService(): service is cleaning up"); Log.d(TAG, "MapClientService service is cleaning up"); } } } } private static synchronized void clearMapClientService() { sMapClientService = null; } @VisibleForTesting Map<BluetoothDevice, MceStateMachine> getInstanceMap() { return mMapInstanceMap; } /** * Connect the given Bluetooth device. * * @param device * @return true if connection is successful, false otherwise. */ public synchronized boolean connect(BluetoothDevice device) { Log.d(TAG, "MAP Mce connect " + device.toString()); return mMceStateMachine.connect(device); if (device == null) { throw new IllegalArgumentException("Null device"); } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP connect device: " + device + ", InstanceMap start state: " + sb.toString()); } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); if (mapStateMachine == null) { // a map state machine instance doesn't exist yet, create a new one if we can. if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { addDeviceToMapAndConnect(device); return true; } else { // Maxed out on the number of allowed connections. // see if some of the current connections can be cleaned-up, to make room. removeUncleanAccounts(); if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { addDeviceToMapAndConnect(device); return true; } else { Log.e(TAG, "Maxed out on the number of allowed MAP connections. " + "Connect request rejected on " + device); return false; } } } // statemachine already exists in the map. int state = getConnectionState(device); if (state == BluetoothProfile.STATE_CONNECTED || state == BluetoothProfile.STATE_CONNECTING) { Log.w(TAG, "Received connect request while already connecting/connected."); return false; } // Statemachine exists but not in connecting or connected state! it should // have been removed form the map. lets get rid of it and add a new one. if (DBG) { Log.d(TAG, "Statemachine exists for a device in unexpected state: " + state); } mMapInstanceMap.remove(mapStateMachine); addDeviceToMapAndConnect(device); if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP connect device: " + device + ", InstanceMap end state: " + sb.toString()); } return true; } private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) { // When creating a new statemachine, its state is set to CONNECTING - which will trigger // connect. MceStateMachine mapStateMachine = new MceStateMachine(this, device); mMapInstanceMap.put(device, mapStateMachine); } public synchronized boolean disconnect(BluetoothDevice device) { Log.d(TAG, "MAP Mce disconnect " + device.toString()); return mMceStateMachine.disconnect(device); if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP disconnect device: " + device + ", InstanceMap start state: " + sb.toString()); } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); // a map state machine instance doesn't exist. maybe it is already gone? if (mapStateMachine == null) { return false; } int connectionState = mapStateMachine.getState(); if (connectionState != BluetoothProfile.STATE_CONNECTED && connectionState != BluetoothProfile.STATE_CONNECTING) { return false; } mapStateMachine.disconnect(); if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP disconnect device: " + device + ", InstanceMap start state: " + sb.toString()); } return true; } public List<BluetoothDevice> getConnectedDevices() { return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED}); } MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) { return mMapInstanceMap.get(device); } public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); List<BluetoothDevice> deviceList = new ArrayList<>(); Loading @@ -117,11 +226,10 @@ public class MapClientService extends ProfileService { } public synchronized int getConnectionState(BluetoothDevice device) { if (mMceStateMachine != null && device.equals(mMceStateMachine.getDevice())) { return mMceStateMachine.getState(); } else { return BluetoothProfile.STATE_DISCONNECTED; } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); // a map state machine instance doesn't exist yet, create a new one if we can. return (mapStateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED : mapStateMachine.getState(); } public boolean setPriority(BluetoothDevice device, int priority) { Loading @@ -142,12 +250,9 @@ public class MapClientService extends ProfileService { public synchronized boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent) { if (mMceStateMachine != null && device.equals(mMceStateMachine.getDevice())) { return mMceStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent); } else { return false; } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); return mapStateMachine != null && mapStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent); } @Override Loading @@ -157,20 +262,21 @@ public class MapClientService extends ProfileService { @Override protected boolean start() { if (DBG) { Log.d(TAG, "start()"); } Log.e(TAG, "start()"); setService(this); if (mMnsServer == null) { mMnsServer = new MnsService(this); } if (mMceStateMachine == null) { mMceStateMachine = new MceStateMachine(this); mMnsServer = MapUtils.newMnsServiceInstance(this); } mAdapter = BluetoothAdapter.getDefaultAdapter(); mStartError = false; IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); registerReceiver(mMapReceiver, filter); removeUncleanAccounts(); return !mStartError; } Loading @@ -179,35 +285,88 @@ public class MapClientService extends ProfileService { if (DBG) { Log.d(TAG, "stop()"); } unregisterReceiver(mMapReceiver); if (mMnsServer != null) { mMnsServer.stop(); } if (mMceStateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) { mMceStateMachine.disconnect(mMceStateMachine.getDevice()); for (MceStateMachine stateMachine : mMapInstanceMap.values()) { if (stateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) { stateMachine.disconnect(); } stateMachine.doQuit(); } mMceStateMachine.doQuit(); return true; } @Override protected void cleanup() { if (DBG) { Log.d(TAG, "cleanup()"); Log.d(TAG, "in Cleanup"); } removeUncleanAccounts(); clearMapClientService(); } void cleanupDevice(BluetoothDevice device) { if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "Cleanup device: " + device + ", InstanceMap start state: " + sb.toString()); } synchronized (mMapInstanceMap) { MceStateMachine stateMachine = mMapInstanceMap.get(device); if (stateMachine != null) { mMapInstanceMap.remove(device); } } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "Cleanup device: " + device + ", InstanceMap end state: " + sb.toString()); } } @VisibleForTesting void removeUncleanAccounts() { if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: " + sb.toString()); } Iterator iterator = mMapInstanceMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<BluetoothDevice, MceStateMachine> profileConnection = (Map.Entry) iterator.next(); if (profileConnection.getValue().getState() == BluetoothProfile.STATE_DISCONNECTED) { iterator.remove(); } } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: " + sb.toString()); } } public synchronized boolean getUnreadMessages(BluetoothDevice device) { if (mMceStateMachine != null && device.equals(mMceStateMachine.getDevice())) { return mMceStateMachine.getUnreadMessages(); } else { MceStateMachine mapStateMachine = mMapInstanceMap.get(device); if (mapStateMachine == null) { return false; } return mapStateMachine.getUnreadMessages(); } @Override public synchronized void dump(StringBuilder sb) { public void dump(StringBuilder sb) { super.dump(sb); println(sb, "StateMachine: " + mMceStateMachine.toString()); ProfileService.println(sb, "# Services Connected: " + mMapInstanceMap.size()); for (MceStateMachine stateMachine : mMapInstanceMap.values()) { stateMachine.dump(sb); } } //Binder object: Must be static class or memory leak may occur Loading Loading @@ -360,4 +519,62 @@ public class MapClientService extends ProfileService { return service.getUnreadMessages(device); } } private class MapBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DBG) { Log.d(TAG, "onReceive: " + action); } if (!action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) && !action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { // we don't care about this intent return; } BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device == null) { Log.e(TAG, "broadcast has NO device param!"); return; } if (DBG) { Log.d(TAG, "broadcast has device: (" + device.getAddress() + ", " + device.getName() + ")"); } MceStateMachine stateMachine = MapClientService.this.mMapInstanceMap.get(device); if (stateMachine == null) { Log.e(TAG, "No Statemachine found for the device from broadcast"); return; } if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { if (stateMachine.getState() == BluetoothProfile.STATE_CONNECTED) { stateMachine.disconnect(); } } if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); if (DBG) { Log.d(TAG, "UUID of SDP: " + uuid); } if (uuid.equals(BluetoothUuid.MAS)) { // Check if we have a valid SDP record. SdpMasRecord masRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); if (DBG) { Log.d(TAG, "SDP = " + masRecord); } int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); if (masRecord == null) { Log.w(TAG, "SDP search ended with no MAS record. Status: " + status); return; } stateMachine.obtainMessage(MceStateMachine.MSG_MAS_SDP_DONE, masRecord).sendToTarget(); } } } } } android/app/src/com/android/bluetooth/mapclient/MapUtils.java 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.mapclient; import android.support.annotation.VisibleForTesting; class MapUtils { private static MnsService sMnsService = null; @VisibleForTesting static void setMnsService(MnsService service) { sMnsService = service; } static MnsService newMnsServiceInstance(MapClientService mapClientService) { return (sMnsService != null) ? new MnsService(mapClientService) : sMnsService; } } android/app/src/com/android/bluetooth/mapclient/MceStateMachine.java +28 −88 Original line number Diff line number Diff line Loading @@ -47,13 +47,9 @@ import android.bluetooth.BluetoothMapClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.SdpMasRecord; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Message; import android.os.ParcelUuid; import android.telecom.PhoneAccount; import android.util.Log; Loading @@ -66,6 +62,7 @@ import com.android.vcard.VCardEntry; import com.android.vcard.VCardProperty; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; Loading Loading @@ -111,7 +108,7 @@ final class MceStateMachine extends StateMachine { private State mConnected; private State mDisconnecting; private BluetoothDevice mDevice; private final BluetoothDevice mDevice; private MapClientService mService; private MasClient mMasClient; private HashMap<String, Bmessage> mSentMessageLog = new HashMap<>(MAX_MESSAGES); Loading @@ -119,14 +116,14 @@ final class MceStateMachine extends StateMachine { private HashMap<Bmessage, PendingIntent> mDeliveryReceiptRequested = new HashMap<>(MAX_MESSAGES); private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA; private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); MceStateMachine(MapClientService service) { MceStateMachine(MapClientService service, BluetoothDevice device) { super(TAG); mService = service; mPreviousState = BluetoothProfile.STATE_DISCONNECTED; mDevice = device; mDisconnected = new Disconnected(); mConnecting = new Connecting(); mDisconnecting = new Disconnecting(); Loading @@ -136,7 +133,7 @@ final class MceStateMachine extends StateMachine { addState(mConnecting); addState(mDisconnecting); addState(mConnected); setInitialState(mDisconnected); setInitialState(mConnecting); start(); } Loading @@ -144,6 +141,13 @@ final class MceStateMachine extends StateMachine { quitNow(); } @Override protected void onQuitting() { if (mService != null) { mService.cleanupDevice(mDevice); } } synchronized BluetoothDevice getDevice() { return mDevice; } Loading Loading @@ -181,19 +185,11 @@ final class MceStateMachine extends StateMachine { return BluetoothProfile.STATE_DISCONNECTED; } public boolean connect(BluetoothDevice device) { if (DBG) { Log.d(TAG, "Connect Request " + device.getAddress()); } sendMessage(MSG_CONNECT, device); return true; } public boolean disconnect(BluetoothDevice device) { public boolean disconnect() { if (DBG) { Log.d(TAG, "Disconnect Request " + device.getAddress()); Log.d(TAG, "Disconnect Request " + mDevice.getAddress()); } sendMessage(MSG_DISCONNECT, device); sendMessage(MSG_DISCONNECT, mDevice); return true; } Loading Loading @@ -291,6 +287,11 @@ final class MceStateMachine extends StateMachine { } } public void dump(StringBuilder sb) { ProfileService.println(sb, "mCurrentDevice: " + mDevice.getAddress() + " (name = " + mDevice.getName() + "), StateMachine: " + this.toString()); } class Disconnected extends State { @Override public void enter() { Loading @@ -299,24 +300,7 @@ final class MceStateMachine extends StateMachine { } onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTED); mPreviousState = BluetoothProfile.STATE_DISCONNECTED; } @Override public boolean processMessage(Message message) { switch (message.what) { case MSG_CONNECT: synchronized (MceStateMachine.this) { mDevice = (BluetoothDevice) message.obj; } transitionTo(mConnecting); break; default: Log.w(TAG, "Unexpected message: " + message.what + " from state:" + this.getName()); return NOT_HANDLED; } return HANDLED; quit(); } @Override Loading @@ -333,12 +317,6 @@ final class MceStateMachine extends StateMachine { } onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTING); IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); // unregisterReceiver in Disconnecting mService.registerReceiver(mMapReceiver, filter); BluetoothAdapter.getDefaultAdapter().cancelDiscovery(); // When commanded to connect begin SDP to find the MAS server. mDevice.sdpSearch(BluetoothUuid.MAS); Loading Loading @@ -437,11 +415,15 @@ final class MceStateMachine extends StateMachine { break; case MSG_GET_MESSAGE_LISTING: // Get latest 50 Unread messages in the last week MessagesFilter filter = new MessagesFilter(); filter.setMessageType((byte) 0); mMasClient.makeRequest( new RequestGetMessagesListing((String) message.obj, 0, filter, 0, 1, 0)); filter.setReadStatus(MessagesFilter.READ_STATUS_UNREAD); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, -7); filter.setPeriod(calendar.getTime(), null); mMasClient.makeRequest(new RequestGetMessagesListing( (String) message.obj, 0, filter, 0, 50, 0)); break; case MSG_MAS_REQUEST_COMPLETED: Loading Loading @@ -633,7 +615,6 @@ final class MceStateMachine extends StateMachine { Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); } onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTING); mService.unregisterReceiver(mMapReceiver); if (mMasClient != null) { mMasClient.makeRequest(new RequestSetNotificationRegistration(false)); Loading Loading @@ -683,45 +664,4 @@ final class MceStateMachine extends StateMachine { } sendMessage(MSG_NOTIFICATION, ev); } private class MapBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (DBG) { Log.d(TAG, "onReceive"); } String action = intent.getAction(); if (DBG) { Log.d(TAG, "onReceive: " + action); } if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (getDevice().equals(device) && getState() == BluetoothProfile.STATE_CONNECTED) { disconnect(device); } } if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) { ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); if (DBG) { Log.d(TAG, "UUID of SDP: " + uuid); } if (uuid.equals(BluetoothUuid.MAS)) { // Check if we have a valid SDP record. SdpMasRecord masRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); if (DBG) { Log.d(TAG, "SDP = " + masRecord); } int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); if (masRecord == null) { Log.w(TAG, "SDP search ended with no MAS record. Status: " + status); return; } obtainMessage(MceStateMachine.MSG_MAS_SDP_DONE, masRecord).sendToTarget(); } } } } } android/app/src/com/android/bluetooth/mapclient/MnsService.java +7 −1 Original line number Diff line number Diff line Loading @@ -122,7 +122,13 @@ class MnsService { Log.d(TAG, "onConnect" + device + " SOCKET: " + socket); } /* Signal to the service that we have received an incoming connection.*/ MnsObexServer srv = new MnsObexServer(sContext.mMceStateMachine, sServerSockets); MceStateMachine stateMachine = sContext.getMceStateMachineForDevice(device); if (stateMachine == null) { Log.e(TAG, "Error: NO statemachine for device: " + device.getAddress() + " (name: " + device.getName()); return false; } MnsObexServer srv = new MnsObexServer(stateMachine, sServerSockets); BluetoothObexTransport transport = new BluetoothObexTransport(socket); try { new ServerSession(transport, srv, null); Loading android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java 0 → 100644 +152 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
android/app/src/com/android/bluetooth/mapclient/MapClientService.java +253 −36 Original line number Diff line number Diff line Loading @@ -21,9 +21,17 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothMapClient; import android.bluetooth.SdpMasRecord; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.ParcelUuid; import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.bluetooth.Utils; Loading @@ -31,8 +39,11 @@ import com.android.bluetooth.btservice.ProfileService; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class MapClientService extends ProfileService { private static final String TAG = "MapClientService"; Loading @@ -40,15 +51,15 @@ public class MapClientService extends ProfileService { static final boolean DBG = false; static final boolean VDBG = false; private static final int MAXIMUM_CONNECTED_DEVICES = 1; static final int MAXIMUM_CONNECTED_DEVICES = 4; private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; MceStateMachine mMceStateMachine; private Map<BluetoothDevice, MceStateMachine> mMapInstanceMap = new ConcurrentHashMap<>(1); private MnsService mMnsServer; private BluetoothAdapter mAdapter; private static MapClientService sMapClientService; private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); public static synchronized MapClientService getMapClientService() { if (sMapClientService != null && sMapClientService.isAvailable()) { Loading Loading @@ -76,28 +87,126 @@ public class MapClientService extends ProfileService { } else { if (DBG) { if (sMapClientService == null) { Log.d(TAG, "setA2dpService(): service not available"); Log.d(TAG, "MapClientService service not available"); } else if (!sMapClientService.isAvailable()) { Log.d(TAG, "setA2dpService(): service is cleaning up"); Log.d(TAG, "MapClientService service is cleaning up"); } } } } private static synchronized void clearMapClientService() { sMapClientService = null; } @VisibleForTesting Map<BluetoothDevice, MceStateMachine> getInstanceMap() { return mMapInstanceMap; } /** * Connect the given Bluetooth device. * * @param device * @return true if connection is successful, false otherwise. */ public synchronized boolean connect(BluetoothDevice device) { Log.d(TAG, "MAP Mce connect " + device.toString()); return mMceStateMachine.connect(device); if (device == null) { throw new IllegalArgumentException("Null device"); } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP connect device: " + device + ", InstanceMap start state: " + sb.toString()); } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); if (mapStateMachine == null) { // a map state machine instance doesn't exist yet, create a new one if we can. if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { addDeviceToMapAndConnect(device); return true; } else { // Maxed out on the number of allowed connections. // see if some of the current connections can be cleaned-up, to make room. removeUncleanAccounts(); if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { addDeviceToMapAndConnect(device); return true; } else { Log.e(TAG, "Maxed out on the number of allowed MAP connections. " + "Connect request rejected on " + device); return false; } } } // statemachine already exists in the map. int state = getConnectionState(device); if (state == BluetoothProfile.STATE_CONNECTED || state == BluetoothProfile.STATE_CONNECTING) { Log.w(TAG, "Received connect request while already connecting/connected."); return false; } // Statemachine exists but not in connecting or connected state! it should // have been removed form the map. lets get rid of it and add a new one. if (DBG) { Log.d(TAG, "Statemachine exists for a device in unexpected state: " + state); } mMapInstanceMap.remove(mapStateMachine); addDeviceToMapAndConnect(device); if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP connect device: " + device + ", InstanceMap end state: " + sb.toString()); } return true; } private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) { // When creating a new statemachine, its state is set to CONNECTING - which will trigger // connect. MceStateMachine mapStateMachine = new MceStateMachine(this, device); mMapInstanceMap.put(device, mapStateMachine); } public synchronized boolean disconnect(BluetoothDevice device) { Log.d(TAG, "MAP Mce disconnect " + device.toString()); return mMceStateMachine.disconnect(device); if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP disconnect device: " + device + ", InstanceMap start state: " + sb.toString()); } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); // a map state machine instance doesn't exist. maybe it is already gone? if (mapStateMachine == null) { return false; } int connectionState = mapStateMachine.getState(); if (connectionState != BluetoothProfile.STATE_CONNECTED && connectionState != BluetoothProfile.STATE_CONNECTING) { return false; } mapStateMachine.disconnect(); if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "MAP disconnect device: " + device + ", InstanceMap start state: " + sb.toString()); } return true; } public List<BluetoothDevice> getConnectedDevices() { return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED}); } MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) { return mMapInstanceMap.get(device); } public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); List<BluetoothDevice> deviceList = new ArrayList<>(); Loading @@ -117,11 +226,10 @@ public class MapClientService extends ProfileService { } public synchronized int getConnectionState(BluetoothDevice device) { if (mMceStateMachine != null && device.equals(mMceStateMachine.getDevice())) { return mMceStateMachine.getState(); } else { return BluetoothProfile.STATE_DISCONNECTED; } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); // a map state machine instance doesn't exist yet, create a new one if we can. return (mapStateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED : mapStateMachine.getState(); } public boolean setPriority(BluetoothDevice device, int priority) { Loading @@ -142,12 +250,9 @@ public class MapClientService extends ProfileService { public synchronized boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent) { if (mMceStateMachine != null && device.equals(mMceStateMachine.getDevice())) { return mMceStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent); } else { return false; } MceStateMachine mapStateMachine = mMapInstanceMap.get(device); return mapStateMachine != null && mapStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent); } @Override Loading @@ -157,20 +262,21 @@ public class MapClientService extends ProfileService { @Override protected boolean start() { if (DBG) { Log.d(TAG, "start()"); } Log.e(TAG, "start()"); setService(this); if (mMnsServer == null) { mMnsServer = new MnsService(this); } if (mMceStateMachine == null) { mMceStateMachine = new MceStateMachine(this); mMnsServer = MapUtils.newMnsServiceInstance(this); } mAdapter = BluetoothAdapter.getDefaultAdapter(); mStartError = false; IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); registerReceiver(mMapReceiver, filter); removeUncleanAccounts(); return !mStartError; } Loading @@ -179,35 +285,88 @@ public class MapClientService extends ProfileService { if (DBG) { Log.d(TAG, "stop()"); } unregisterReceiver(mMapReceiver); if (mMnsServer != null) { mMnsServer.stop(); } if (mMceStateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) { mMceStateMachine.disconnect(mMceStateMachine.getDevice()); for (MceStateMachine stateMachine : mMapInstanceMap.values()) { if (stateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) { stateMachine.disconnect(); } stateMachine.doQuit(); } mMceStateMachine.doQuit(); return true; } @Override protected void cleanup() { if (DBG) { Log.d(TAG, "cleanup()"); Log.d(TAG, "in Cleanup"); } removeUncleanAccounts(); clearMapClientService(); } void cleanupDevice(BluetoothDevice device) { if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "Cleanup device: " + device + ", InstanceMap start state: " + sb.toString()); } synchronized (mMapInstanceMap) { MceStateMachine stateMachine = mMapInstanceMap.get(device); if (stateMachine != null) { mMapInstanceMap.remove(device); } } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "Cleanup device: " + device + ", InstanceMap end state: " + sb.toString()); } } @VisibleForTesting void removeUncleanAccounts() { if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: " + sb.toString()); } Iterator iterator = mMapInstanceMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<BluetoothDevice, MceStateMachine> profileConnection = (Map.Entry) iterator.next(); if (profileConnection.getValue().getState() == BluetoothProfile.STATE_DISCONNECTED) { iterator.remove(); } } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: " + sb.toString()); } } public synchronized boolean getUnreadMessages(BluetoothDevice device) { if (mMceStateMachine != null && device.equals(mMceStateMachine.getDevice())) { return mMceStateMachine.getUnreadMessages(); } else { MceStateMachine mapStateMachine = mMapInstanceMap.get(device); if (mapStateMachine == null) { return false; } return mapStateMachine.getUnreadMessages(); } @Override public synchronized void dump(StringBuilder sb) { public void dump(StringBuilder sb) { super.dump(sb); println(sb, "StateMachine: " + mMceStateMachine.toString()); ProfileService.println(sb, "# Services Connected: " + mMapInstanceMap.size()); for (MceStateMachine stateMachine : mMapInstanceMap.values()) { stateMachine.dump(sb); } } //Binder object: Must be static class or memory leak may occur Loading Loading @@ -360,4 +519,62 @@ public class MapClientService extends ProfileService { return service.getUnreadMessages(device); } } private class MapBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DBG) { Log.d(TAG, "onReceive: " + action); } if (!action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) && !action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { // we don't care about this intent return; } BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device == null) { Log.e(TAG, "broadcast has NO device param!"); return; } if (DBG) { Log.d(TAG, "broadcast has device: (" + device.getAddress() + ", " + device.getName() + ")"); } MceStateMachine stateMachine = MapClientService.this.mMapInstanceMap.get(device); if (stateMachine == null) { Log.e(TAG, "No Statemachine found for the device from broadcast"); return; } if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { if (stateMachine.getState() == BluetoothProfile.STATE_CONNECTED) { stateMachine.disconnect(); } } if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); if (DBG) { Log.d(TAG, "UUID of SDP: " + uuid); } if (uuid.equals(BluetoothUuid.MAS)) { // Check if we have a valid SDP record. SdpMasRecord masRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); if (DBG) { Log.d(TAG, "SDP = " + masRecord); } int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); if (masRecord == null) { Log.w(TAG, "SDP search ended with no MAS record. Status: " + status); return; } stateMachine.obtainMessage(MceStateMachine.MSG_MAS_SDP_DONE, masRecord).sendToTarget(); } } } } }
android/app/src/com/android/bluetooth/mapclient/MapUtils.java 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.mapclient; import android.support.annotation.VisibleForTesting; class MapUtils { private static MnsService sMnsService = null; @VisibleForTesting static void setMnsService(MnsService service) { sMnsService = service; } static MnsService newMnsServiceInstance(MapClientService mapClientService) { return (sMnsService != null) ? new MnsService(mapClientService) : sMnsService; } }
android/app/src/com/android/bluetooth/mapclient/MceStateMachine.java +28 −88 Original line number Diff line number Diff line Loading @@ -47,13 +47,9 @@ import android.bluetooth.BluetoothMapClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.SdpMasRecord; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Message; import android.os.ParcelUuid; import android.telecom.PhoneAccount; import android.util.Log; Loading @@ -66,6 +62,7 @@ import com.android.vcard.VCardEntry; import com.android.vcard.VCardProperty; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; Loading Loading @@ -111,7 +108,7 @@ final class MceStateMachine extends StateMachine { private State mConnected; private State mDisconnecting; private BluetoothDevice mDevice; private final BluetoothDevice mDevice; private MapClientService mService; private MasClient mMasClient; private HashMap<String, Bmessage> mSentMessageLog = new HashMap<>(MAX_MESSAGES); Loading @@ -119,14 +116,14 @@ final class MceStateMachine extends StateMachine { private HashMap<Bmessage, PendingIntent> mDeliveryReceiptRequested = new HashMap<>(MAX_MESSAGES); private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA; private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); MceStateMachine(MapClientService service) { MceStateMachine(MapClientService service, BluetoothDevice device) { super(TAG); mService = service; mPreviousState = BluetoothProfile.STATE_DISCONNECTED; mDevice = device; mDisconnected = new Disconnected(); mConnecting = new Connecting(); mDisconnecting = new Disconnecting(); Loading @@ -136,7 +133,7 @@ final class MceStateMachine extends StateMachine { addState(mConnecting); addState(mDisconnecting); addState(mConnected); setInitialState(mDisconnected); setInitialState(mConnecting); start(); } Loading @@ -144,6 +141,13 @@ final class MceStateMachine extends StateMachine { quitNow(); } @Override protected void onQuitting() { if (mService != null) { mService.cleanupDevice(mDevice); } } synchronized BluetoothDevice getDevice() { return mDevice; } Loading Loading @@ -181,19 +185,11 @@ final class MceStateMachine extends StateMachine { return BluetoothProfile.STATE_DISCONNECTED; } public boolean connect(BluetoothDevice device) { if (DBG) { Log.d(TAG, "Connect Request " + device.getAddress()); } sendMessage(MSG_CONNECT, device); return true; } public boolean disconnect(BluetoothDevice device) { public boolean disconnect() { if (DBG) { Log.d(TAG, "Disconnect Request " + device.getAddress()); Log.d(TAG, "Disconnect Request " + mDevice.getAddress()); } sendMessage(MSG_DISCONNECT, device); sendMessage(MSG_DISCONNECT, mDevice); return true; } Loading Loading @@ -291,6 +287,11 @@ final class MceStateMachine extends StateMachine { } } public void dump(StringBuilder sb) { ProfileService.println(sb, "mCurrentDevice: " + mDevice.getAddress() + " (name = " + mDevice.getName() + "), StateMachine: " + this.toString()); } class Disconnected extends State { @Override public void enter() { Loading @@ -299,24 +300,7 @@ final class MceStateMachine extends StateMachine { } onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTED); mPreviousState = BluetoothProfile.STATE_DISCONNECTED; } @Override public boolean processMessage(Message message) { switch (message.what) { case MSG_CONNECT: synchronized (MceStateMachine.this) { mDevice = (BluetoothDevice) message.obj; } transitionTo(mConnecting); break; default: Log.w(TAG, "Unexpected message: " + message.what + " from state:" + this.getName()); return NOT_HANDLED; } return HANDLED; quit(); } @Override Loading @@ -333,12 +317,6 @@ final class MceStateMachine extends StateMachine { } onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTING); IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); // unregisterReceiver in Disconnecting mService.registerReceiver(mMapReceiver, filter); BluetoothAdapter.getDefaultAdapter().cancelDiscovery(); // When commanded to connect begin SDP to find the MAS server. mDevice.sdpSearch(BluetoothUuid.MAS); Loading Loading @@ -437,11 +415,15 @@ final class MceStateMachine extends StateMachine { break; case MSG_GET_MESSAGE_LISTING: // Get latest 50 Unread messages in the last week MessagesFilter filter = new MessagesFilter(); filter.setMessageType((byte) 0); mMasClient.makeRequest( new RequestGetMessagesListing((String) message.obj, 0, filter, 0, 1, 0)); filter.setReadStatus(MessagesFilter.READ_STATUS_UNREAD); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, -7); filter.setPeriod(calendar.getTime(), null); mMasClient.makeRequest(new RequestGetMessagesListing( (String) message.obj, 0, filter, 0, 50, 0)); break; case MSG_MAS_REQUEST_COMPLETED: Loading Loading @@ -633,7 +615,6 @@ final class MceStateMachine extends StateMachine { Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); } onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTING); mService.unregisterReceiver(mMapReceiver); if (mMasClient != null) { mMasClient.makeRequest(new RequestSetNotificationRegistration(false)); Loading Loading @@ -683,45 +664,4 @@ final class MceStateMachine extends StateMachine { } sendMessage(MSG_NOTIFICATION, ev); } private class MapBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (DBG) { Log.d(TAG, "onReceive"); } String action = intent.getAction(); if (DBG) { Log.d(TAG, "onReceive: " + action); } if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (getDevice().equals(device) && getState() == BluetoothProfile.STATE_CONNECTED) { disconnect(device); } } if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) { ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); if (DBG) { Log.d(TAG, "UUID of SDP: " + uuid); } if (uuid.equals(BluetoothUuid.MAS)) { // Check if we have a valid SDP record. SdpMasRecord masRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); if (DBG) { Log.d(TAG, "SDP = " + masRecord); } int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); if (masRecord == null) { Log.w(TAG, "SDP search ended with no MAS record. Status: " + status); return; } obtainMessage(MceStateMachine.MSG_MAS_SDP_DONE, masRecord).sendToTarget(); } } } } }
android/app/src/com/android/bluetooth/mapclient/MnsService.java +7 −1 Original line number Diff line number Diff line Loading @@ -122,7 +122,13 @@ class MnsService { Log.d(TAG, "onConnect" + device + " SOCKET: " + socket); } /* Signal to the service that we have received an incoming connection.*/ MnsObexServer srv = new MnsObexServer(sContext.mMceStateMachine, sServerSockets); MceStateMachine stateMachine = sContext.getMceStateMachineForDevice(device); if (stateMachine == null) { Log.e(TAG, "Error: NO statemachine for device: " + device.getAddress() + " (name: " + device.getName()); return false; } MnsObexServer srv = new MnsObexServer(stateMachine, sServerSockets); BluetoothObexTransport transport = new BluetoothObexTransport(socket); try { new ServerSession(transport, srv, null); Loading
android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java 0 → 100644 +152 −0 File added.Preview size limit exceeded, changes collapsed. Show changes