Merge "MIDI: Add MidiUmpDeviceService" into main

This commit is contained in:
Robert Wu
2023-07-21 16:53:09 +00:00
committed by Android (Google) Code Review
4 changed files with 405 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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.
*
* <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;
}
}
}

View File

@@ -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<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) {
addPackageDeviceServer(serviceInfo, user.getUserIdentifier());
{
Intent intent = new Intent(MidiDeviceService.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());
}
}
}
}
{
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;
}
}