| package com.google.android.connecteddevice.service; |
| |
| import static com.google.android.connecteddevice.util.SafeLog.logd; |
| import static com.google.android.connecteddevice.util.SafeLog.loge; |
| import static com.google.android.connecteddevice.util.SafeLog.logw; |
| |
| import android.app.Service; |
| import android.bluetooth.BluetoothDevice; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.ParcelUuid; |
| import android.os.RemoteException; |
| import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder; |
| import com.google.android.connecteddevice.transport.spp.Connection; |
| import com.google.android.connecteddevice.transport.spp.IConnectedDeviceSppDelegate; |
| import com.google.android.connecteddevice.transport.spp.ISppCallback; |
| import com.google.android.connecteddevice.transport.spp.PendingConnection; |
| import com.google.android.connecteddevice.transport.spp.PendingSentMessage; |
| import com.google.android.connecteddevice.transport.spp.SppManager; |
| import java.time.Duration; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.Executors; |
| |
| /** |
| * Early start service that will hold a {@link IConnectedDeviceSppDelegate} reference to pass |
| * bluetooth socket connection related events to {@link ConnectedDeviceService}. |
| */ |
| public class SppService extends Service { |
| private static final String TAG = "SppService"; |
| private static final int MAX_BIND_ATTEMPTS = 3; |
| |
| private static final Duration BIND_RETRY_DURATION = Duration.ofSeconds(1); |
| |
| private final ConcurrentHashMap<Connection, SppManager> activeConnections = |
| new ConcurrentHashMap<>(); |
| // Map from a PendingConnection's unique identifier to its associated sppManager. |
| private final ConcurrentHashMap<Integer, SppManager> pendingConnections = |
| new ConcurrentHashMap<>(); |
| |
| private int bindAttempts; |
| private IConnectedDeviceSppDelegate delegate; |
| |
| private final Executor callbackExecutor = Executors.newSingleThreadExecutor(); |
| private final ISppCallback sppCallback = |
| new ISppCallback.Stub() { |
| @Override |
| public boolean onStartConnectionAsServerRequested( |
| ParcelUuid serviceUuid, boolean isSecure) { |
| return startConnectionAsServer(serviceUuid, isSecure); |
| } |
| |
| @Override |
| public void onStartConnectionAsClientRequested( |
| ParcelUuid serviceUuid, BluetoothDevice remoteDevice, boolean isSecure) { |
| startConnectionAsClient(serviceUuid, remoteDevice, isSecure); |
| } |
| |
| @Override |
| public PendingSentMessage onSendMessageRequested(Connection connection, byte[] message) { |
| PendingSentMessage pendingSentMessage = new PendingSentMessage(); |
| sendMessage(connection, message, pendingSentMessage); |
| return pendingSentMessage; |
| } |
| |
| @Override |
| public void onDisconnectRequested(Connection connection) { |
| SppManager manager = activeConnections.get(connection); |
| if (manager == null) { |
| loge(TAG, "Can not find suitable manager to disconnect the remote device. Ignored."); |
| return; |
| } |
| manager.cleanup(); |
| } |
| |
| @Override |
| public void onCancelConnectionAttemptRequested(int pendingConnectionId) { |
| SppManager manager = pendingConnections.get(pendingConnectionId); |
| if (manager == null) { |
| loge( |
| TAG, |
| "Can not find suitable manager related to pending connection with id " |
| + pendingConnectionId |
| + "to cancel connection attempt. Ignored."); |
| return; |
| } |
| manager.cleanup(); |
| pendingConnections.remove(pendingConnectionId); |
| } |
| }; |
| |
| private final ServiceConnection serviceConnection = |
| new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| logd(TAG, "Connected to remote service."); |
| delegate = IConnectedDeviceSppDelegate.Stub.asInterface(service); |
| try { |
| delegate.setCallback(sppCallback); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while set callback to delegate.", e); |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName name) { |
| logd(TAG, "Disconnected to remote service."); |
| // TODO(b/170164323) Need to handle the service disconnect situation. |
| cleanUp(); |
| } |
| }; |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| logd(TAG, "Service is created, start binding to CompanionDeviceSupportService."); |
| bindAttempts = 0; |
| bindToService(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| cleanUp(); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return null; |
| } |
| |
| private void cleanUp() { |
| for (SppManager manager : activeConnections.values()) { |
| manager.cleanup(); |
| } |
| activeConnections.clear(); |
| pendingConnections.clear(); |
| if (delegate == null) { |
| return; |
| } |
| try { |
| delegate.clearCallback(); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while clear callback of delegate.", e); |
| } |
| delegate = null; |
| } |
| |
| private void bindToService() { |
| Intent intent = new Intent(this, ConnectedDeviceService.class); |
| intent.setAction(ConnectedDeviceSppDelegateBinder.ACTION_BIND_SPP); |
| boolean success = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); |
| if (success) { |
| logd(TAG, "Successfully bind to CompanionDeviceSupportService"); |
| return; |
| } |
| bindAttempts++; |
| if (bindAttempts > MAX_BIND_ATTEMPTS) { |
| loge( |
| TAG, |
| "Failed to bind to CompanionDeviceSupportService after " |
| + bindAttempts |
| + " attempts. Aborting."); |
| return; |
| } |
| logw( |
| TAG, |
| "Unable to bind to CompanionDeviceSupportService. Trying again with a duration of " |
| + BIND_RETRY_DURATION.toMillis() |
| + " milliseconds."); |
| new Handler(Looper.getMainLooper()) |
| .postDelayed(this::bindToService, BIND_RETRY_DURATION.toMillis()); |
| } |
| |
| private boolean startConnectionAsServer(ParcelUuid serviceUuid, boolean isSecure) { |
| SppManager manager = new SppManager(isSecure); |
| PendingConnection pendingConnection = new PendingConnection(serviceUuid.getUuid(), isSecure); |
| pendingConnections.put(pendingConnection.getId(), manager); |
| manager.registerCallback( |
| generateConnectionCallback(manager, serviceUuid, isSecure), callbackExecutor); |
| return manager.startListening(serviceUuid.getUuid()); |
| } |
| |
| private void startConnectionAsClient( |
| ParcelUuid serviceUuid, BluetoothDevice device, boolean isSecure) { |
| SppManager manager = new SppManager(isSecure); |
| PendingConnection pendingConnection = |
| new PendingConnection(serviceUuid.getUuid(), device, isSecure); |
| pendingConnections.put(pendingConnection.getId(), manager); |
| |
| manager.registerCallback( |
| generateConnectionCallback(manager, serviceUuid, isSecure), callbackExecutor); |
| manager.connect(device, serviceUuid.getUuid()); |
| } |
| |
| private boolean sendMessage( |
| Connection connection, byte[] message, PendingSentMessage pendingSentMessage) { |
| SppManager manager = activeConnections.get(connection); |
| if (manager == null) { |
| loge(TAG, "Can not find suitable manager for the connection to send message. Ignore."); |
| return false; |
| } |
| return manager.write(message, pendingSentMessage); |
| } |
| |
| private SppManager.ConnectionCallback generateConnectionCallback( |
| SppManager manager, ParcelUuid uuid, boolean isSecure) { |
| return new SppManager.ConnectionCallback() { |
| @Override |
| public void onRemoteDeviceConnected(BluetoothDevice device) { |
| Connection connection = new Connection(uuid, device, isSecure, device.getName()); |
| activeConnections.put(connection, manager); |
| manager.addOnMessageReceivedListener( |
| generateMessageReceivedListener(connection), callbackExecutor); |
| try { |
| // TODO(b/170164323) Need to handle the service disconnect situation. |
| for (Map.Entry<Integer, SppManager> entry : pendingConnections.entrySet()) { |
| if (entry.getValue() == manager) { |
| pendingConnections.remove(entry.getKey()); |
| if (delegate == null) { |
| logw(TAG, "Remote device connected with no delegate set"); |
| } else { |
| delegate.notifyConnected(entry.getKey(), device, device.getName()); |
| } |
| break; |
| } |
| } |
| } catch (RemoteException e) { |
| loge(TAG, "Error while notify remote device connected.", e); |
| } |
| } |
| |
| @Override |
| public void onRemoteDeviceDisconnected(BluetoothDevice device) { |
| logd(TAG, "Remote device disconnected"); |
| if (delegate == null) { |
| logw(TAG, "Remote device disconnected with no delegate set"); |
| activeConnections.values().removeIf(value -> value == manager); |
| return; |
| } |
| IConnectedDeviceSppDelegate localDelegate = delegate; |
| |
| for (Connection connection : activeConnections.keySet()) { |
| if (activeConnections.get(connection) != manager) { |
| continue; |
| } |
| try { |
| // TODO(b/170164323) Need to handle the service disconnect situation. |
| localDelegate.notifyError(connection); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while notify remote device disconnected.", e); |
| } |
| break; |
| } |
| activeConnections.values().removeIf(value -> value == manager); |
| } |
| }; |
| } |
| |
| private SppManager.OnMessageReceivedListener generateMessageReceivedListener( |
| Connection connection) { |
| return (device, message) -> { |
| if (delegate == null) { |
| logw(TAG, "Received message from remote device with no delegate set"); |
| return; |
| } |
| |
| try { |
| delegate.notifyMessageReceived(connection, message); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while notify remote message received.", e); |
| } |
| }; |
| } |
| } |