Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 655cbf00 authored by Robert Wu's avatar Robert Wu
Browse files

MIDI: Add MidiUmpDeviceService

This CL allows apps to expose a virtual UMP MIDI device.

MidiDeviceService existed before and MidiUmpDeviceService acts
similarly. The main change between the services is that apps now
expose ports instead of inputs and outputs. This is due to UMP being
bidirectional.

Bug: 291115176
Test: MidiEchoTest
Change-Id: If2370420dd758287a90abfac7b12f6b3087c82e3
parent 1d652155
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -26389,6 +26389,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 {
+65 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+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>
 * &lt;service android:name=".VirtualDeviceService"
 *         android:label="&#64;string/service_name">
 *     &lt;intent-filter>
 *         &lt;action android:name="android.media.midi.MidiUmpDeviceService" />
 *     &lt;/intent-filter>
 *         &lt;property android:name="android.media.midi.MidiUmpDeviceService"
 *             android:resource="@xml/device_info" />
 * &lt;/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;
        }
    }
}
+168 −19
Original line number Diff line number Diff line
@@ -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,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) {
@@ -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());
                    }
                }
            }
        }
@@ -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<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();
@@ -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;
    }
}