Loading core/api/current.txt +11 −0 Original line number Diff line number Diff line Loading @@ -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<android.media.midi.MidiReceiver> 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<android.media.midi.MidiReceiver> onGetInputPortReceivers(); field public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; } } package android.media.projection { media/java/android/media/midi/MidiDeviceServer.java +65 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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(); Loading @@ -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(); Loading Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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"); } Loading Loading @@ -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; } } media/java/android/media/midi/MidiUmpDeviceService.java 0 → 100644 +161 −0 Original line number Diff line number Diff line /* * 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. * * <p>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:</p> * <pre> * <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></pre> */ 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<MidiReceiver> 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<MidiReceiver> 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<MidiReceiver> getOutputPortReceivers() { if (mServer == null) { return new ArrayList<MidiReceiver>(); } 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; } } } services/midi/java/com/android/server/midi/MidiService.java +168 −19 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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") Loading Loading @@ -971,11 +977,13 @@ 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; } { Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent, resolveFlags, user.getUserIdentifier()); if (resolveInfos != null) { Loading @@ -983,7 +991,23 @@ public class MidiService extends IMidiManager.Stub { for (int i = 0; i < count; i++) { ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; if (serviceInfo != null) { addPackageDeviceServer(serviceInfo, user.getUserIdentifier()); addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier()); } } } } { Intent intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE); List<ResolveInfo> 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()); } } } } Loading Loading @@ -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()); } } Loading Loading @@ -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 { Loading Loading @@ -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<String> portNames = new ArrayList<String>(); 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 <device> 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, "<port> outside of <device> 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, "<device> 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 <device> 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<Device> iterator = mDevicesByInfo.values().iterator(); Loading Loading @@ -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; } } Loading
core/api/current.txt +11 −0 Original line number Diff line number Diff line Loading @@ -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<android.media.midi.MidiReceiver> 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<android.media.midi.MidiReceiver> onGetInputPortReceivers(); field public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; } } package android.media.projection {
media/java/android/media/midi/MidiDeviceServer.java +65 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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(); Loading @@ -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(); Loading Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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"); } Loading Loading @@ -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; } }
media/java/android/media/midi/MidiUmpDeviceService.java 0 → 100644 +161 −0 Original line number Diff line number Diff line /* * 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. * * <p>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:</p> * <pre> * <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></pre> */ 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<MidiReceiver> 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<MidiReceiver> 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<MidiReceiver> getOutputPortReceivers() { if (mServer == null) { return new ArrayList<MidiReceiver>(); } 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; } } }
services/midi/java/com/android/server/midi/MidiService.java +168 −19 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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") Loading Loading @@ -971,11 +977,13 @@ 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; } { Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent, resolveFlags, user.getUserIdentifier()); if (resolveInfos != null) { Loading @@ -983,7 +991,23 @@ public class MidiService extends IMidiManager.Stub { for (int i = 0; i < count; i++) { ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; if (serviceInfo != null) { addPackageDeviceServer(serviceInfo, user.getUserIdentifier()); addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier()); } } } } { Intent intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE); List<ResolveInfo> 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()); } } } } Loading Loading @@ -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()); } } Loading Loading @@ -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 { Loading Loading @@ -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<String> portNames = new ArrayList<String>(); 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 <device> 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, "<port> outside of <device> 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, "<device> 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 <device> 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<Device> iterator = mDevicesByInfo.values().iterator(); Loading Loading @@ -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; } }