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

Commit abdb0021 authored by Robert Wu's avatar Robert Wu Committed by Android (Google) Code Review
Browse files

Merge "MIDI: Add MidiUmpDeviceService" into main

parents c0820205 655cbf00
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -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 {
+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;
    }
}