From e0a6ca64fac5bd4f10139321604031816e90adb4 Mon Sep 17 00:00:00 2001
From: Mike Lockwood An app can provide a MIDI Service that can be used by other apps. For example,
-an app can provide a custom synthesizer that other apps can send messages to.
-<service android:name="MySynthDeviceService"> +<service android:name="MySynthDeviceService" + android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"> <intent-filter> <action android:name="android.media.midi.MidiDeviceService" /> </intent-filter> diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml index 15aa581..b0b389a 100644 --- a/media/packages/BluetoothMidiService/AndroidManifest.xml +++ b/media/packages/BluetoothMidiService/AndroidManifest.xml @@ -8,7 +8,8 @@- + diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java index 60c6570..e6d59e4 100644 --- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java @@ -24,10 +24,11 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; import android.content.Context; -import android.media.midi.MidiReceiver; -import android.media.midi.MidiManager; -import android.media.midi.MidiDeviceServer; import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceServer; +import android.media.midi.MidiDeviceStatus; +import android.media.midi.MidiManager; +import android.media.midi.MidiReceiver; import android.os.Bundle; import android.os.IBinder; import android.util.Log; @@ -81,6 +82,18 @@ public final class BluetoothMidiDevice { private final BluetoothPacketDecoder mPacketDecoder = new BluetoothPacketDecoder(MAX_PACKET_SIZE); + private final MidiDeviceServer.Callback mDeviceServerCallback + = new MidiDeviceServer.Callback() { + @Override + public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { + } + + @Override + public void onClose() { + close(); + } + }; + private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, @@ -213,7 +226,7 @@ public final class BluetoothMidiDevice { inputPortReceivers[0] = mEventScheduler.getReceiver(); mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1, - null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, null); + null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback); mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0]; @@ -248,11 +261,12 @@ public final class BluetoothMidiDevice { private void close() { synchronized (mBluetoothDevice) { - mEventScheduler.close(); + mEventScheduler.close(); + mService.deviceClosed(mBluetoothDevice); + if (mDeviceServer != null) { IoUtils.closeQuietly(mDeviceServer); mDeviceServer = null; - mService.deviceClosed(mBluetoothDevice); } if (mBluetoothGatt != null) { mBluetoothGatt.close(); diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index 370f125..d1bbbfc 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -16,8 +16,11 @@ package com.android.server.midi; +import android.bluetooth.BluetoothDevice; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -25,11 +28,13 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.XmlResourceParser; import android.media.midi.IMidiDeviceListener; +import android.media.midi.IMidiDeviceOpenCallback; import android.media.midi.IMidiDeviceServer; import android.media.midi.IMidiManager; import android.media.midi.MidiDeviceInfo; import android.media.midi.MidiDeviceService; import android.media.midi.MidiDeviceStatus; +import android.media.midi.MidiManager; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -78,6 +83,10 @@ public class MidiService extends IMidiManager.Stub { private final HashMap mDevicesByInfo = new HashMap (); + // list of all Bluetooth devices, keyed by BluetoothDevice + private final HashMap mBluetoothDevices + = new HashMap (); + // list of all devices, keyed by IMidiDeviceServer private final HashMap mDevicesByServer = new HashMap (); @@ -86,6 +95,9 @@ public class MidiService extends IMidiManager.Stub { private final PackageManager mPackageManager; + // UID of BluetoothMidiService + private final int mBluetoothServiceUid; + // PackageMonitor for listening to package changes private final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override @@ -115,6 +127,9 @@ public class MidiService extends IMidiManager.Stub { // List of all receivers for this client private final ArrayList mListeners = new ArrayList (); + // List of all device connections for this client + private final HashMap mDeviceConnections + = new HashMap (); public Client(IBinder token) { mToken = token; @@ -132,8 +147,33 @@ public class MidiService extends IMidiManager.Stub { public void removeListener(IMidiDeviceListener listener) { mListeners.remove(listener); - if (mListeners.size() == 0) { - removeClient(mToken); + if (mListeners.size() == 0 && mDeviceConnections.size() == 0) { + close(); + } + } + + public void addDeviceConnection(Device device, IMidiDeviceOpenCallback callback) { + DeviceConnection connection = new DeviceConnection(device, this, callback); + mDeviceConnections.put(connection.getToken(), connection); + device.addDeviceConnection(connection); + } + + // called from MidiService.closeDevice() + public void removeDeviceConnection(IBinder token) { + DeviceConnection connection = mDeviceConnections.remove(token); + if (connection != null) { + connection.getDevice().removeDeviceConnection(connection); + } + if (mListeners.size() == 0 && mDeviceConnections.size() == 0) { + close(); + } + } + + // called from Device.close() + public void removeDeviceConnection(DeviceConnection connection) { + mDeviceConnections.remove(connection.getToken()); + if (mListeners.size() == 0 && mDeviceConnections.size() == 0) { + close(); } } @@ -178,8 +218,21 @@ public class MidiService extends IMidiManager.Stub { } } + private void close() { + synchronized (mClients) { + mClients.remove(mToken); + mToken.unlinkToDeath(this, 0); + } + + for (DeviceConnection connection : mDeviceConnections.values()) { + connection.getDevice().removeDeviceConnection(connection); + } + } + + @Override public void binderDied() { - removeClient(mToken); + Log.d(TAG, "Client died: " + this); + close(); } @Override @@ -190,6 +243,12 @@ public class MidiService extends IMidiManager.Stub { sb.append(mPid); sb.append(" listener count: "); sb.append(mListeners.size()); + sb.append(" Device Connections:"); + for (DeviceConnection connection : mDeviceConnections.values()) { + sb.append(" "); + } return sb.toString(); } } @@ -211,57 +270,96 @@ public class MidiService extends IMidiManager.Stub { } } - private void removeClient(IBinder token) { - mClients.remove(token); - } - private final class Device implements IBinder.DeathRecipient { - private final IMidiDeviceServer mServer; - private final MidiDeviceInfo mDeviceInfo; + private IMidiDeviceServer mServer; + private MidiDeviceInfo mDeviceInfo; + private final BluetoothDevice mBluetoothDevice; private MidiDeviceStatus mDeviceStatus; - private IBinder mDeviceStatusToken; + // ServiceInfo for the device's MidiDeviceServer implementation (virtual devices only) private final ServiceInfo mServiceInfo; // UID of device implementation private final int mUid; + // ServiceConnection for implementing Service (virtual devices only) + // mServiceConnection is non-null when connected or attempting to connect to the service + private ServiceConnection mServiceConnection; + + // List of all device connections for this device + private final ArrayList mDeviceConnections + = new ArrayList (); + public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo, ServiceInfo serviceInfo, int uid) { - mServer = server; mDeviceInfo = deviceInfo; mServiceInfo = serviceInfo; mUid = uid; + mBluetoothDevice = (BluetoothDevice)deviceInfo.getProperties().getParcelable( + MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE);; + setDeviceServer(server); + } + + public Device(BluetoothDevice bluetoothDevice) { + mBluetoothDevice = bluetoothDevice; + mServiceInfo = null; + mUid = mBluetoothServiceUid; + } + + private void setDeviceServer(IMidiDeviceServer server) { + if (server != null) { + if (mServer != null) { + Log.e(TAG, "mServer already set in setDeviceServer"); + return; + } + IBinder binder = server.asBinder(); + try { + if (mDeviceInfo == null) { + mDeviceInfo = server.getDeviceInfo(); + } + binder.linkToDeath(this, 0); + mServer = server; + } catch (RemoteException e) { + mServer = null; + return; + } + mDevicesByServer.put(binder, this); + } else if (mServer != null) { + server = mServer; + mServer = null; + + IBinder binder = server.asBinder(); + mDevicesByServer.remove(binder); + + try { + server.closeDevice(); + binder.unlinkToDeath(this, 0); + } catch (RemoteException e) { + // nothing to do here + } + } + + if (mDeviceConnections != null) { + for (DeviceConnection connection : mDeviceConnections) { + connection.notifyClient(server); + } + } } public MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; } + // only used for bluetooth devices, which are created before we have a MidiDeviceInfo + public void setDeviceInfo(MidiDeviceInfo deviceInfo) { + mDeviceInfo = deviceInfo; + } + public MidiDeviceStatus getDeviceStatus() { return mDeviceStatus; } - public void setDeviceStatus(IBinder token, MidiDeviceStatus status) { + public void setDeviceStatus(MidiDeviceStatus status) { mDeviceStatus = status; - - if (mDeviceStatusToken == null && token != null) { - // register a death recipient so we can clear the status when the device dies - try { - token.linkToDeath(new IBinder.DeathRecipient() { - @Override - public void binderDied() { - // reset to default status and clear the token - mDeviceStatus = new MidiDeviceStatus(mDeviceInfo); - mDeviceStatusToken = null; - notifyDeviceStatusChanged(Device.this, mDeviceStatus); - } - }, 0); - mDeviceStatusToken = token; - } catch (RemoteException e) { - // reset to default status - mDeviceStatus = new MidiDeviceStatus(mDeviceInfo); - } - } } public IMidiDeviceServer getDeviceServer() { @@ -284,11 +382,102 @@ public class MidiService extends IMidiManager.Stub { return (!mDeviceInfo.isPrivate() || mUid == uid); } + public void addDeviceConnection(DeviceConnection connection) { + synchronized (mDeviceConnections) { + if (mServer != null) { + mDeviceConnections.add(connection); + connection.notifyClient(mServer); + } else if (mServiceConnection == null && + (mServiceInfo != null || mBluetoothDevice != null)) { + mDeviceConnections.add(connection); + + mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + IMidiDeviceServer server = IMidiDeviceServer.Stub.asInterface(service); + setDeviceServer(server); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + setDeviceServer(null); + mServiceConnection = null; + } + }; + + Intent intent; + if (mBluetoothDevice != null) { + intent = new Intent(MidiManager.BLUETOOTH_MIDI_SERVICE_INTENT); + intent.setComponent(new ComponentName( + MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE, + MidiManager.BLUETOOTH_MIDI_SERVICE_CLASS)); + intent.putExtra("device", mBluetoothDevice); + } else { + intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); + intent.setComponent( + new ComponentName(mServiceInfo.packageName, mServiceInfo.name)); + } + + if (!mContext.bindService(intent, mServiceConnection, + Context.BIND_AUTO_CREATE)) { + Log.e(TAG, "Unable to bind service: " + intent); + setDeviceServer(null); + mServiceConnection = null; + } + } else { + Log.e(TAG, "No way to connect to device in addDeviceConnection"); + connection.notifyClient(null); + } + } + } + + public void removeDeviceConnection(DeviceConnection connection) { + synchronized (mDeviceConnections) { + mDeviceConnections.remove(connection); + + if (mDeviceConnections.size() == 0 && mServiceConnection != null) { + mContext.unbindService(mServiceConnection); + mServiceConnection = null; + if (mBluetoothDevice != null) { + // Bluetooth devices are ephemeral - remove when no clients exist + synchronized (mDevicesByInfo) { + closeLocked(); + } + } else { + setDeviceServer(null); + } + } + } + } + + // synchronize on mDevicesByInfo + public void closeLocked() { + synchronized (mDeviceConnections) { + for (DeviceConnection connection : mDeviceConnections) { + connection.getClient().removeDeviceConnection(connection); + } + mDeviceConnections.clear(); + } + setDeviceServer(null); + + // closed virtual devices should not be removed from mDevicesByInfo + // since they can be restarted on demand + if (mServiceInfo == null) { + removeDeviceLocked(this); + } else { + mDeviceStatus = new MidiDeviceStatus(mDeviceInfo); + } + + if (mBluetoothDevice != null) { + mBluetoothDevices.remove(mBluetoothDevice); + } + } + + @Override public void binderDied() { + Log.d(TAG, "Device died: " + this); synchronized (mDevicesByInfo) { - if (mDevicesByInfo.remove(mDeviceInfo) != null) { - removeDeviceLocked(this); - } + closeLocked(); } } @@ -300,10 +489,56 @@ public class MidiService extends IMidiManager.Stub { sb.append(mDeviceStatus); sb.append(" UID: "); sb.append(mUid); + sb.append(" DeviceConnection count: "); + sb.append(mDeviceConnections.size()); + sb.append(" mServiceConnection: "); + sb.append(mServiceConnection); return sb.toString(); } } + // Represents a connection between a client and a device + private final class DeviceConnection { + private final IBinder mToken = new Binder(); + private final Device mDevice; + private final Client mClient; + private IMidiDeviceOpenCallback mCallback; + + public DeviceConnection(Device device, Client client, IMidiDeviceOpenCallback callback) { + mDevice = device; + mClient = client; + mCallback = callback; + } + + public Device getDevice() { + return mDevice; + } + + public Client getClient() { + return mClient; + } + + public IBinder getToken() { + return mToken; + } + + public void notifyClient(IMidiDeviceServer deviceServer) { + if (mCallback != null) { + try { + mCallback.onDeviceOpened(deviceServer, (deviceServer == null ? null : mToken)); + } catch (RemoteException e) { + // Client binderDied() method will do necessary cleanup, so nothing to do here + } + mCallback = null; + } + } + + @Override + public String toString() { + return "DeviceConnection Device ID: " + mDevice.getDeviceInfo().getId(); + } + } + public MidiService(Context context) { mContext = context; mPackageManager = context.getPackageManager(); @@ -321,6 +556,18 @@ public class MidiService extends IMidiManager.Stub { } } } + + PackageInfo info; + try { + info = mPackageManager.getPackageInfo(MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE, 0); + } catch (PackageManager.NameNotFoundException e) { + info = null; + } + if (info != null && info.applicationInfo != null) { + mBluetoothServiceUid = info.applicationInfo.uid; + } else { + mBluetoothServiceUid = -1; + } } @Override @@ -355,18 +602,61 @@ public class MidiService extends IMidiManager.Stub { } @Override - public IMidiDeviceServer openDevice(IBinder token, MidiDeviceInfo deviceInfo) { - Device device = mDevicesByInfo.get(deviceInfo); - if (device == null) { - Log.e(TAG, "device not found in openDevice: " + deviceInfo); - return null; + public void openDevice(IBinder token, MidiDeviceInfo deviceInfo, + IMidiDeviceOpenCallback callback) { + Client client = getClient(token); + if (client == null) return; + + Device device; + synchronized (mDevicesByInfo) { + device = mDevicesByInfo.get(deviceInfo); + if (device == null) { + throw new IllegalArgumentException("device does not exist: " + deviceInfo); + } + if (!device.isUidAllowed(Binder.getCallingUid())) { + throw new SecurityException("Attempt to open private device with wrong UID"); + } + } + + // clear calling identity so bindService does not fail + long identity = Binder.clearCallingIdentity(); + try { + client.addDeviceConnection(device, callback); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void openBluetoothDevice(IBinder token, BluetoothDevice bluetoothDevice, + IMidiDeviceOpenCallback callback) { + Client client = getClient(token); + if (client == null) return; + + // Bluetooth devices are created on demand + Device device; + synchronized (mDevicesByInfo) { + device = mBluetoothDevices.get(bluetoothDevice); + if (device == null) { + device = new Device(bluetoothDevice); + mBluetoothDevices.put(bluetoothDevice, device); + } } - if (!device.isUidAllowed(Binder.getCallingUid())) { - throw new SecurityException("Attempt to open private device with wrong UID"); + // clear calling identity so bindService does not fail + long identity = Binder.clearCallingIdentity(); + try { + client.addDeviceConnection(device, callback); + } finally { + Binder.restoreCallingIdentity(identity); } + } - return device.getDeviceServer(); + @Override + public void closeDevice(IBinder clientToken, IBinder deviceToken) { + Client client = getClient(clientToken); + if (client == null) return; + client.removeDeviceConnection(deviceToken); } @Override @@ -376,6 +666,8 @@ public class MidiService extends IMidiManager.Stub { int uid = Binder.getCallingUid(); if (type == MidiDeviceInfo.TYPE_USB && uid != Process.SYSTEM_UID) { throw new SecurityException("only system can create USB devices"); + } else if (type == MidiDeviceInfo.TYPE_BLUETOOTH && uid != mBluetoothServiceUid) { + throw new SecurityException("only MidiBluetoothService can create Bluetooth devices"); } synchronized (mDevicesByInfo) { @@ -389,8 +681,7 @@ public class MidiService extends IMidiManager.Stub { synchronized (mDevicesByInfo) { Device device = mDevicesByServer.get(server.asBinder()); if (device != null) { - mDevicesByInfo.remove(device.getDeviceInfo()); - removeDeviceLocked(device); + device.closeLocked(); } } } @@ -420,19 +711,16 @@ public class MidiService extends IMidiManager.Stub { } @Override - public void setDeviceStatus(IBinder token, MidiDeviceStatus status) { - MidiDeviceInfo deviceInfo = status.getDeviceInfo(); - Device device = mDevicesByInfo.get(deviceInfo); - if (device == null) { - // Just return quietly here if device no longer exists - return; - } - if (Binder.getCallingUid() != device.getUid()) { - throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid() - + " does not match device's UID " + device.getUid()); + public void setDeviceStatus(IMidiDeviceServer server, MidiDeviceStatus status) { + Device device = mDevicesByServer.get(server.asBinder()); + if (device != null) { + if (Binder.getCallingUid() != device.getUid()) { + throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid() + + " does not match device's UID " + device.getUid()); + } + device.setDeviceStatus(status); + notifyDeviceStatusChanged(device, status); } - device.setDeviceStatus(token, status); - notifyDeviceStatusChanged(device, status); } private void notifyDeviceStatusChanged(Device device, MidiDeviceStatus status) { @@ -452,18 +740,24 @@ public class MidiService extends IMidiManager.Stub { int id = mNextDeviceId++; MidiDeviceInfo deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts, inputPortNames, outputPortNames, properties, isPrivate); - Device device = new Device(server, deviceInfo, serviceInfo, uid); - if (server != null) { - IBinder binder = server.asBinder(); - try { - binder.linkToDeath(device, 0); - } catch (RemoteException e) { - return null; + Device device = null; + BluetoothDevice bluetoothDevice = null; + if (type == MidiDeviceInfo.TYPE_BLUETOOTH) { + bluetoothDevice = (BluetoothDevice)properties.getParcelable( + MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE); + device = mBluetoothDevices.get(bluetoothDevice); + if (device != null) { + device.setDeviceInfo(deviceInfo); } - mDevicesByServer.put(binder, device); + } + if (device == null) { + device = new Device(server, deviceInfo, serviceInfo, uid); } mDevicesByInfo.put(deviceInfo, device); + if (bluetoothDevice != null) { + mBluetoothDevices.put(bluetoothDevice, device); + } synchronized (mClients) { for (Client c : mClients.values()) { @@ -478,8 +772,9 @@ public class MidiService extends IMidiManager.Stub { private void removeDeviceLocked(Device device) { IMidiDeviceServer server = device.getDeviceServer(); if (server != null) { - mDevicesByServer.remove(server); + mDevicesByServer.remove(server.asBinder()); } + mDevicesByInfo.remove(device.getDeviceInfo()); synchronized (mClients) { for (Client c : mClients.values()) { @@ -516,6 +811,15 @@ public class MidiService extends IMidiManager.Stub { MidiDeviceService.SERVICE_INTERFACE); if (parser == null) return; + // ignore virtual device servers that do not require the correct permission + if (!android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE.equals( + serviceInfo.permission)) { + Log.w(TAG, "Skipping MIDI device service " + serviceInfo.packageName + + ": it does not require the permission " + + android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE); + return; + } + Bundle properties = null; int numInputPorts = 0; int numOutputPorts = 0; -- cgit v1.1