diff --git a/core/api/current.txt b/core/api/current.txt index eaefe84beb1f1..0d736953e3b6a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26391,6 +26391,17 @@ package android.media.midi { method public abstract void onDisconnect(android.media.midi.MidiReceiver); } + public abstract class MidiUmpDeviceService extends android.app.Service { + ctor public MidiUmpDeviceService(); + method @Nullable public final android.media.midi.MidiDeviceInfo getDeviceInfo(); + method @NonNull public final java.util.List getOutputPortReceivers(); + method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); + method public void onClose(); + method public void onDeviceStatusChanged(@Nullable android.media.midi.MidiDeviceStatus); + method @NonNull public abstract java.util.List onGetInputPortReceivers(); + field public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; + } + } package android.media.projection { diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java index fc33cef9e7339..8f1b9fe1d76d7 100644 --- a/media/java/android/media/midi/MidiDeviceServer.java +++ b/media/java/android/media/midi/MidiDeviceServer.java @@ -25,6 +25,7 @@ import android.system.Os; import android.system.OsConstants; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.midi.MidiDispatcher; import dalvik.system.CloseGuard; @@ -34,6 +35,7 @@ import libcore.io.IoUtils; import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; @@ -83,6 +85,14 @@ public final class MidiDeviceServer implements Closeable { private AtomicInteger mTotalInputBytes = new AtomicInteger(); private AtomicInteger mTotalOutputBytes = new AtomicInteger(); + private static final int UNUSED_UID = -1; + + private final Object mUmpUidLock = new Object(); + @GuardedBy("mUmpUidLock") + private final int[] mUmpInputPortUids; + @GuardedBy("mUmpUidLock") + private final int[] mUmpOutputPortUids; + public interface Callback { /** * Called to notify when an our device status has changed @@ -137,6 +147,9 @@ public final class MidiDeviceServer implements Closeable { int portNumber = mOutputPort.getPortNumber(); mInputPortOutputPorts[portNumber] = null; mInputPortOpen[portNumber] = false; + synchronized (mUmpUidLock) { + mUmpInputPortUids[portNumber] = UNUSED_UID; + } mTotalOutputBytes.addAndGet(mOutputPort.pullTotalBytesCount()); updateTotalBytes(); updateDeviceStatus(); @@ -162,6 +175,9 @@ public final class MidiDeviceServer implements Closeable { dispatcher.getSender().disconnect(mInputPort); int openCount = dispatcher.getReceiverCount(); mOutputPortOpenCount[portNumber] = openCount; + synchronized (mUmpUidLock) { + mUmpOutputPortUids[portNumber] = UNUSED_UID; + } mTotalInputBytes.addAndGet(mInputPort.pullTotalBytesCount()); updateTotalBytes(); updateDeviceStatus(); @@ -210,6 +226,25 @@ public final class MidiDeviceServer implements Closeable { return null; } + if (isUmpDevice()) { + if (portNumber >= mOutputPortCount) { + Log.e(TAG, "out portNumber out of range in openInputPort: " + portNumber); + return null; + } + synchronized (mUmpUidLock) { + if (mUmpInputPortUids[portNumber] != UNUSED_UID) { + Log.e(TAG, "input port already open in openInputPort: " + portNumber); + return null; + } + if ((mUmpOutputPortUids[portNumber] != UNUSED_UID) + && (Binder.getCallingUid() != mUmpOutputPortUids[portNumber])) { + Log.e(TAG, "different uid for output in openInputPort: " + portNumber); + return null; + } + mUmpInputPortUids[portNumber] = Binder.getCallingUid(); + } + } + try { FileDescriptor[] pair = createSeqPacketSocketPair(); MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber); @@ -242,6 +277,25 @@ public final class MidiDeviceServer implements Closeable { return null; } + if (isUmpDevice()) { + if (portNumber >= mInputPortCount) { + Log.e(TAG, "in portNumber out of range in openOutputPort: " + portNumber); + return null; + } + synchronized (mUmpUidLock) { + if (mUmpOutputPortUids[portNumber] != UNUSED_UID) { + Log.e(TAG, "output port already open in openOutputPort: " + portNumber); + return null; + } + if ((mUmpInputPortUids[portNumber] != UNUSED_UID) + && (Binder.getCallingUid() != mUmpInputPortUids[portNumber])) { + Log.e(TAG, "different uid for input in openOutputPort: " + portNumber); + return null; + } + mUmpOutputPortUids[portNumber] = Binder.getCallingUid(); + } + } + try { FileDescriptor[] pair = createSeqPacketSocketPair(); MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber); @@ -358,6 +412,13 @@ public final class MidiDeviceServer implements Closeable { mInputPortOpen = new boolean[mInputPortCount]; mOutputPortOpenCount = new int[numOutputPorts]; + synchronized (mUmpUidLock) { + mUmpInputPortUids = new int[mInputPortCount]; + mUmpOutputPortUids = new int[mOutputPortCount]; + Arrays.fill(mUmpInputPortUids, UNUSED_UID); + Arrays.fill(mUmpOutputPortUids, UNUSED_UID); + } + mGuard.open("close"); } @@ -467,4 +528,8 @@ public final class MidiDeviceServer implements Closeable { Log.e(TAG, "RemoteException in updateTotalBytes"); } } + + private boolean isUmpDevice() { + return mDeviceInfo.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN; + } } diff --git a/media/java/android/media/midi/MidiUmpDeviceService.java b/media/java/android/media/midi/MidiUmpDeviceService.java new file mode 100644 index 0000000000000..d189e3a88f032 --- /dev/null +++ b/media/java/android/media/midi/MidiUmpDeviceService.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2023 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 android.media.midi; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A service that implements a virtual MIDI device for Universal MIDI Packets (UMP). + * Subclasses must implement the {@link #onGetInputPortReceivers} method to provide a + * list of {@link MidiReceiver}s to receive data sent to the device's input ports. + * Similarly, subclasses can call {@link #getOutputPortReceivers} to fetch a list + * of {@link MidiReceiver}s for sending data out the output ports. + * + * Unlike traditional MIDI byte streams, only complete UMPs should be sent. + * Unlike with {@link #MidiDeviceService}, the number of input and output ports must be equal. + * + *

To extend this class, you must declare the service in your manifest file with + * an intent filter with the {@link #SERVICE_INTERFACE} action + * and meta-data to describe the virtual device. + * For example:

+ *
+ * <service android:name=".VirtualDeviceService"
+ *         android:label="@string/service_name">
+ *     <intent-filter>
+ *         <action android:name="android.media.midi.MidiUmpDeviceService" />
+ *     </intent-filter>
+ *         <property android:name="android.media.midi.MidiUmpDeviceService"
+ *             android:resource="@xml/device_info" />
+ * </service>
+ */ +public abstract class MidiUmpDeviceService extends Service { + private static final String TAG = "MidiUmpDeviceService"; + + public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; + + private IMidiManager mMidiManager; + private MidiDeviceServer mServer; + private MidiDeviceInfo mDeviceInfo; + + private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() { + @Override + public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { + MidiUmpDeviceService.this.onDeviceStatusChanged(status); + } + + @Override + public void onClose() { + MidiUmpDeviceService.this.onClose(); + } + }; + + @Override + public void onCreate() { + mMidiManager = IMidiManager.Stub.asInterface( + ServiceManager.getService(Context.MIDI_SERVICE)); + MidiDeviceServer server; + try { + MidiDeviceInfo deviceInfo = mMidiManager.getServiceDeviceInfo(getPackageName(), + this.getClass().getName()); + if (deviceInfo == null) { + Log.e(TAG, "Could not find MidiDeviceInfo for MidiUmpDeviceService " + this); + return; + } + mDeviceInfo = deviceInfo; + + List inputPortReceivers = onGetInputPortReceivers(); + if (inputPortReceivers == null) { + Log.e(TAG, "Could not get input port receivers for MidiUmpDeviceService " + this); + return; + } + MidiReceiver[] inputPortReceiversArr = new MidiReceiver[inputPortReceivers.size()]; + inputPortReceivers.toArray(inputPortReceiversArr); + server = new MidiDeviceServer(mMidiManager, inputPortReceiversArr, deviceInfo, + mCallback); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo"); + server = null; + } + mServer = server; + } + + /** + * Returns a list of {@link MidiReceiver} for the device's input ports. + * Subclasses must override this to provide the receivers which will receive + * data sent to the device's input ports. + * The number of input and output ports must be equal and non-zero. + * @return list of MidiReceivers + */ + public abstract @NonNull List onGetInputPortReceivers(); + + /** + * Returns a list of {@link MidiReceiver} for the device's output ports. + * These can be used to send data out the device's output ports. + * The number of input and output ports must be equal and non-zero. + * @return the list of MidiReceivers + */ + public final @NonNull List getOutputPortReceivers() { + if (mServer == null) { + return new ArrayList(); + } else { + return Arrays.asList(mServer.getOutputPortReceivers()); + } + } + + /** + * Returns the {@link MidiDeviceInfo} instance for this service + * @return the MidiDeviceInfo of the virtual MIDI device + */ + public final @Nullable MidiDeviceInfo getDeviceInfo() { + return mDeviceInfo; + } + + /** + * Called to notify when the {@link MidiDeviceStatus} has changed + * @param status the current status of the MIDI device + */ + public void onDeviceStatusChanged(@Nullable MidiDeviceStatus status) { + } + + /** + * Called to notify when the virtual MIDI device running in this service has been closed by + * all its clients + */ + public void onClose() { + } + + @Override + public @Nullable IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) { + return mServer.getBinderInterface().asBinder(); + } else { + return null; + } + } +} diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index 5e7dd596ea38e..dd78f21a54e8b 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -31,6 +31,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.media.MediaMetrics; import android.media.midi.IBluetoothMidiService; @@ -43,6 +44,7 @@ import android.media.midi.MidiDeviceInfo; import android.media.midi.MidiDeviceService; import android.media.midi.MidiDeviceStatus; import android.media.midi.MidiManager; +import android.media.midi.MidiUmpDeviceService; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -580,10 +582,14 @@ public class MidiService extends IMidiManager.Stub { intent.setComponent(new ComponentName( MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE, MidiManager.BLUETOOTH_MIDI_SERVICE_CLASS)); - } else { + } else if (!isUmpDevice(mDeviceInfo)) { intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); intent.setComponent( new ComponentName(mServiceInfo.packageName, mServiceInfo.name)); + } else { + intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE); + intent.setComponent( + new ComponentName(mServiceInfo.packageName, mServiceInfo.name)); } if (!mContext.bindServiceAsUser(intent, mServiceConnection, @@ -696,8 +702,8 @@ public class MidiService extends IMidiManager.Stub { isDeviceDisconnected ? "true" : "false") .set(MediaMetrics.Property.IS_SHARED, !mDeviceInfo.isPrivate() ? "true" : "false") - .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP, mDeviceInfo.getDefaultProtocol() - != MidiDeviceInfo.PROTOCOL_UNKNOWN ? "true" : "false") + .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP, + isUmpDevice(mDeviceInfo) ? "true" : "false") .set(MediaMetrics.Property.USING_ALSA, mDeviceInfo.getProperties().get( MidiDeviceInfo.PROPERTY_ALSA_CARD) != null ? "true" : "false") .set(MediaMetrics.Property.EVENT, "deviceClosed") @@ -971,19 +977,37 @@ public class MidiService extends IMidiManager.Stub { private void onStartOrUnlockUser(TargetUser user, boolean matchDirectBootUnaware) { Log.d(TAG, "onStartOrUnlockUser " + user.getUserIdentifier() + " matchDirectBootUnaware: " + matchDirectBootUnaware); - Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); int resolveFlags = PackageManager.GET_META_DATA; if (matchDirectBootUnaware) { resolveFlags |= PackageManager.MATCH_DIRECT_BOOT_UNAWARE; } - List resolveInfos = mPackageManager.queryIntentServicesAsUser(intent, - resolveFlags, user.getUserIdentifier()); - if (resolveInfos != null) { - int count = resolveInfos.size(); - for (int i = 0; i < count; i++) { - ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; - if (serviceInfo != null) { - addPackageDeviceServer(serviceInfo, user.getUserIdentifier()); + + { + Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); + List resolveInfos = mPackageManager.queryIntentServicesAsUser(intent, + resolveFlags, user.getUserIdentifier()); + if (resolveInfos != null) { + int count = resolveInfos.size(); + for (int i = 0; i < count; i++) { + ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; + if (serviceInfo != null) { + addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier()); + } + } + } + } + + { + Intent intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE); + List resolveInfos = mPackageManager.queryIntentServicesAsUser(intent, + resolveFlags, user.getUserIdentifier()); + if (resolveInfos != null) { + int count = resolveInfos.size(); + for (int i = 0; i < count; i++) { + ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; + if (serviceInfo != null) { + addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier()); + } } } } @@ -1057,13 +1081,11 @@ public class MidiService extends IMidiManager.Stub { if (device.isUidAllowed(uid) && device.isUserIdAllowed(userId)) { // UMP devices have protocols that are not PROTOCOL_UNKNOWN if (transport == MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS) { - if (device.getDeviceInfo().getDefaultProtocol() - != MidiDeviceInfo.PROTOCOL_UNKNOWN) { + if (isUmpDevice(device.getDeviceInfo())) { deviceInfos.add(device.getDeviceInfo()); } } else if (transport == MidiManager.TRANSPORT_MIDI_BYTE_STREAM) { - if (device.getDeviceInfo().getDefaultProtocol() - == MidiDeviceInfo.PROTOCOL_UNKNOWN) { + if (!isUmpDevice(device.getDeviceInfo())) { deviceInfos.add(device.getDeviceInfo()); } } @@ -1364,14 +1386,15 @@ public class MidiService extends IMidiManager.Stub { ServiceInfo[] services = info.services; if (services == null) return; for (int i = 0; i < services.length; i++) { - addPackageDeviceServer(services[i], userId); + addLegacyPackageDeviceServer(services[i], userId); + addUmpPackageDeviceServer(services[i], userId); } } private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private void addPackageDeviceServer(ServiceInfo serviceInfo, int userId) { - Log.d(TAG, "addPackageDeviceServer()" + userId); + private void addLegacyPackageDeviceServer(ServiceInfo serviceInfo, int userId) { + Log.d(TAG, "addLegacyPackageDeviceServer()" + userId); XmlResourceParser parser = null; try { @@ -1507,6 +1530,128 @@ public class MidiService extends IMidiManager.Stub { } } + @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS) + private void addUmpPackageDeviceServer(ServiceInfo serviceInfo, int userId) { + Log.d(TAG, "addUmpPackageDeviceServer()" + userId); + XmlResourceParser parser = null; + + try { + ComponentName componentName = new ComponentName(serviceInfo.packageName, + serviceInfo.name); + int resId = mPackageManager.getProperty(MidiUmpDeviceService.SERVICE_INTERFACE, + componentName).getResourceId(); + Resources resources = mPackageManager.getResourcesForApplication( + serviceInfo.packageName); + parser = resources.getXml(resId); + 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 numPorts = 0; + boolean isPrivate = false; + ArrayList portNames = new ArrayList(); + + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.END_DOCUMENT) { + break; + } else if (eventType == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + if ("device".equals(tagName)) { + if (properties != null) { + Log.w(TAG, "nested elements in metadata for " + + serviceInfo.packageName); + continue; + } + properties = new Bundle(); + properties.putParcelable(MidiDeviceInfo.PROPERTY_SERVICE_INFO, serviceInfo); + numPorts = 0; + isPrivate = false; + + int count = parser.getAttributeCount(); + for (int i = 0; i < count; i++) { + String name = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if ("private".equals(name)) { + isPrivate = "true".equals(value); + } else { + properties.putString(name, value); + } + } + } else if ("port".equals(tagName)) { + if (properties == null) { + Log.w(TAG, " outside of in metadata for " + + serviceInfo.packageName); + continue; + } + numPorts++; + + String portName = null; + int count = parser.getAttributeCount(); + for (int i = 0; i < count; i++) { + String name = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if ("name".equals(name)) { + portName = value; + break; + } + } + portNames.add(portName); + } + } else if (eventType == XmlPullParser.END_TAG) { + String tagName = parser.getName(); + if ("device".equals(tagName)) { + if (properties != null) { + if (numPorts == 0) { + Log.w(TAG, " with no ports in metadata for " + + serviceInfo.packageName); + continue; + } + + int uid; + try { + ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser( + serviceInfo.packageName, 0, userId); + uid = appInfo.uid; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "could not fetch ApplicationInfo for " + + serviceInfo.packageName); + continue; + } + + synchronized (mDevicesByInfo) { + addDeviceLocked(MidiDeviceInfo.TYPE_VIRTUAL, + numPorts, numPorts, + portNames.toArray(EMPTY_STRING_ARRAY), + portNames.toArray(EMPTY_STRING_ARRAY), + properties, null, serviceInfo, isPrivate, uid, + MidiDeviceInfo.PROTOCOL_UMP_MIDI_2_0, userId); + } + // setting properties to null signals that we are no longer + // processing a + properties = null; + portNames.clear(); + } + } + } + } + } catch (PackageManager.NameNotFoundException e) { + // No such property + } catch (Exception e) { + Log.w(TAG, "Unable to load component info " + serviceInfo.toString(), e); + } finally { + if (parser != null) parser.close(); + } + } + private void removePackageDeviceServers(String packageName, int userId) { synchronized (mDevicesByInfo) { Iterator iterator = mDevicesByInfo.values().iterator(); @@ -1649,4 +1794,8 @@ public class MidiService extends IMidiManager.Stub { private int getCallingUserId() { return UserHandle.getUserId(Binder.getCallingUid()); } + + private boolean isUmpDevice(MidiDeviceInfo deviceInfo) { + return deviceInfo.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN; + } }