summaryrefslogtreecommitdiffstats
path: root/services/midi/java
diff options
context:
space:
mode:
authorMike Lockwood <lockwood@google.com>2015-03-27 13:23:41 -0700
committerMike Lockwood <lockwood@google.com>2015-03-27 13:23:41 -0700
commit5781cd5b218dc3e5a19d8f69e6fb9f310bed4df6 (patch)
tree7c74f6c49adbe1da5d4b580bbdd69561116f76b9 /services/midi/java
parent1e650e26a7bb2f02635530a46ecd715f9911d40e (diff)
downloadframeworks_base-5781cd5b218dc3e5a19d8f69e6fb9f310bed4df6.zip
frameworks_base-5781cd5b218dc3e5a19d8f69e6fb9f310bed4df6.tar.gz
frameworks_base-5781cd5b218dc3e5a19d8f69e6fb9f310bed4df6.tar.bz2
Make the MIDI Manager optional, enabled by "android.software.midi" feature
Change-Id: I76d442ea28beea4b9e2876bfef501d8f61403702
Diffstat (limited to 'services/midi/java')
-rw-r--r--services/midi/java/com/android/server/midi/MidiService.java674
1 files changed, 674 insertions, 0 deletions
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
new file mode 100644
index 0000000..1d2180e
--- /dev/null
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -0,0 +1,674 @@
+/*
+ * Copyright (C) 2014 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 an
+ * limitations under the License.
+ */
+
+package com.android.server.midi;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.XmlResourceParser;
+import android.media.midi.IMidiDeviceListener;
+import android.media.midi.IMidiDeviceServer;
+import android.media.midi.IMidiManager;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceService;
+import android.media.midi.MidiDeviceStatus;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.XmlUtils;
+import com.android.server.SystemService;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+public class MidiService extends IMidiManager.Stub {
+
+ public static class Lifecycle extends SystemService {
+ private MidiService mMidiService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ mMidiService = new MidiService(getContext());
+ publishBinderService(Context.MIDI_SERVICE, mMidiService);
+ }
+ }
+
+ private static final String TAG = "MidiService";
+
+ private final Context mContext;
+
+ // list of all our clients, keyed by Binder token
+ private final HashMap<IBinder, Client> mClients = new HashMap<IBinder, Client>();
+
+ // list of all devices, keyed by MidiDeviceInfo
+ private final HashMap<MidiDeviceInfo, Device> mDevicesByInfo
+ = new HashMap<MidiDeviceInfo, Device>();
+
+ // list of all devices, keyed by IMidiDeviceServer
+ private final HashMap<IBinder, Device> mDevicesByServer = new HashMap<IBinder, Device>();
+
+ // used for assigning IDs to MIDI devices
+ private int mNextDeviceId = 1;
+
+ private final PackageManager mPackageManager;
+
+ // PackageMonitor for listening to package changes
+ private final PackageMonitor mPackageMonitor = new PackageMonitor() {
+ @Override
+ public void onPackageAdded(String packageName, int uid) {
+ addPackageDeviceServers(packageName);
+ }
+
+ @Override
+ public void onPackageModified(String packageName) {
+ removePackageDeviceServers(packageName);
+ addPackageDeviceServers(packageName);
+ }
+
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ removePackageDeviceServers(packageName);
+ }
+ };
+
+ private final class Client implements IBinder.DeathRecipient {
+ // Binder token for this client
+ private final IBinder mToken;
+ // This client's UID
+ private final int mUid;
+ // This client's PID
+ private final int mPid;
+ // List of all receivers for this client
+ private final ArrayList<IMidiDeviceListener> mListeners
+ = new ArrayList<IMidiDeviceListener>();
+
+ public Client(IBinder token) {
+ mToken = token;
+ mUid = Binder.getCallingUid();
+ mPid = Binder.getCallingPid();
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public void addListener(IMidiDeviceListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeListener(IMidiDeviceListener listener) {
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ removeClient(mToken);
+ }
+ }
+
+ public void deviceAdded(Device device) {
+ // ignore private devices that our client cannot access
+ if (!device.isUidAllowed(mUid)) return;
+
+ MidiDeviceInfo deviceInfo = device.getDeviceInfo();
+ try {
+ for (IMidiDeviceListener listener : mListeners) {
+ listener.onDeviceAdded(deviceInfo);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception", e);
+ }
+ }
+
+ public void deviceRemoved(Device device) {
+ // ignore private devices that our client cannot access
+ if (!device.isUidAllowed(mUid)) return;
+
+ MidiDeviceInfo deviceInfo = device.getDeviceInfo();
+ try {
+ for (IMidiDeviceListener listener : mListeners) {
+ listener.onDeviceRemoved(deviceInfo);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception", e);
+ }
+ }
+
+ public void deviceStatusChanged(Device device, MidiDeviceStatus status) {
+ // ignore private devices that our client cannot access
+ if (!device.isUidAllowed(mUid)) return;
+
+ try {
+ for (IMidiDeviceListener listener : mListeners) {
+ listener.onDeviceStatusChanged(status);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception", e);
+ }
+ }
+
+ public void binderDied() {
+ removeClient(mToken);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Client: UID: ");
+ sb.append(mUid);
+ sb.append(" PID: ");
+ sb.append(mPid);
+ sb.append(" listener count: ");
+ sb.append(mListeners.size());
+ return sb.toString();
+ }
+ }
+
+ private Client getClient(IBinder token) {
+ synchronized (mClients) {
+ Client client = mClients.get(token);
+ if (client == null) {
+ client = new Client(token);
+
+ try {
+ token.linkToDeath(client, 0);
+ } catch (RemoteException e) {
+ return null;
+ }
+ mClients.put(token, client);
+ }
+ return client;
+ }
+ }
+
+ private void removeClient(IBinder token) {
+ mClients.remove(token);
+ }
+
+ private final class Device implements IBinder.DeathRecipient {
+ private final IMidiDeviceServer mServer;
+ private final MidiDeviceInfo mDeviceInfo;
+ private MidiDeviceStatus mDeviceStatus;
+ private IBinder mDeviceStatusToken;
+ // ServiceInfo for the device's MidiDeviceServer implementation (virtual devices only)
+ private final ServiceInfo mServiceInfo;
+ // UID of device implementation
+ private final int mUid;
+
+ public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo,
+ ServiceInfo serviceInfo, int uid) {
+ mServer = server;
+ mDeviceInfo = deviceInfo;
+ mServiceInfo = serviceInfo;
+ mUid = uid;
+ }
+
+ public MidiDeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ public MidiDeviceStatus getDeviceStatus() {
+ return mDeviceStatus;
+ }
+
+ public void setDeviceStatus(IBinder token, MidiDeviceStatus status) {
+ mDeviceStatus = status;
+
+ if (mDeviceStatusToken == null && token != null) {
+ // register a death recipient so we can clear the status when the device dies
+ try {
+ token.linkToDeath(new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ // reset to default status and clear the token
+ mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
+ mDeviceStatusToken = null;
+ notifyDeviceStatusChanged(Device.this, mDeviceStatus);
+ }
+ }, 0);
+ mDeviceStatusToken = token;
+ } catch (RemoteException e) {
+ // reset to default status
+ mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
+ }
+ }
+ }
+
+ public IMidiDeviceServer getDeviceServer() {
+ return mServer;
+ }
+
+ public ServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ public String getPackageName() {
+ return (mServiceInfo == null ? null : mServiceInfo.packageName);
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public boolean isUidAllowed(int uid) {
+ return (!mDeviceInfo.isPrivate() || mUid == uid);
+ }
+
+ public void binderDied() {
+ synchronized (mDevicesByInfo) {
+ if (mDevicesByInfo.remove(mDeviceInfo) != null) {
+ removeDeviceLocked(this);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Device: ");
+ sb.append(mDeviceInfo);
+ sb.append(" UID: ");
+ sb.append(mUid);
+ return sb.toString();
+ }
+ }
+
+ public MidiService(Context context) {
+ mContext = context;
+ mPackageManager = context.getPackageManager();
+ mPackageMonitor.register(context, null, true);
+
+ Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
+ List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServices(intent,
+ PackageManager.GET_META_DATA);
+ 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);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void registerListener(IBinder token, IMidiDeviceListener listener) {
+ Client client = getClient(token);
+ if (client == null) return;
+ client.addListener(listener);
+ }
+
+ @Override
+ public void unregisterListener(IBinder token, IMidiDeviceListener listener) {
+ Client client = getClient(token);
+ if (client == null) return;
+ client.removeListener(listener);
+ }
+
+ private static final MidiDeviceInfo[] EMPTY_DEVICE_INFO_ARRAY = new MidiDeviceInfo[0];
+
+ public MidiDeviceInfo[] getDeviceList() {
+ ArrayList<MidiDeviceInfo> deviceInfos = new ArrayList<MidiDeviceInfo>();
+ int uid = Binder.getCallingUid();
+
+ synchronized (mDevicesByInfo) {
+ for (Device device : mDevicesByInfo.values()) {
+ if (device.isUidAllowed(uid)) {
+ deviceInfos.add(device.getDeviceInfo());
+ }
+ }
+ }
+
+ return deviceInfos.toArray(EMPTY_DEVICE_INFO_ARRAY);
+ }
+
+ @Override
+ public IMidiDeviceServer openDevice(IBinder token, MidiDeviceInfo deviceInfo) {
+ Device device = mDevicesByInfo.get(deviceInfo);
+ if (device == null) {
+ Log.e(TAG, "device not found in openDevice: " + deviceInfo);
+ return null;
+ }
+
+ if (!device.isUidAllowed(Binder.getCallingUid())) {
+ throw new SecurityException("Attempt to open private device with wrong UID");
+ }
+
+ return device.getDeviceServer();
+ }
+
+ @Override
+ public MidiDeviceInfo registerDeviceServer(IMidiDeviceServer server, int numInputPorts,
+ int numOutputPorts, String[] inputPortNames, String[] outputPortNames,
+ Bundle properties, int type) {
+ int uid = Binder.getCallingUid();
+ if (type != MidiDeviceInfo.TYPE_VIRTUAL && uid != Process.SYSTEM_UID) {
+ throw new SecurityException("only system can create non-virtual devices");
+ }
+
+ synchronized (mDevicesByInfo) {
+ return addDeviceLocked(type, numInputPorts, numOutputPorts, inputPortNames,
+ outputPortNames, properties, server, null, false, uid);
+ }
+ }
+
+ @Override
+ public void unregisterDeviceServer(IMidiDeviceServer server) {
+ synchronized (mDevicesByInfo) {
+ Device device = mDevicesByServer.get(server.asBinder());
+ if (device != null) {
+ mDevicesByInfo.remove(device.getDeviceInfo());
+ removeDeviceLocked(device);
+ }
+ }
+ }
+
+ @Override
+ public MidiDeviceInfo getServiceDeviceInfo(String packageName, String className) {
+ synchronized (mDevicesByInfo) {
+ for (Device device : mDevicesByInfo.values()) {
+ ServiceInfo serviceInfo = device.getServiceInfo();
+ if (serviceInfo != null &&
+ packageName.equals(serviceInfo.packageName) &&
+ className.equals(serviceInfo.name)) {
+ return device.getDeviceInfo();
+ }
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public MidiDeviceStatus getDeviceStatus(MidiDeviceInfo deviceInfo) {
+ Device device = mDevicesByInfo.get(deviceInfo);
+ if (device == null) {
+ throw new IllegalArgumentException("no such device for " + deviceInfo);
+ }
+ return device.getDeviceStatus();
+ }
+
+ @Override
+ public void setDeviceStatus(IBinder token, MidiDeviceStatus status) {
+ MidiDeviceInfo deviceInfo = status.getDeviceInfo();
+ Device device = mDevicesByInfo.get(deviceInfo);
+ if (device == null) {
+ // Just return quietly here if device no longer exists
+ return;
+ }
+ if (Binder.getCallingUid() != device.getUid()) {
+ throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid()
+ + " does not match device's UID " + device.getUid());
+ }
+ device.setDeviceStatus(token, status);
+ notifyDeviceStatusChanged(device, status);
+ }
+
+ private void notifyDeviceStatusChanged(Device device, MidiDeviceStatus status) {
+ synchronized (mClients) {
+ for (Client c : mClients.values()) {
+ c.deviceStatusChanged(device, status);
+ }
+ }
+ }
+
+ // synchronize on mDevicesByInfo
+ private MidiDeviceInfo addDeviceLocked(int type, int numInputPorts, int numOutputPorts,
+ String[] inputPortNames, String[] outputPortNames, Bundle properties,
+ IMidiDeviceServer server, ServiceInfo serviceInfo,
+ boolean isPrivate, int uid) {
+
+ int id = mNextDeviceId++;
+ MidiDeviceInfo deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts,
+ inputPortNames, outputPortNames, properties, isPrivate);
+ Device device = new Device(server, deviceInfo, serviceInfo, uid);
+
+ if (server != null) {
+ IBinder binder = server.asBinder();
+ try {
+ binder.linkToDeath(device, 0);
+ } catch (RemoteException e) {
+ return null;
+ }
+ mDevicesByServer.put(binder, device);
+ }
+ mDevicesByInfo.put(deviceInfo, device);
+
+ synchronized (mClients) {
+ for (Client c : mClients.values()) {
+ c.deviceAdded(device);
+ }
+ }
+
+ return deviceInfo;
+ }
+
+ // synchronize on mDevicesByInfo
+ private void removeDeviceLocked(Device device) {
+ IMidiDeviceServer server = device.getDeviceServer();
+ if (server != null) {
+ mDevicesByServer.remove(server);
+ }
+
+ synchronized (mClients) {
+ for (Client c : mClients.values()) {
+ c.deviceRemoved(device);
+ }
+ }
+ }
+
+ private void addPackageDeviceServers(String packageName) {
+ PackageInfo info;
+
+ try {
+ info = mPackageManager.getPackageInfo(packageName,
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "handlePackageUpdate could not find package " + packageName, e);
+ return;
+ }
+
+ ServiceInfo[] services = info.services;
+ if (services == null) return;
+ for (int i = 0; i < services.length; i++) {
+ addPackageDeviceServer(services[i]);
+ }
+ }
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private void addPackageDeviceServer(ServiceInfo serviceInfo) {
+ XmlResourceParser parser = null;
+
+ try {
+ parser = serviceInfo.loadXmlMetaData(mPackageManager,
+ MidiDeviceService.SERVICE_INTERFACE);
+ if (parser == null) return;
+
+ Bundle properties = null;
+ int numInputPorts = 0;
+ int numOutputPorts = 0;
+ boolean isPrivate = false;
+ ArrayList<String> inputPortNames = new ArrayList<String>();
+ ArrayList<String> outputPortNames = 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);
+ numInputPorts = 0;
+ numOutputPorts = 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 ("input-port".equals(tagName)) {
+ if (properties == null) {
+ Log.w(TAG, "<input-port> outside of <device> in metadata for "
+ + serviceInfo.packageName);
+ continue;
+ }
+ numInputPorts++;
+
+ 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;
+ }
+ }
+ inputPortNames.add(portName);
+ } else if ("output-port".equals(tagName)) {
+ if (properties == null) {
+ Log.w(TAG, "<output-port> outside of <device> in metadata for "
+ + serviceInfo.packageName);
+ continue;
+ }
+ numOutputPorts++;
+
+ 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;
+ }
+ }
+ outputPortNames.add(portName);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ String tagName = parser.getName();
+ if ("device".equals(tagName)) {
+ if (properties != null) {
+ if (numInputPorts == 0 && numOutputPorts == 0) {
+ Log.w(TAG, "<device> with no ports in metadata for "
+ + serviceInfo.packageName);
+ continue;
+ }
+
+ int uid;
+ try {
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
+ serviceInfo.packageName, 0);
+ uid = appInfo.uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "could not fetch ApplicationInfo for "
+ + serviceInfo.packageName);
+ continue;
+ }
+
+ synchronized (mDevicesByInfo) {
+ addDeviceLocked(MidiDeviceInfo.TYPE_VIRTUAL,
+ numInputPorts, numOutputPorts,
+ inputPortNames.toArray(EMPTY_STRING_ARRAY),
+ outputPortNames.toArray(EMPTY_STRING_ARRAY),
+ properties, null, serviceInfo, isPrivate, uid);
+ }
+ // setting properties to null signals that we are no longer
+ // processing a <device>
+ properties = null;
+ inputPortNames.clear();
+ outputPortNames.clear();
+ }
+ }
+ }
+ }
+ } 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) {
+ synchronized (mDevicesByInfo) {
+ Iterator<Device> iterator = mDevicesByInfo.values().iterator();
+ while (iterator.hasNext()) {
+ Device device = iterator.next();
+ if (packageName.equals(device.getPackageName())) {
+ iterator.remove();
+ removeDeviceLocked(device);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+
+ pw.println("MIDI Manager State:");
+ pw.increaseIndent();
+
+ pw.println("Devices:");
+ pw.increaseIndent();
+ synchronized (mDevicesByInfo) {
+ for (Device device : mDevicesByInfo.values()) {
+ pw.println(device.toString());
+ }
+ }
+ pw.decreaseIndent();
+
+ pw.println("Clients:");
+ pw.increaseIndent();
+ synchronized (mClients) {
+ for (Client client : mClients.values()) {
+ pw.println(client.toString());
+ }
+ }
+ pw.decreaseIndent();
+ }
+}