summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--media/java/android/media/midi/MidiDevice.java43
-rw-r--r--media/jni/Android.mk3
-rw-r--r--media/jni/midi/android_media_midi_MidiDevice.cpp49
-rw-r--r--media/native/midi/Android.bp21
-rw-r--r--media/native/midi/Android.mk22
-rw-r--r--media/native/midi/MidiDeviceRegistry.cpp106
-rw-r--r--media/native/midi/MidiDeviceRegistry.h104
-rw-r--r--media/native/midi/MidiPortRegistry.cpp200
-rw-r--r--media/native/midi/MidiPortRegistry.h192
-rw-r--r--media/native/midi/midi.cpp317
-rw-r--r--media/native/midi/midi.h202
-rw-r--r--media/tests/NativeMidiDemo/Android.mk30
-rw-r--r--media/tests/NativeMidiDemo/AndroidManifest.xml20
-rw-r--r--media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java354
-rw-r--r--media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java32
-rw-r--r--media/tests/NativeMidiDemo/jni/Android.mk35
-rw-r--r--media/tests/NativeMidiDemo/jni/messagequeue.cpp138
-rw-r--r--media/tests/NativeMidiDemo/jni/messagequeue.h30
-rw-r--r--media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp285
-rw-r--r--media/tests/NativeMidiDemo/res/layout/main.xml93
-rw-r--r--media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--media/tests/NativeMidiDemo/res/values/strings.xml6
25 files changed, 2282 insertions, 0 deletions
diff --git a/media/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java
index f79cd06a3b5a..4b43260ece01 100644
--- a/media/java/android/media/midi/MidiDevice.java
+++ b/media/java/android/media/midi/MidiDevice.java
@@ -35,6 +35,10 @@ import java.io.IOException;
* Instances of this class are created by {@link MidiManager#openDevice}.
*/
public final class MidiDevice implements Closeable {
+ static {
+ System.loadLibrary("media_jni");
+ }
+
private static final String TAG = "MidiDevice";
private final MidiDeviceInfo mDeviceInfo;
@@ -43,6 +47,7 @@ public final class MidiDevice implements Closeable {
private final IBinder mClientToken;
private final IBinder mDeviceToken;
private boolean mIsDeviceClosed;
+ private boolean mIsMirroredToNative;
private final CloseGuard mGuard = CloseGuard.get();
@@ -209,10 +214,45 @@ public final class MidiDevice implements Closeable {
}
}
+ /**
+ * Makes Midi Device available to the Native API
+ * @hide
+ */
+ public void mirrorToNative() throws IOException {
+ if (mIsDeviceClosed || mIsMirroredToNative) {
+ return;
+ }
+
+ int result = mirrorToNative(mDeviceServer.asBinder(), mDeviceInfo.getId());
+ if (result != 0) {
+ throw new IOException("Failed mirroring to native: " + result);
+ }
+
+ mIsMirroredToNative = true;
+ }
+
+ /**
+ * Makes Midi Device no longer available to the Native API
+ * @hide
+ */
+ public void removeFromNative() throws IOException {
+ if (!mIsMirroredToNative) {
+ return;
+ }
+
+ int result = removeFromNative(mDeviceInfo.getId());
+ if (result != 0) {
+ throw new IOException("Failed removing from native: " + result);
+ }
+
+ mIsMirroredToNative = false;
+ }
+
@Override
public void close() throws IOException {
synchronized (mGuard) {
if (!mIsDeviceClosed) {
+ removeFromNative();
mGuard.close();
mIsDeviceClosed = true;
try {
@@ -238,4 +278,7 @@ public final class MidiDevice implements Closeable {
public String toString() {
return ("MidiDevice: " + mDeviceInfo.toString());
}
+
+ private native int mirrorToNative(IBinder deviceServerBinder, int uid);
+ private native int removeFromNative(int uid);
}
diff --git a/media/jni/Android.mk b/media/jni/Android.mk
index 861ed0a3b08a..23bf3d6711f6 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -26,6 +26,7 @@ LOCAL_SRC_FILES:= \
android_mtp_MtpDatabase.cpp \
android_mtp_MtpDevice.cpp \
android_mtp_MtpServer.cpp \
+ midi/android_media_midi_MidiDevice.cpp \
LOCAL_SHARED_LIBRARIES := \
libandroid_runtime \
@@ -34,6 +35,7 @@ LOCAL_SHARED_LIBRARIES := \
libbinder \
libmedia \
libmediadrm \
+ libmidi \
libskia \
libui \
liblog \
@@ -55,6 +57,7 @@ LOCAL_C_INCLUDES += \
external/tremor/Tremor \
frameworks/base/core/jni \
frameworks/base/libs/hwui \
+ frameworks/base/media/native \
frameworks/av/media/libmedia \
frameworks/av/media/libstagefright \
frameworks/av/media/mtp \
diff --git a/media/jni/midi/android_media_midi_MidiDevice.cpp b/media/jni/midi/android_media_midi_MidiDevice.cpp
new file mode 100644
index 000000000000..1e54bacc469b
--- /dev/null
+++ b/media/jni/midi/android_media_midi_MidiDevice.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "Midi-JNI"
+
+#include <android_util_Binder.h>
+#include <midi/MidiDeviceRegistry.h>
+#include <nativehelper/jni.h>
+#include <utils/Log.h>
+
+using namespace android;
+using namespace android::media::midi;
+
+extern "C" jint Java_android_media_midi_MidiDevice_mirrorToNative(
+ JNIEnv *env, jobject thiz, jobject midiDeviceServer, jint id)
+{
+ (void)thiz;
+ sp<IBinder> serverBinder = ibinderForJavaObject(env, midiDeviceServer);
+ if (serverBinder.get() == NULL) {
+ ALOGE("Could not obtain IBinder from passed jobject");
+ return -EINVAL;
+ }
+ // return MidiDeviceManager::getInstance().addDevice(serverBinder, uid);
+ return MidiDeviceRegistry::getInstance().addDevice(
+ new BpMidiDeviceServer(serverBinder), id);
+}
+
+extern "C" jint Java_android_media_midi_MidiDevice_removeFromNative(
+ JNIEnv *env, jobject thiz, jint uid)
+{
+ (void)env;
+ (void)thiz;
+ // return MidiDeviceManager::getInstance().removeDevice(uid);
+ return MidiDeviceRegistry::getInstance().removeDevice(uid);
+}
diff --git a/media/native/midi/Android.bp b/media/native/midi/Android.bp
new file mode 100644
index 000000000000..3500805d2794
--- /dev/null
+++ b/media/native/midi/Android.bp
@@ -0,0 +1,21 @@
+// Copyright (C) 2017 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.
+
+// The headers module is in frameworks/media/native/midi/Android.bp.
+ndk_library {
+ name: "libmidi.ndk",
+ symbol_file: "libmidi.map.txt",
+ first_version: "26",
+// unversioned_until: "current",
+}
diff --git a/media/native/midi/Android.mk b/media/native/midi/Android.mk
new file mode 100644
index 000000000000..b91c430e2bca
--- /dev/null
+++ b/media/native/midi/Android.mk
@@ -0,0 +1,22 @@
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+ ../../java/android/media/midi/IMidiDeviceServer.aidl \
+ midi.cpp \
+ MidiDeviceRegistry.cpp \
+ MidiPortRegistry.cpp
+
+LOCAL_AIDL_INCLUDES := \
+ $(FRAMEWORKS_BASE_JAVA_SRC_DIRS) \
+ frameworks/native/aidl/binder
+
+LOCAL_CFLAGS += -Wall -Werror -O0
+
+LOCAL_MODULE := libmidi
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SHARED_LIBRARIES := liblog libbinder libutils libmedia
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/media/native/midi/MidiDeviceRegistry.cpp b/media/native/midi/MidiDeviceRegistry.cpp
new file mode 100644
index 000000000000..8854a08a0a45
--- /dev/null
+++ b/media/native/midi/MidiDeviceRegistry.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include "MidiDeviceRegistry.h"
+
+namespace android {
+
+ANDROID_SINGLETON_STATIC_INSTANCE(media::midi::MidiDeviceRegistry);
+
+namespace media {
+namespace midi {
+
+MidiDeviceRegistry::MidiDeviceRegistry() : mNextDeviceToken(1) {
+}
+
+status_t MidiDeviceRegistry::addDevice(sp<BpMidiDeviceServer> server, int32_t deviceId) {
+ if (server.get() == nullptr) {
+ return -EINVAL;
+ }
+
+ std::lock_guard<std::mutex> guard(mMapsLock);
+ mServers[deviceId] = server;
+ return OK;
+}
+
+status_t MidiDeviceRegistry::removeDevice(int32_t deviceId) {
+ std::lock_guard<std::mutex> guard(mMapsLock);
+ mServers.erase(deviceId);
+ const auto& iter = mUidToToken.find(deviceId);
+ if (iter != mUidToToken.end()) {
+ mTokenToUid.erase(iter->second);
+ mUidToToken.erase(iter);
+ }
+ return OK;
+}
+
+//NOTE: This creates an entry if not found, or returns an existing one.
+status_t MidiDeviceRegistry::obtainDeviceToken(int32_t deviceId, AMIDI_Device *deviceTokenPtr) {
+ std::lock_guard<std::mutex> guard(mMapsLock);
+ const auto& serversIter = mServers.find(deviceId);
+ if (serversIter == mServers.end()) {
+ // Not found.
+ return -EINVAL;
+ }
+
+ const auto& iter = mUidToToken.find(deviceId);
+ if (iter != mUidToToken.end()) {
+ *deviceTokenPtr = iter->second;
+ } else {
+ *deviceTokenPtr = mNextDeviceToken++;
+ mTokenToUid[*deviceTokenPtr] = deviceId;
+ mUidToToken[deviceId] = *deviceTokenPtr;
+ }
+ return OK;
+}
+
+status_t MidiDeviceRegistry::releaseDevice(AMIDI_Device deviceToken) {
+ std::lock_guard<std::mutex> guard(mMapsLock);
+ const auto& iter = mTokenToUid.find(deviceToken);
+ if (iter == mTokenToUid.end()) {
+ // Not found
+ return -EINVAL;
+ }
+
+ mServers.erase(iter->second);
+ mUidToToken.erase(iter->second);
+ mTokenToUid.erase(iter);
+ return OK;
+}
+
+status_t MidiDeviceRegistry::getDeviceByToken(
+ AMIDI_Device deviceToken, sp<BpMidiDeviceServer> *devicePtr) {
+ std::lock_guard<std::mutex> guard(mMapsLock);
+ int32_t id = -1;
+ {
+ const auto& iter = mTokenToUid.find(deviceToken);
+ if (iter == mTokenToUid.end()) {
+ return -EINVAL;
+ }
+ id = iter->second;
+ }
+ const auto& iter = mServers.find(id);
+ if (iter == mServers.end()) {
+ return -EINVAL;
+ }
+
+ *devicePtr = iter->second;
+ return OK;
+}
+
+} // namespace midi
+} // namespace media
+} // namespace android
diff --git a/media/native/midi/MidiDeviceRegistry.h b/media/native/midi/MidiDeviceRegistry.h
new file mode 100644
index 000000000000..93be7338385b
--- /dev/null
+++ b/media/native/midi/MidiDeviceRegistry.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#ifndef ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_
+#define ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_
+
+#include <map>
+#include <mutex>
+
+#include <binder/IBinder.h>
+#include <utils/Errors.h>
+#include <utils/Singleton.h>
+
+#include "android/media/midi/BpMidiDeviceServer.h"
+#include "midi.h"
+
+namespace android {
+namespace media {
+namespace midi {
+
+/*
+ * Maintains a thread-safe, (singleton) list of MIDI devices with associated Binder interfaces,
+ * which are exposed to the Native API via (Java) MidiDevice.mirrorToNative() &
+ * MidiDevice.removeFromNative().
+ * (Called via MidiDeviceManager::addDevice() MidiManager::removeDevice()).
+ */
+class MidiDeviceRegistry : public Singleton<MidiDeviceRegistry> {
+ public:
+ /* Add a MIDI Device to the registry.
+ *
+ * server The Binder interface to the MIDI device server.
+ * deviceUId The unique ID of the device obtained from
+ * the Java API via MidiDeviceInfo.getId().
+ */
+ status_t addDevice(sp<BpMidiDeviceServer> server, int32_t deviceId);
+
+ /* Remove the device (and associated server) from the Device registry.
+ *
+ * deviceUid The ID of the device which was used in the call to addDevice().
+ */
+ status_t removeDevice(int32_t deviceId);
+
+ /* Gets a device token associated with the device ID. This is used by the
+ * native API to identify/access the device.
+ * Multiple calls without releasing the token will return the same value.
+ *
+ * deviceUid The ID of the device.
+ * deviceTokenPtr Receives the device (native) token associated with the device ID.
+ * returns: OK on success, error code otherwise.
+ */
+ status_t obtainDeviceToken(int32_t deviceId, AMIDI_Device *deviceTokenPtr);
+
+ /*
+ * Releases the native API device token associated with a MIDI device.
+ *
+ * deviceToken The device (native) token associated with the device ID.
+ */
+ status_t releaseDevice(AMIDI_Device deviceToken);
+
+ /*
+ * Gets the Device server binder interface associated with the device token.
+ *
+ * deviceToken The device (native) token associated with the device ID.
+ */
+ status_t getDeviceByToken(AMIDI_Device deviceToken, sp<BpMidiDeviceServer> *devicePtr);
+
+ private:
+ friend class Singleton<MidiDeviceRegistry>;
+ MidiDeviceRegistry();
+
+ // Access Mutex
+ std::mutex mMapsLock;
+
+ // maps device IDs to servers
+ std::map<int32_t, sp<BpMidiDeviceServer>> mServers;
+
+ // maps device tokens to device ID
+ std::map<AMIDI_Device, int32_t> mTokenToUid;
+
+ // maps device IDs to device tokens
+ std::map<int32_t, AMIDI_Device> mUidToToken;
+
+ // Value of next device token to dole out.
+ AMIDI_Device mNextDeviceToken;
+};
+
+} // namespace midi
+} // namespace media
+} // namespace android
+
+#endif // ANDROID_MEDIA_MIDI_DEVICE_REGISTRY_H_
diff --git a/media/native/midi/MidiPortRegistry.cpp b/media/native/midi/MidiPortRegistry.cpp
new file mode 100644
index 000000000000..fa70af8d0947
--- /dev/null
+++ b/media/native/midi/MidiPortRegistry.cpp
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include "MidiPortRegistry.h"
+
+namespace android {
+
+ANDROID_SINGLETON_STATIC_INSTANCE(media::midi::MidiPortRegistry);
+
+namespace media {
+namespace midi {
+
+//TODO Note that these 2 are identical
+struct MidiPortRegistry::OutputPort {
+ AMIDI_Device device;
+ sp<IBinder> binderToken;
+ base::unique_fd ufd;
+};
+
+struct MidiPortRegistry::InputPort {
+ AMIDI_Device device;
+ sp<IBinder> binderToken;
+ base::unique_fd ufd;
+};
+
+MidiPortRegistry::MidiPortRegistry() : mNextOutputPortToken(0), mNextInputPortToken(0) {
+}
+
+status_t MidiPortRegistry::addOutputPort(
+ AMIDI_Device device,
+ sp<IBinder> portToken,
+ base::unique_fd &&ufd,
+ AMIDI_OutputPort *portPtr) {
+ *portPtr = mNextOutputPortToken++;
+
+ OutputPortEntry* portEntry = new OutputPortEntry;
+ portEntry->port = new OutputPort;
+ portEntry->state = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE;
+ portEntry->port = new OutputPort;
+ portEntry->port->device = device;
+ portEntry->port->binderToken = portToken;
+ portEntry->port->ufd = std::move(ufd);
+
+ mOutputPortMap[*portPtr] = portEntry;
+
+ return OK;
+}
+
+status_t MidiPortRegistry::removeOutputPort(
+ AMIDI_OutputPort port,
+ AMIDI_Device *devicePtr,
+ sp<IBinder> *portTokenPtr) {
+ OutputPortMap::iterator itr = mOutputPortMap.find(port);
+ if (itr == mOutputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ OutputPortEntry *entry = mOutputPortMap[port];
+ int portState = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE;
+ while (!entry->state.compare_exchange_weak(portState, MIDI_OUTPUT_PORT_STATE_CLOSED)) {
+ if (portState == MIDI_OUTPUT_PORT_STATE_CLOSED) {
+ return -EINVAL; // Already closed
+ }
+ }
+ *devicePtr = entry->port->device;
+ *portTokenPtr = entry->port->binderToken;
+ delete entry->port;
+ entry->port = nullptr;
+
+ mOutputPortMap.erase(itr);
+
+ return OK;
+}
+
+status_t MidiPortRegistry::getOutputPortFdAndLock(
+ AMIDI_OutputPort port, base::unique_fd **ufdPtr) {
+ if (mOutputPortMap.find(port) == mOutputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ OutputPortEntry *entry = mOutputPortMap[port];
+ int portState = MIDI_OUTPUT_PORT_STATE_OPEN_IDLE;
+ if (!entry->state.compare_exchange_strong(portState, MIDI_OUTPUT_PORT_STATE_OPEN_ACTIVE)) {
+ // The port has been closed.
+ return -EPIPE;
+ }
+ *ufdPtr = &entry->port->ufd;
+
+ return OK;
+}
+
+status_t MidiPortRegistry::unlockOutputPort(AMIDI_OutputPort port) {
+ if (mOutputPortMap.find(port) == mOutputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ OutputPortEntry *entry = mOutputPortMap[port];
+ entry->state.store(MIDI_OUTPUT_PORT_STATE_OPEN_IDLE);
+ return OK;
+}
+
+status_t MidiPortRegistry::addInputPort(
+ AMIDI_Device device,
+ sp<IBinder> portToken,
+ base::unique_fd &&ufd,
+ AMIDI_InputPort *portPtr) {
+ *portPtr = mNextInputPortToken++;
+
+ InputPortEntry *entry = new InputPortEntry;
+
+ entry->state = MIDI_INPUT_PORT_STATE_OPEN_IDLE;
+ entry->port = new InputPort;
+ entry->port->device = device;
+ entry->port->binderToken = portToken;
+ entry->port->ufd = std::move(ufd);
+
+ mInputPortMap[*portPtr] = entry;
+
+ return OK;
+}
+
+status_t MidiPortRegistry::removeInputPort(
+ AMIDI_InputPort port,
+ AMIDI_Device *devicePtr,
+ sp<IBinder> *portTokenPtr) {
+ InputPortMap::iterator itr = mInputPortMap.find(port);
+ if (itr == mInputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ InputPortEntry *entry = mInputPortMap[port];
+ int portState = MIDI_INPUT_PORT_STATE_OPEN_IDLE;
+ while (!entry->state.compare_exchange_weak(portState, MIDI_INPUT_PORT_STATE_CLOSED)) {
+ if (portState == MIDI_INPUT_PORT_STATE_CLOSED) return -EINVAL; // Already closed
+ }
+
+ *devicePtr = entry->port->device;
+ *portTokenPtr = entry->port->binderToken;
+ delete entry->port;
+ entry->port = nullptr;
+
+ mInputPortMap.erase(itr);
+
+ return OK;
+}
+
+status_t MidiPortRegistry::getInputPortFd(AMIDI_InputPort port, base::unique_fd **ufdPtr) {
+ if (mInputPortMap.find(port) == mInputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ InputPortEntry *entry = mInputPortMap[port];
+
+ *ufdPtr = &entry->port->ufd;
+
+ return OK;
+}
+
+status_t MidiPortRegistry::getInputPortFdAndLock(AMIDI_InputPort port, base::unique_fd **ufdPtr) {
+ if (mInputPortMap.find(port) == mInputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ InputPortEntry *entry = mInputPortMap[port];
+
+ int portState = MIDI_INPUT_PORT_STATE_OPEN_IDLE;
+ if (!entry->state.compare_exchange_strong(portState, MIDI_INPUT_PORT_STATE_OPEN_ACTIVE)) {
+ // The port has been closed.
+ return -EPIPE;
+ }
+ *ufdPtr = &entry->port->ufd;
+ return OK;
+}
+
+status_t MidiPortRegistry::MidiPortRegistry::unlockInputPort(AMIDI_InputPort port) {
+ if (mInputPortMap.find(port) == mInputPortMap.end()) {
+ return -EINVAL;
+ }
+
+ InputPortEntry *entry = mInputPortMap[port];
+ entry->state.store(MIDI_INPUT_PORT_STATE_OPEN_IDLE);
+ return OK;
+}
+
+} // namespace midi
+} // namespace media
+} // namespace android
diff --git a/media/native/midi/MidiPortRegistry.h b/media/native/midi/MidiPortRegistry.h
new file mode 100644
index 000000000000..f1ffb78e98ab
--- /dev/null
+++ b/media/native/midi/MidiPortRegistry.h
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#ifndef ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_
+#define ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_
+
+#include <atomic>
+#include <map>
+
+#include <android-base/unique_fd.h>
+#include <binder/IBinder.h>
+#include <utils/Errors.h>
+#include <utils/Singleton.h>
+
+#include "midi.h"
+
+namespace android {
+namespace media {
+namespace midi {
+
+/*
+ * Maintains lists of all active input and output MIDI ports and controls access to them. Provides
+ * exclusive access to specific MIDI ports.
+ */
+class MidiPortRegistry : public Singleton<MidiPortRegistry> {
+ public:
+ /*
+ * Creates an output port entry and associates it with the specified MIDI device.
+ * Called by AMIDI_openOutputPort();
+ *
+ * device The native API device ID.
+ * portToken The port token (returned from the device server).
+ * udf File descriptor for the data port associated with the MIDI output port.
+ * portPtr Receives the native API port ID of the port being opened.
+ */
+ status_t addOutputPort(
+ AMIDI_Device device,
+ sp<IBinder> portToken,
+ base::unique_fd &&ufd,
+ AMIDI_OutputPort *portPtr);
+
+ /*
+ * Removes for the output port list a previously added output port.
+ * Called by AMIDI_closeOutputPort();
+ *
+ * port The native API port ID of the port being closed.
+ * devicePtr Receives the native API device ID associated with the port.
+ * portTokenPtr Receives the binder token associated with the port.
+ */
+ status_t removeOutputPort(
+ AMIDI_OutputPort port,
+ AMIDI_Device *devicePtr,
+ sp<IBinder> *portTokenPtr);
+
+ /*
+ * Creates an input port entry and associates it with the specified MIDI device.
+ * Called by AMIDI_openInputPort();
+ *
+ * device The native API device ID.
+ * portToken The port token (returned from the device server).
+ * udf File descriptor for the data port associated with the MIDI input port.
+ * portPtr Receives the native API port ID of the port being opened.
+ */
+ status_t addInputPort(
+ AMIDI_Device device,
+ sp<IBinder> portToken,
+ base::unique_fd &&ufd,
+ AMIDI_InputPort *portPtr);
+
+ /*
+ * Removes for the input port list a previously added input port.
+ * Called by AMIDI_closeINputPort();
+ *
+ * port The native API port ID of the port being closed.
+ * devicePtr Receives the native API device ID associated with the port.
+ * portTokenPtr Receives the binder token associated with the port.
+ */
+ status_t removeInputPort(
+ AMIDI_InputPort port,
+ AMIDI_Device *devicePtr,
+ sp<IBinder> *portTokenPtr);
+
+ /*
+ * Retrieves an exclusive-access file descriptor for an output port.
+ * Called from AMIDI_receive().
+ *
+ * port The native API id of the output port.
+ * ufdPtr Receives the exclusive-access file descriptor for the output port.
+ */
+ status_t getOutputPortFdAndLock(AMIDI_OutputPort port, base::unique_fd **ufdPtr);
+
+ /*
+ * Releases exclusive-access to the port and invalidates the previously received file
+ * descriptor.
+ * Called from AMIDI_receive().
+ *
+ * port The native API id of the output port.
+ */
+ status_t unlockOutputPort(AMIDI_OutputPort port);
+
+ /*
+ * Retrieves an exclusive-access file descriptor for an input port.
+ * (Not being used as (perhaps) AMIDI_sendWithTimestamp() doesn't need exclusive access
+ * to the port).
+ *
+ * port The native API id of the input port.
+ * ufdPtr Receives the exclusive-access file descriptor for the input port.
+ */
+ status_t getInputPortFdAndLock(AMIDI_InputPort port, base::unique_fd **ufdPtr);
+
+ /*
+ * Releases exclusive-access to the port and invalidates the previously received file
+ * descriptor.
+ * (Not used. See above).
+ *
+ * port The native API id of the input port.
+ */
+ status_t unlockInputPort(AMIDI_InputPort port);
+
+ /*
+ * Retrieves an unlocked (multi-access) file descriptor for an input port.
+ * Used by AMIDI_sendWith(), AMIDI_sendWithTimestamp & AMIDI_flush.
+ *
+ * port The native API id of the input port.
+ * ufdPtr Receives the multi-access file descriptor for the input port.
+ */
+ status_t getInputPortFd(AMIDI_InputPort port, base::unique_fd **ufdPtr);
+
+ private:
+ friend class Singleton<MidiPortRegistry>;
+ MidiPortRegistry();
+
+ /*
+ * Output (data receiving) ports.
+ */
+ struct OutputPort;
+ enum {
+ MIDI_OUTPUT_PORT_STATE_CLOSED = 0,
+ MIDI_OUTPUT_PORT_STATE_OPEN_IDLE,
+ MIDI_OUTPUT_PORT_STATE_OPEN_ACTIVE
+ };
+
+ struct OutputPortEntry {
+ std::atomic_int state;
+ OutputPort *port;
+ };
+
+ typedef std::map<AMIDI_OutputPort, OutputPortEntry*> OutputPortMap;
+ // Access is synchronized per record via 'state' field.
+ std::atomic<AMIDI_OutputPort> mNextOutputPortToken;
+ OutputPortMap mOutputPortMap;
+
+ /*
+ * Input (data sending) ports.
+ */
+ struct InputPort;
+ enum {
+ MIDI_INPUT_PORT_STATE_CLOSED = 0,
+ MIDI_INPUT_PORT_STATE_OPEN_IDLE,
+ MIDI_INPUT_PORT_STATE_OPEN_ACTIVE
+ };
+
+ struct InputPortEntry {
+ std::atomic_int state;
+ InputPort *port;
+ };
+
+ typedef std::map<AMIDI_OutputPort, InputPortEntry*> InputPortMap;
+ // Access is synchronized per record via 'state' field.
+ std::atomic<AMIDI_InputPort> mNextInputPortToken;
+ InputPortMap mInputPortMap;
+
+};
+
+} // namespace midi
+} // namespace media
+} // namespace android
+
+#endif // ANDROID_MEDIA_MIDI_PORT_REGISTRY_H_
diff --git a/media/native/midi/midi.cpp b/media/native/midi/midi.cpp
new file mode 100644
index 000000000000..1bf0bd080e94
--- /dev/null
+++ b/media/native/midi/midi.cpp
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#define LOG_TAG "NativeMIDI"
+
+#include <poll.h>
+#include <unistd.h>
+
+#include <binder/Binder.h>
+#include <utils/Errors.h>
+#include <utils/Log.h>
+
+#include "android/media/midi/BpMidiDeviceServer.h"
+#include "media/MidiDeviceInfo.h"
+#include "MidiDeviceRegistry.h"
+#include "MidiPortRegistry.h"
+
+#include "midi.h"
+
+using android::IBinder;
+using android::BBinder;
+using android::OK;
+using android::sp;
+using android::status_t;
+using android::base::unique_fd;
+using android::binder::Status;
+using android::media::midi::BpMidiDeviceServer;
+using android::media::midi::MidiDeviceInfo;
+using android::media::midi::MidiDeviceRegistry;
+using android::media::midi::MidiPortRegistry;
+
+#define SIZE_MIDIRECEIVEBUFFER AMIDI_BUFFER_SIZE
+
+/* TRANSFER PACKET FORMAT (as defined in MidiPortImpl.java)
+ *
+ * Transfer packet format is as follows (see MidiOutputPort.mThread.run() to see decomposition):
+ * |oc|md|md| ......... |md|ts|ts|ts|ts|ts|ts|ts|ts|
+ * ^ +--------------------+-----------------------+
+ * | ^ ^
+ * | | |
+ * | | + timestamp (8 bytes)
+ * | |
+ * | + MIDI data bytes (numBytes bytes)
+ * |
+ * + OpCode (AMIDI_OPCODE_DATA)
+ *
+ * NOTE: The socket pair is configured to use SOCK_SEQPACKET mode.
+ * SOCK_SEQPACKET, for a sequenced-packet socket that is connection-oriented, preserves message
+ * boundaries, and delivers messages in the order that they were sent.
+ * So 'read()' always returns a whole message.
+ */
+
+status_t AMIDI_getDeviceById(int32_t id, AMIDI_Device *devicePtr) {
+ return MidiDeviceRegistry::getInstance().obtainDeviceToken(id, devicePtr);
+}
+
+status_t AMIDI_getDeviceInfo(AMIDI_Device device, AMIDI_DeviceInfo *deviceInfoPtr) {
+ sp<BpMidiDeviceServer> deviceServer;
+ status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer);
+ if (result != OK) {
+ ALOGE("AMIDI_getDeviceInfo bad device token %d: %d", device, result);
+ return result;
+ }
+
+ MidiDeviceInfo deviceInfo;
+ Status txResult = deviceServer->getDeviceInfo(&deviceInfo);
+ if (!txResult.isOk()) {
+ ALOGE("AMIDI_getDeviceInfo transaction error: %d", txResult.transactionError());
+ return txResult.transactionError();
+ }
+
+ deviceInfoPtr->type = deviceInfo.getType();
+ deviceInfoPtr->uid = deviceInfo.getUid();
+ deviceInfoPtr->isPrivate = deviceInfo.isPrivate();
+ deviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size();
+ deviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size();
+ return OK;
+}
+
+/*
+ * Output (receiving) API
+ */
+status_t AMIDI_openOutputPort(AMIDI_Device device, int portNumber, AMIDI_OutputPort *outputPortPtr) {
+ sp<BpMidiDeviceServer> deviceServer;
+ status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer);
+ if (result != OK) {
+ ALOGE("AMIDI_openOutputPort bad device token %d: %d", device, result);
+ return result;
+ }
+
+ sp<BBinder> portToken(new BBinder());
+ unique_fd ufd;
+ Status txResult = deviceServer->openOutputPort(portToken, portNumber, &ufd);
+ if (!txResult.isOk()) {
+ ALOGE("AMIDI_openOutputPort transaction error: %d", txResult.transactionError());
+ return txResult.transactionError();
+ }
+
+ result = MidiPortRegistry::getInstance().addOutputPort(
+ device, portToken, std::move(ufd), outputPortPtr);
+ if (result != OK) {
+ ALOGE("AMIDI_openOutputPort port registration error: %d", result);
+ // Close port
+ return result;
+ }
+ return OK;
+}
+
+ssize_t AMIDI_receive(AMIDI_OutputPort outputPort, AMIDI_Message *messages, ssize_t maxMessages) {
+ unique_fd *ufd;
+ // TODO: May return a nicer self-unlocking object
+ status_t result = MidiPortRegistry::getInstance().getOutputPortFdAndLock(outputPort, &ufd);
+ if (result != OK) {
+ return result;
+ }
+
+ ssize_t messagesRead = 0;
+ while (messagesRead < maxMessages) {
+ struct pollfd checkFds[1] = { { *ufd, POLLIN, 0 } };
+ int pollResult = poll(checkFds, 1, 0);
+ if (pollResult < 1) {
+ result = android::INVALID_OPERATION;
+ break;
+ }
+
+ AMIDI_Message *message = &messages[messagesRead];
+ uint8_t readBuffer[AMIDI_PACKET_SIZE];
+ memset(readBuffer, 0, sizeof(readBuffer));
+ ssize_t readCount = read(*ufd, readBuffer, sizeof(readBuffer));
+ if (readCount == EINTR) {
+ continue;
+ }
+ if (readCount < 1) {
+ result = android::NOT_ENOUGH_DATA;
+ break;
+ }
+
+ // set Packet Format definition at the top of this file.
+ size_t dataSize = 0;
+ message->opcode = readBuffer[0];
+ message->timestamp = 0;
+ if (message->opcode == AMIDI_OPCODE_DATA && readCount >= AMIDI_PACKET_OVERHEAD) {
+ dataSize = readCount - AMIDI_PACKET_OVERHEAD;
+ if (dataSize) {
+ memcpy(message->buffer, readBuffer + 1, dataSize);
+ }
+ message->timestamp = *(uint64_t*) (readBuffer + readCount - sizeof(uint64_t));
+ }
+ message->len = dataSize;
+ ++messagesRead;
+ }
+
+ MidiPortRegistry::getInstance().unlockOutputPort(outputPort);
+ return result == OK ? messagesRead : result;
+}
+
+status_t AMIDI_closeOutputPort(AMIDI_OutputPort outputPort) {
+ AMIDI_Device device;
+ sp<IBinder> portToken;
+ status_t result =
+ MidiPortRegistry::getInstance().removeOutputPort(outputPort, &device, &portToken);
+ if (result != OK) {
+ return result;
+ }
+
+ sp<BpMidiDeviceServer> deviceServer;
+ result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer);
+ if (result != OK) {
+ return result;
+ }
+
+ Status txResult = deviceServer->closePort(portToken);
+ if (!txResult.isOk()) {
+ return txResult.transactionError();
+ }
+ return OK;
+}
+
+/*
+ * Input (sending) API
+ */
+status_t AMIDI_openInputPort(AMIDI_Device device, int portNumber, AMIDI_InputPort *inputPortPtr) {
+ sp<BpMidiDeviceServer> deviceServer;
+ status_t result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer);
+ if (result != OK) {
+ ALOGE("AMIDI_openInputPort bad device token %d: %d", device, result);
+ return result;
+ }
+
+ sp<BBinder> portToken(new BBinder());
+ unique_fd ufd; // this is the file descriptor of the "receive" port s
+ Status txResult = deviceServer->openInputPort(portToken, portNumber, &ufd);
+ if (!txResult.isOk()) {
+ ALOGE("AMIDI_openInputPort transaction error: %d", txResult.transactionError());
+ return txResult.transactionError();
+ }
+
+ result = MidiPortRegistry::getInstance().addInputPort(
+ device, portToken, std::move(ufd), inputPortPtr);
+ if (result != OK) {
+ ALOGE("AMIDI_openInputPort port registration error: %d", result);
+ // Close port
+ return result;
+ }
+
+ return OK;
+}
+
+status_t AMIDI_closeInputPort(AMIDI_InputPort inputPort) {
+ AMIDI_Device device;
+ sp<IBinder> portToken;
+ status_t result = MidiPortRegistry::getInstance().removeInputPort(
+ inputPort, &device, &portToken);
+ if (result != OK) {
+ ALOGE("AMIDI_closeInputPort remove port error: %d", result);
+ return result;
+ }
+
+ sp<BpMidiDeviceServer> deviceServer;
+ result = MidiDeviceRegistry::getInstance().getDeviceByToken(device, &deviceServer);
+ if (result != OK) {
+ ALOGE("AMIDI_closeInputPort can't find device error: %d", result);
+ return result;
+ }
+
+ Status txResult = deviceServer->closePort(portToken);
+ if (!txResult.isOk()) {
+ result = txResult.transactionError();
+ ALOGE("AMIDI_closeInputPort transaction error: %d", result);
+ return result;
+ }
+
+ return OK;
+}
+
+ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort /*inputPort*/) {
+ return SIZE_MIDIRECEIVEBUFFER;
+}
+
+static ssize_t AMIDI_makeSendBuffer(uint8_t *buffer, uint8_t *data, ssize_t numBytes, uint64_t timestamp) {
+ buffer[0] = AMIDI_OPCODE_DATA;
+ memcpy(buffer + 1, data, numBytes);
+ memcpy(buffer + 1 + numBytes, &timestamp, sizeof(timestamp));
+ return numBytes + AMIDI_PACKET_OVERHEAD;
+}
+
+// Handy debugging function.
+//static void AMIDI_logBuffer(uint8_t *data, size_t numBytes) {
+// for (size_t index = 0; index < numBytes; index++) {
+// ALOGI(" data @%zu [0x%X]", index, data[index]);
+// }
+//}
+
+ssize_t AMIDI_send(AMIDI_InputPort inputPort, uint8_t *buffer, ssize_t numBytes) {
+ return AMIDI_sendWithTimestamp(inputPort, buffer, numBytes, 0);
+}
+
+ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort inputPort, uint8_t *data,
+ ssize_t numBytes, int64_t timestamp) {
+
+ if (numBytes > SIZE_MIDIRECEIVEBUFFER) {
+ return android::BAD_VALUE;
+ }
+
+ // AMIDI_logBuffer(data, numBytes);
+
+ unique_fd *ufd = NULL;
+ status_t result = MidiPortRegistry::getInstance().getInputPortFd(inputPort, &ufd);
+ if (result != OK) {
+ return result;
+ }
+
+ uint8_t writeBuffer[SIZE_MIDIRECEIVEBUFFER + AMIDI_PACKET_OVERHEAD];
+ ssize_t numTransferBytes = AMIDI_makeSendBuffer(writeBuffer, data, numBytes, timestamp);
+ ssize_t numWritten = write(*ufd, writeBuffer, numTransferBytes);
+
+ if (numWritten < numTransferBytes) {
+ ALOGE("AMIDI_sendWithTimestamp Couldn't write MIDI data buffer. requested:%zu, written%zu",
+ numTransferBytes, numWritten);
+ }
+
+ return numWritten - AMIDI_PACKET_OVERHEAD;
+}
+
+status_t AMIDI_flush(AMIDI_InputPort inputPort) {
+ unique_fd *ufd = NULL;
+ status_t result = MidiPortRegistry::getInstance().getInputPortFd(inputPort, &ufd);
+ if (result != OK) {
+ return result;
+ }
+
+ uint8_t opCode = AMIDI_OPCODE_FLUSH;
+ ssize_t numTransferBytes = 1;
+ ssize_t numWritten = write(*ufd, &opCode, numTransferBytes);
+
+ if (numWritten < numTransferBytes) {
+ ALOGE("AMIDI_flush Couldn't write MIDI flush. requested:%zu, written%zu",
+ numTransferBytes, numWritten);
+ return android::INVALID_OPERATION;
+ }
+
+ return OK;
+}
+
diff --git a/media/native/midi/midi.h b/media/native/midi/midi.h
new file mode 100644
index 000000000000..717bc666253e
--- /dev/null
+++ b/media/native/midi/midi.h
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#ifndef ANDROID_MEDIA_MIDI_H_
+#define ANDROID_MEDIA_MIDI_H_
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include <utils/Errors.h>
+
+using android::status_t;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//typedef struct _AMIDI_Device;
+//typedef struct _AMIDI_InputPort;
+//typedef struct _AMIDI_OutputPort;
+//typedef _AMIDI_Device* AMIDI_Device;
+//typedef _AMIDI_InputPort* AMIDI_InputPort;
+//typedef _AMIDI_OutputPort* AMIDI_OutputPort;
+
+typedef int32_t AMIDI_Device;
+typedef int32_t AMIDI_InputPort;
+typedef int32_t AMIDI_OutputPort;
+
+//TODO - Do we want to wrap this stuff in namespace android { namespace media { namespace midi {?
+
+enum {
+ AMIDI_INVALID_HANDLE = -1
+};
+
+enum {
+ AMIDI_OPCODE_DATA = 1,
+ AMIDI_OPCODE_FLUSH = 2,
+ AMIDI_PACKET_SIZE = 1024, /* !!! Currently MidiPortImpl.MAX_PACKET_SIZE !!! */
+ AMIDI_PACKET_OVERHEAD = 9,
+ AMIDI_BUFFER_SIZE = AMIDI_PACKET_SIZE - AMIDI_PACKET_OVERHEAD
+ /* !!! TBD, currently MidiPortImpl.MAX_PACKET_DATA_SIZE !!! */
+};
+
+typedef struct {
+ uint32_t opcode;
+ uint8_t buffer[AMIDI_BUFFER_SIZE];
+ size_t len;
+ int64_t timestamp;
+} AMIDI_Message;
+
+enum {
+ AMIDI_DEVICE_TYPE_USB = 1,
+ AMIDI_DEVICE_TYPE_VIRTUAL = 2,
+ AMIDI_DEVICE_TYPE_BLUETOOTH = 3
+};
+
+typedef struct {
+ int32_t type;
+ int32_t uid;
+ int32_t isPrivate;
+ int32_t inputPortCount;
+ int32_t outputPortCount;
+} AMIDI_DeviceInfo;
+
+/*
+ * Device API
+ */
+/*
+ * Retrieves the native API device token for the specified Java API device ID.
+ *
+ * uid The Java API id of the device.
+ * devicePtr Receives the associated native API token for the device.
+ *
+ * Returns OK or a (negative) error code.
+ */
+status_t AMIDI_getDeviceById(int32_t id, AMIDI_Device *devicePtr);
+
+/*
+ * Retrieves information for the native MIDI device.
+ *
+ * device The Native API token for the device.
+ * deviceInfoPtr Receives the associated device info.
+ *
+ * Returns OK or a (negative) error code.
+ */
+status_t AMIDI_getDeviceInfo(AMIDI_Device device, AMIDI_DeviceInfo *deviceInfoPtr);
+
+/*
+ * API for receiving data from the Output port of a device.
+ */
+/*
+ * Opens the output port.
+ *
+ * device Identifies the device.
+ * portNumber Specifies the zero-based port index on the device to open.
+ * outputPortPtr Receives the native API port identifier of the opened port.
+ *
+ * Returns OK, or a (negative) error code.
+ */
+status_t AMIDI_openOutputPort(AMIDI_Device device, int portNumber, AMIDI_OutputPort *outputPortPtr);
+
+/*
+ * Receives any pending MIDI messages (up to the specified maximum number of messages).
+ *
+ * outputPort Identifies the port to receive messages from.
+ * messages Points to an array (size maxMessages) to receive the MIDI messages.
+ * maxMessages The number of messages allocated in the messages array.
+ *
+ * Returns the number of messages received, or a (negative) error code.
+ */
+ssize_t AMIDI_receive(AMIDI_OutputPort outputPort, AMIDI_Message *messages, ssize_t maxMessages);
+
+/*
+ * Closes the output port.
+ *
+ * outputPort The native API port identifier of the port.
+ *
+ * Returns OK, or a (negative) error code.
+ */
+status_t AMIDI_closeOutputPort(AMIDI_OutputPort outputPort);
+
+/*
+ * API for sending data to the Input port of a device.
+ */
+/*
+ * Opens the input port.
+ *
+ * device Identifies the device.
+ * portNumber Specifies the zero-based port index on the device to open.
+ * inputPortPtr Receives the native API port identifier of the opened port.
+ *
+ * Returns OK, or a (negative) error code.
+ */
+status_t AMIDI_openInputPort(AMIDI_Device device, int portNumber, AMIDI_InputPort *inputPortPtr);
+
+/*
+ * Returns the maximum number of bytes that can be received in a single MIDI message.
+ */
+ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort inputPort);
+
+/*
+ * Sends data to the specified input port.
+ *
+ * inputPort The native API identifier of the port to send data to.
+ * buffer Points to the array of bytes containing the data to send.
+ * numBytes Specifies the number of bytes to write.
+ *
+ * Returns The number of bytes sent or a (negative) error code.
+ */
+ssize_t AMIDI_send(AMIDI_InputPort inputPort, uint8_t *buffer, ssize_t numBytes);
+
+/*
+ * Sends data to the specified input port with a timestamp.
+ *
+ * inputPort The native API identifier of the port to send data to.
+ * buffer Points to the array of bytes containing the data to send.
+ * numBytes Specifies the number of bytes to write.
+ * timestamp The time stamp to associate with the sent data.
+ *
+ * Returns The number of bytes sent or a (negative) error code.
+ */
+ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort inputPort, uint8_t *buffer,
+ ssize_t numBytes, int64_t timestamp);
+
+/*
+ * Sends a message with a 'MIDI flush command code' to the specified port.
+ *
+ * inputPort The native API identifier of the port to send the flush message to.
+ *
+ * Returns OK, or a (negative) error code.
+ */
+status_t AMIDI_flush(AMIDI_InputPort inputPort);
+
+/*
+ * Closes the input port.
+ *
+ * inputPort The native API port identifier of the port.
+ *
+ *
+ * Returns OK, or a (negative) error code.
+ */
+status_t AMIDI_closeInputPort(AMIDI_InputPort inputPort);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ANDROID_MEDIA_MIDI_H_ */
diff --git a/media/tests/NativeMidiDemo/Android.mk b/media/tests/NativeMidiDemo/Android.mk
new file mode 100644
index 000000000000..6b08f6bba3f4
--- /dev/null
+++ b/media/tests/NativeMidiDemo/Android.mk
@@ -0,0 +1,30 @@
+# Copyright (C) 2016 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := NativeMidiDemo
+
+#LOCAL_SDK_VERSION := current
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_SRC_FILES := $(call all-java-files-under, java)
+
+LOCAL_JNI_SHARED_LIBRARIES := libnativemidi_jni
+
+include $(BUILD_PACKAGE)
+
+# Include packages in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/media/tests/NativeMidiDemo/AndroidManifest.xml b/media/tests/NativeMidiDemo/AndroidManifest.xml
new file mode 100644
index 000000000000..322873f11895
--- /dev/null
+++ b/media/tests/NativeMidiDemo/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.nativemididemo"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application
+ android:allowBackup="false"
+ android:fullBackupContent="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name">
+ <uses-feature android:name="android.software.midi" android:required="true"/>
+ <activity android:name=".NativeMidi"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java
new file mode 100644
index 000000000000..0969b10b1f90
--- /dev/null
+++ b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/NativeMidi.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2016 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 com.example.android.nativemididemo;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiOutputPort;
+import android.media.midi.MidiReceiver;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import java.io.IOException;
+
+public class NativeMidi extends Activity
+{
+ private TextView mCallbackStatusTextView;
+ private TextView mJavaMidiStatusTextView;
+ private TextView mMessagesTextView;
+ private RadioGroup mMidiDevicesRadioGroup;
+ private Handler mTimerHandler = new Handler();
+ private boolean mAudioWorks;
+ private final int mMinFramesPerBuffer = 32; // See {min|max}PlaySamples in nativemidi-jni.cpp
+ private final int mMaxFramesPerBuffer = 1000;
+ private int mFramesPerBuffer;
+
+ private TouchableScrollView mMessagesContainer;
+ private MidiManager mMidiManager;
+ private MidiOutputPortSelector mActivePortSelector;
+
+ private Runnable mTimerRunnable = new Runnable() {
+ private long mLastTime;
+ private long mLastPlaybackCounter;
+ private int mLastCallbackRate;
+ private long mLastUntouchedTime;
+
+ @Override
+ public void run() {
+ final long checkIntervalMs = 1000;
+ long currentTime = System.currentTimeMillis();
+ long currentPlaybackCounter = getPlaybackCounter();
+ if (currentTime - mLastTime >= checkIntervalMs) {
+ int callbackRate = Math.round(
+ (float)(currentPlaybackCounter - mLastPlaybackCounter) /
+ ((float)(currentTime - mLastTime) / (float)1000));
+ if (mLastCallbackRate != callbackRate) {
+ mCallbackStatusTextView.setText(
+ "CB: " + callbackRate + " Hz");
+ mLastCallbackRate = callbackRate;
+ }
+ mLastTime = currentTime;
+ mLastPlaybackCounter = currentPlaybackCounter;
+ }
+
+ String[] newMessages = getRecentMessages();
+ if (newMessages != null) {
+ for (String message : newMessages) {
+ mMessagesTextView.append(message);
+ mMessagesTextView.append("\n");
+ }
+ if (!mMessagesContainer.isTouched) {
+ if (mLastUntouchedTime == 0) mLastUntouchedTime = currentTime;
+ if (currentTime - mLastUntouchedTime > 3000) {
+ mMessagesContainer.fullScroll(View.FOCUS_DOWN);
+ }
+ } else {
+ mLastUntouchedTime = 0;
+ }
+ }
+
+ mTimerHandler.postDelayed(this, checkIntervalMs / 4);
+ }
+ };
+
+ private void addMessage(final String message) {
+ mTimerHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mMessagesTextView.append(message);
+ }
+ }, 0);
+ }
+
+ private class MidiOutputPortSelector implements View.OnClickListener {
+ private final MidiDeviceInfo mDeviceInfo;
+ private final int mPortNumber;
+ private MidiDevice mDevice;
+ private MidiOutputPort mOutputPort;
+
+ MidiOutputPortSelector() {
+ mDeviceInfo = null;
+ mPortNumber = -1;
+ }
+
+ MidiOutputPortSelector(MidiDeviceInfo info, int portNumber) {
+ mDeviceInfo = info;
+ mPortNumber = portNumber;
+ }
+
+ MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; }
+
+ @Override
+ public void onClick(View v) {
+ if (mActivePortSelector != null) {
+ mActivePortSelector.close();
+ mActivePortSelector = null;
+ }
+ if (mDeviceInfo == null) {
+ mActivePortSelector = this;
+ return;
+ }
+ mMidiManager.openDevice(mDeviceInfo, new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ addMessage("! Failed to open MIDI device !\n");
+ } else {
+ mDevice = device;
+ try {
+ mDevice.mirrorToNative();
+ startReadingMidi(mDevice.getInfo().getId(), mPortNumber);
+ } catch (IOException e) {
+ addMessage("! Failed to mirror to native !\n" + e.getMessage() + "\n");
+ }
+
+ mActivePortSelector = MidiOutputPortSelector.this;
+
+ mOutputPort = device.openOutputPort(mPortNumber);
+ mOutputPort.connect(mMidiReceiver);
+ }
+ }
+ }, null);
+ }
+
+ void closePortOnly() {
+ stopReadingMidi();
+ }
+
+ void close() {
+ closePortOnly();
+ try {
+ if (mOutputPort != null) {
+ mOutputPort.close();
+ }
+ } catch (IOException e) {
+ mMessagesTextView.append("! Port close error: " + e + "\n");
+ } finally {
+ mOutputPort = null;
+ }
+ try {
+ if (mDevice != null) {
+ mDevice.close();
+ }
+ } catch (IOException e) {
+ mMessagesTextView.append("! Device close error: " + e + "\n");
+ } finally {
+ mDevice = null;
+ }
+ }
+ }
+
+ private MidiManager.DeviceCallback mMidiDeviceCallback = new MidiManager.DeviceCallback() {
+ @Override
+ public void onDeviceAdded(MidiDeviceInfo info) {
+ Bundle deviceProps = info.getProperties();
+ String deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_NAME);
+ if (deviceName == null) {
+ deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_MANUFACTURER);
+ }
+
+ for (MidiDeviceInfo.PortInfo port : info.getPorts()) {
+ if (port.getType() != MidiDeviceInfo.PortInfo.TYPE_OUTPUT) continue;
+ String portName = port.getName();
+ int portNumber = port.getPortNumber();
+ if (portName.length() == 0) portName = "[" + portNumber + "]";
+ portName += "@" + deviceName;
+ RadioButton outputDevice = new RadioButton(NativeMidi.this);
+ outputDevice.setText(portName);
+ outputDevice.setTag(info);
+ outputDevice.setOnClickListener(new MidiOutputPortSelector(info, portNumber));
+ mMidiDevicesRadioGroup.addView(outputDevice);
+ }
+
+ NativeMidi.this.updateKeepScreenOn();
+ }
+
+ @Override
+ public void onDeviceRemoved(MidiDeviceInfo info) {
+ if (mActivePortSelector != null && info.equals(mActivePortSelector.getDeviceInfo())) {
+ mActivePortSelector.close();
+ mActivePortSelector = null;
+ }
+ int removeButtonStart = -1, removeButtonCount = 0;
+ final int buttonCount = mMidiDevicesRadioGroup.getChildCount();
+ boolean checked = false;
+ for (int i = 0; i < buttonCount; ++i) {
+ RadioButton button = (RadioButton) mMidiDevicesRadioGroup.getChildAt(i);
+ if (!info.equals(button.getTag())) continue;
+ if (removeButtonStart == -1) removeButtonStart = i;
+ ++removeButtonCount;
+ if (button.isChecked()) checked = true;
+ }
+ if (removeButtonStart != -1) {
+ mMidiDevicesRadioGroup.removeViews(removeButtonStart, removeButtonCount);
+ if (checked) {
+ mMidiDevicesRadioGroup.check(R.id.device_none);
+ }
+ }
+
+ NativeMidi.this.updateKeepScreenOn();
+ }
+ };
+
+ private class JavaMidiReceiver extends MidiReceiver implements Runnable {
+ @Override
+ public void onSend(byte[] data, int offset,
+ int count, long timestamp) throws IOException {
+ mTimerHandler.removeCallbacks(this);
+ mTimerHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mJavaMidiStatusTextView.setText("Java: MSG");
+ }
+ }, 0);
+ mTimerHandler.postDelayed(this, 100);
+ }
+
+ @Override
+ public void run() {
+ mJavaMidiStatusTextView.setText("Java: ---");
+ }
+ }
+
+ private JavaMidiReceiver mMidiReceiver = new JavaMidiReceiver();
+
+ private void updateKeepScreenOn() {
+ if (mMidiDevicesRadioGroup.getChildCount() > 1) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ mCallbackStatusTextView = (TextView) findViewById(R.id.callback_status);
+ mJavaMidiStatusTextView = (TextView) findViewById(R.id.java_midi_status);
+ mMessagesTextView = (TextView) findViewById(R.id.messages);
+ mMessagesContainer = (TouchableScrollView) findViewById(R.id.messages_scroll);
+ mMidiDevicesRadioGroup = (RadioGroup) findViewById(R.id.devices);
+ RadioButton deviceNone = (RadioButton) findViewById(R.id.device_none);
+ deviceNone.setOnClickListener(new MidiOutputPortSelector());
+
+ AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+ if (sampleRate == null) sampleRate = "48000";
+ String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+ if (framesPerBuffer == null) framesPerBuffer = Integer.toString(mMaxFramesPerBuffer);
+ mFramesPerBuffer = Integer.parseInt(framesPerBuffer);
+ String audioInitResult = initAudio(Integer.parseInt(sampleRate), mFramesPerBuffer);
+ mMessagesTextView.append("Open SL ES init: " + audioInitResult + "\n");
+
+ if (audioInitResult.startsWith("Success")) {
+ mAudioWorks = true;
+ mTimerHandler.postDelayed(mTimerRunnable, 0);
+ mTimerHandler.postDelayed(mMidiReceiver, 0);
+ }
+
+ mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE);
+ mMidiManager.registerDeviceCallback(mMidiDeviceCallback, new Handler());
+ for (MidiDeviceInfo info : mMidiManager.getDevices()) {
+ mMidiDeviceCallback.onDeviceAdded(info);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mAudioWorks) {
+ mTimerHandler.removeCallbacks(mTimerRunnable);
+ pauseAudio();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mAudioWorks) {
+ mTimerHandler.postDelayed(mTimerRunnable, 0);
+ resumeAudio();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mActivePortSelector != null) {
+ mActivePortSelector.close();
+ mActivePortSelector = null;
+ }
+ shutdownAudio();
+ super.onDestroy();
+ }
+
+ public void onClearMessages(View v) {
+ mMessagesTextView.setText("");
+ }
+
+ public void onClosePort(View v) {
+ if (mActivePortSelector != null) {
+ mActivePortSelector.closePortOnly();
+ }
+ }
+
+ private native String initAudio(int sampleRate, int playSamples);
+ private native void pauseAudio();
+ private native void resumeAudio();
+ private native void shutdownAudio();
+
+ private native long getPlaybackCounter();
+ private native String[] getRecentMessages();
+
+ private native void startReadingMidi(int deviceId, int portNumber);
+ private native void stopReadingMidi();
+
+ static {
+ System.loadLibrary("nativemidi_jni");
+ }
+}
diff --git a/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java
new file mode 100644
index 000000000000..645aafa88507
--- /dev/null
+++ b/media/tests/NativeMidiDemo/java/com/example/android/nativemididemo/TouchableScrollView.java
@@ -0,0 +1,32 @@
+package com.example.android.nativemididemo;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+public class TouchableScrollView extends ScrollView {
+ public boolean isTouched;
+
+ public TouchableScrollView(Context context) {
+ super(context);
+ }
+
+ public TouchableScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ isTouched = true;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ isTouched = false;
+ break;
+ }
+ return super.onTouchEvent(event);
+ }
+}
diff --git a/media/tests/NativeMidiDemo/jni/Android.mk b/media/tests/NativeMidiDemo/jni/Android.mk
new file mode 100644
index 000000000000..69a64bd0218b
--- /dev/null
+++ b/media/tests/NativeMidiDemo/jni/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2016 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_MODULE := libnativemidi_jni
+
+LOCAL_SRC_FILES := \
+ nativemidi-jni.cpp \
+ messagequeue.cpp
+
+LOCAL_CFLAGS += -Wall -Wextra -Werror -O0
+
+LOCAL_C_INCLUDES += \
+ frameworks/base/media/native
+
+LOCAL_CXX_STL := libc++_static
+
+LOCAL_SHARED_LIBRARIES := libOpenSLES libmidi
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/media/tests/NativeMidiDemo/jni/messagequeue.cpp b/media/tests/NativeMidiDemo/jni/messagequeue.cpp
new file mode 100644
index 000000000000..ffaef38bed8c
--- /dev/null
+++ b/media/tests/NativeMidiDemo/jni/messagequeue.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ */
+
+#include <atomic>
+#include <stdio.h>
+#include <string.h>
+
+#include "messagequeue.h"
+
+namespace nativemididemo {
+
+static const int messageBufferSize = 64 * 1024;
+static char messageBuffer[messageBufferSize];
+static std::atomic_ullong messagesLastWritePosition;
+
+void writeMessage(const char* message)
+{
+ static unsigned long long lastWritePos = 0;
+ size_t messageLen = strlen(message);
+ if (messageLen == 0) return;
+
+ messageLen += 1; // Also count in the null terminator.
+ char buffer[1024];
+ if (messageLen >= messageBufferSize) {
+ snprintf(buffer, sizeof(buffer), "!!! Message too long: %zu bytes !!!", messageLen);
+ message = buffer;
+ messageLen = strlen(message);
+ }
+
+ size_t wrappedWritePos = lastWritePos % messageBufferSize;
+ if (wrappedWritePos + messageLen >= messageBufferSize) {
+ size_t tailLen = messageBufferSize - wrappedWritePos;
+ memset(messageBuffer + wrappedWritePos, 0, tailLen);
+ lastWritePos += tailLen;
+ wrappedWritePos = 0;
+ }
+
+ memcpy(messageBuffer + wrappedWritePos, message, messageLen);
+ lastWritePos += messageLen;
+ messagesLastWritePosition.store(lastWritePos);
+}
+
+static char messageBufferCopy[messageBufferSize];
+
+jobjectArray getRecentMessagesForJava(JNIEnv* env, jobject)
+{
+ static unsigned long long lastReadPos = 0;
+ const char* overrunMessage = "";
+ size_t messagesCount = 0;
+ jobjectArray result = NULL;
+
+ // First we copy the portion of the message buffer into messageBufferCopy. If after finishing
+ // the copy we notice that the writer has mutated the portion of the buffer that we were
+ // copying, we report an overrun. Afterwards we can safely read messages from the copy.
+ memset(messageBufferCopy, 0, sizeof(messageBufferCopy));
+ unsigned long long lastWritePos = messagesLastWritePosition.load();
+ if (lastWritePos - lastReadPos > messageBufferSize) {
+ overrunMessage = "!!! Message buffer overrun !!!";
+ messagesCount = 1;
+ lastReadPos = lastWritePos;
+ goto create_array;
+ }
+ if (lastWritePos == lastReadPos) return result;
+ if (lastWritePos / messageBufferSize == lastReadPos / messageBufferSize) {
+ size_t wrappedReadPos = lastReadPos % messageBufferSize;
+ memcpy(messageBufferCopy + wrappedReadPos,
+ messageBuffer + wrappedReadPos,
+ lastWritePos % messageBufferSize - wrappedReadPos);
+ } else {
+ size_t wrappedReadPos = lastReadPos % messageBufferSize;
+ memcpy(messageBufferCopy, messageBuffer, lastWritePos % messageBufferSize);
+ memcpy(messageBufferCopy + wrappedReadPos,
+ messageBuffer + wrappedReadPos,
+ messageBufferSize - wrappedReadPos);
+ }
+ {
+ unsigned long long newLastWritePos = messagesLastWritePosition.load();
+ if (newLastWritePos - lastReadPos > messageBufferSize) {
+ overrunMessage = "!!! Message buffer overrun !!!";
+ messagesCount = 1;
+ lastReadPos = lastWritePos = newLastWritePos;
+ goto create_array;
+ }
+ }
+ // Otherwise we ignore newLastWritePos, since we only have a copy of the buffer
+ // up to lastWritePos.
+
+ for (unsigned long long readPos = lastReadPos; readPos < lastWritePos; ) {
+ size_t messageLen = strlen(messageBufferCopy + (readPos % messageBufferSize));
+ if (messageLen != 0) {
+ readPos += messageLen + 1;
+ messagesCount++;
+ } else {
+ // Skip to the beginning of the buffer.
+ readPos = (readPos / messageBufferSize + 1) * messageBufferSize;
+ }
+ }
+ if (messagesCount == 0) {
+ lastReadPos = lastWritePos;
+ return result;
+ }
+
+create_array:
+ result = env->NewObjectArray(
+ messagesCount, env->FindClass("java/lang/String"), env->NewStringUTF(overrunMessage));
+ if (lastWritePos == lastReadPos) return result;
+
+ jsize arrayIndex = 0;
+ while (lastReadPos < lastWritePos) {
+ size_t wrappedReadPos = lastReadPos % messageBufferSize;
+ if (messageBufferCopy[wrappedReadPos] != '\0') {
+ jstring message = env->NewStringUTF(messageBufferCopy + wrappedReadPos);
+ env->SetObjectArrayElement(result, arrayIndex++, message);
+ lastReadPos += env->GetStringLength(message) + 1;
+ env->DeleteLocalRef(message);
+ } else {
+ // Skip to the beginning of the buffer.
+ lastReadPos = (lastReadPos / messageBufferSize + 1) * messageBufferSize;
+ }
+ }
+ return result;
+}
+
+} // namespace nativemididemo
diff --git a/media/tests/NativeMidiDemo/jni/messagequeue.h b/media/tests/NativeMidiDemo/jni/messagequeue.h
new file mode 100644
index 000000000000..20aa9e805d12
--- /dev/null
+++ b/media/tests/NativeMidiDemo/jni/messagequeue.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ */
+
+#ifndef NATIVEMIDIDEMO_MESSAGEQUEUE_H
+#define NATIVEMIDIDEMO_MESSAGEQUEUE_H
+
+#include <jni.h>
+
+namespace nativemididemo {
+
+void writeMessage(const char* message);
+jobjectArray getRecentMessagesForJava(JNIEnv* env, jobject thiz);
+
+} // namespace nativemididemo
+
+#endif // NATIVEMIDIDEMO_MESSAGEQUEUE_H
diff --git a/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp b/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp
new file mode 100644
index 000000000000..8aa874ae0748
--- /dev/null
+++ b/media/tests/NativeMidiDemo/jni/nativemidi-jni.cpp
@@ -0,0 +1,285 @@
+#include <atomic>
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <jni.h>
+
+#include <midi/midi.h>
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+
+#include "messagequeue.h"
+
+extern "C" {
+JNIEXPORT jstring JNICALL Java_com_example_android_nativemididemo_NativeMidi_initAudio(
+ JNIEnv* env, jobject thiz, jint sampleRate, jint playSamples);
+JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_pauseAudio(
+ JNIEnv* env, jobject thiz);
+JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_resumeAudio(
+ JNIEnv* env, jobject thiz);
+JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_shutdownAudio(
+ JNIEnv* env, jobject thiz);
+JNIEXPORT jlong JNICALL Java_com_example_android_nativemididemo_NativeMidi_getPlaybackCounter(
+ JNIEnv* env, jobject thiz);
+JNIEXPORT jobjectArray JNICALL Java_com_example_android_nativemididemo_NativeMidi_getRecentMessages(
+ JNIEnv* env, jobject thiz);
+JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_startReadingMidi(
+ JNIEnv* env, jobject thiz, jint deviceId, jint portNumber);
+JNIEXPORT void JNICALL Java_com_example_android_nativemididemo_NativeMidi_stopReadingMidi(
+ JNIEnv* env, jobject thiz);
+}
+
+static const char* errStrings[] = {
+ "SL_RESULT_SUCCESS", // 0
+ "SL_RESULT_PRECONDITIONS_VIOLATED", // 1
+ "SL_RESULT_PARAMETER_INVALID", // 2
+ "SL_RESULT_MEMORY_FAILURE", // 3
+ "SL_RESULT_RESOURCE_ERROR", // 4
+ "SL_RESULT_RESOURCE_LOST", // 5
+ "SL_RESULT_IO_ERROR", // 6
+ "SL_RESULT_BUFFER_INSUFFICIENT", // 7
+ "SL_RESULT_CONTENT_CORRUPTED", // 8
+ "SL_RESULT_CONTENT_UNSUPPORTED", // 9
+ "SL_RESULT_CONTENT_NOT_FOUND", // 10
+ "SL_RESULT_PERMISSION_DENIED", // 11
+ "SL_RESULT_FEATURE_UNSUPPORTED", // 12
+ "SL_RESULT_INTERNAL_ERROR", // 13
+ "SL_RESULT_UNKNOWN_ERROR", // 14
+ "SL_RESULT_OPERATION_ABORTED", // 15
+ "SL_RESULT_CONTROL_LOST" }; // 16
+static const char* getSLErrStr(int code) {
+ return errStrings[code];
+}
+
+static SLObjectItf engineObject;
+static SLEngineItf engineEngine;
+static SLObjectItf outputMixObject;
+static SLObjectItf playerObject;
+static SLPlayItf playerPlay;
+static SLAndroidSimpleBufferQueueItf playerBufferQueue;
+
+static const int minPlaySamples = 32;
+static const int maxPlaySamples = 1000;
+static std::atomic_int playSamples(maxPlaySamples);
+static short playBuffer[maxPlaySamples];
+
+static std::atomic_ullong sharedCounter;
+
+static AMIDI_Device midiDevice = AMIDI_INVALID_HANDLE;
+static std::atomic<AMIDI_OutputPort> midiOutputPort(AMIDI_INVALID_HANDLE);
+
+static int setPlaySamples(int newPlaySamples)
+{
+ if (newPlaySamples < minPlaySamples) newPlaySamples = minPlaySamples;
+ if (newPlaySamples > maxPlaySamples) newPlaySamples = maxPlaySamples;
+ playSamples.store(newPlaySamples);
+ return newPlaySamples;
+}
+
+// Amount of messages we are ready to handle during one callback cycle.
+static const size_t MAX_INCOMING_MIDI_MESSAGES = 20;
+// Static allocation to save time in the callback.
+static AMIDI_Message incomingMidiMessages[MAX_INCOMING_MIDI_MESSAGES];
+
+static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void */*context*/)
+{
+ sharedCounter++;
+
+ AMIDI_OutputPort outputPort = midiOutputPort.load();
+ if (outputPort != AMIDI_INVALID_HANDLE) {
+ char midiDumpBuffer[1024];
+ ssize_t midiReceived = AMIDI_receive(
+ outputPort, incomingMidiMessages, MAX_INCOMING_MIDI_MESSAGES);
+ if (midiReceived >= 0) {
+ for (ssize_t i = 0; i < midiReceived; ++i) {
+ AMIDI_Message* msg = &incomingMidiMessages[i];
+ if (msg->opcode == AMIDI_OPCODE_DATA) {
+ memset(midiDumpBuffer, 0, sizeof(midiDumpBuffer));
+ int pos = snprintf(midiDumpBuffer, sizeof(midiDumpBuffer),
+ "%" PRIx64 " ", msg->timestamp);
+ for (uint8_t *b = msg->buffer, *e = b + msg->len; b < e; ++b) {
+ pos += snprintf(midiDumpBuffer + pos, sizeof(midiDumpBuffer) - pos,
+ "%02x ", *b);
+ }
+ nativemididemo::writeMessage(midiDumpBuffer);
+ } else if (msg->opcode == AMIDI_OPCODE_FLUSH) {
+ nativemididemo::writeMessage("MIDI flush");
+ }
+ }
+ } else {
+ snprintf(midiDumpBuffer, sizeof(midiDumpBuffer),
+ "! MIDI Receive error: %s !", strerror(-midiReceived));
+ nativemididemo::writeMessage(midiDumpBuffer);
+ }
+ }
+
+ size_t usedBufferSize = playSamples.load() * sizeof(playBuffer[0]);
+ if (usedBufferSize > sizeof(playBuffer)) {
+ usedBufferSize = sizeof(playBuffer);
+ }
+ (*bq)->Enqueue(bq, playBuffer, usedBufferSize);
+}
+
+jstring Java_com_example_android_nativemididemo_NativeMidi_initAudio(
+ JNIEnv* env, jobject, jint sampleRate, jint playSamples) {
+ const char* stage;
+ SLresult result;
+ char printBuffer[1024];
+
+ playSamples = setPlaySamples(playSamples);
+
+ result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
+ if (SL_RESULT_SUCCESS != result) { stage = "slCreateEngine"; goto handle_error; }
+
+ result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
+ if (SL_RESULT_SUCCESS != result) { stage = "realize Engine object"; goto handle_error; }
+
+ result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
+ if (SL_RESULT_SUCCESS != result) { stage = "get Engine interface"; goto handle_error; }
+
+ result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
+ if (SL_RESULT_SUCCESS != result) { stage = "CreateOutputMix"; goto handle_error; }
+
+ result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
+ if (SL_RESULT_SUCCESS != result) { stage = "realize OutputMix object"; goto handle_error; }
+
+ {
+ SLDataFormat_PCM format_pcm = { SL_DATAFORMAT_PCM, 1, (SLuint32)sampleRate * 1000,
+ SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
+ SL_SPEAKER_FRONT_LEFT, SL_BYTEORDER_LITTLEENDIAN };
+ SLDataLocator_AndroidSimpleBufferQueue loc_bufq =
+ { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };
+ SLDataSource audioSrc = { &loc_bufq, &format_pcm };
+ SLDataLocator_OutputMix loc_outmix = { SL_DATALOCATOR_OUTPUTMIX, outputMixObject };
+ SLDataSink audioSnk = { &loc_outmix, NULL };
+ const SLInterfaceID ids[1] = { SL_IID_BUFFERQUEUE };
+ const SLboolean req[1] = { SL_BOOLEAN_TRUE };
+ result = (*engineEngine)->CreateAudioPlayer(
+ engineEngine, &playerObject, &audioSrc, &audioSnk, 1, ids, req);
+ if (SL_RESULT_SUCCESS != result) { stage = "CreateAudioPlayer"; goto handle_error; }
+
+ result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
+ if (SL_RESULT_SUCCESS != result) { stage = "realize Player object"; goto handle_error; }
+ }
+
+ result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
+ if (SL_RESULT_SUCCESS != result) { stage = "get Play interface"; goto handle_error; }
+
+ result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue);
+ if (SL_RESULT_SUCCESS != result) { stage = "get BufferQueue interface"; goto handle_error; }
+
+ result = (*playerBufferQueue)->RegisterCallback(playerBufferQueue, bqPlayerCallback, NULL);
+ if (SL_RESULT_SUCCESS != result) { stage = "register BufferQueue callback"; goto handle_error; }
+
+ result = (*playerBufferQueue)->Enqueue(playerBufferQueue, playBuffer, sizeof(playBuffer));
+ if (SL_RESULT_SUCCESS != result) {
+ stage = "enqueue into PlayerBufferQueue"; goto handle_error; }
+
+ result = (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
+ if (SL_RESULT_SUCCESS != result) {
+ stage = "SetPlayState(SL_PLAYSTATE_PLAYING)"; goto handle_error; }
+
+ snprintf(printBuffer, sizeof(printBuffer),
+ "Success, sample rate %d, buffer samples %d", sampleRate, playSamples);
+ return env->NewStringUTF(printBuffer);
+
+handle_error:
+ snprintf(printBuffer, sizeof(printBuffer), "Error at %s: %s", stage, getSLErrStr(result));
+ return env->NewStringUTF(printBuffer);
+}
+
+void Java_com_example_android_nativemididemo_NativeMidi_pauseAudio(
+ JNIEnv*, jobject) {
+ if (playerPlay != NULL) {
+ (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PAUSED);
+ }
+}
+
+void Java_com_example_android_nativemididemo_NativeMidi_resumeAudio(
+ JNIEnv*, jobject) {
+ if (playerBufferQueue != NULL && playerPlay != NULL) {
+ (*playerBufferQueue)->Enqueue(playerBufferQueue, playBuffer, sizeof(playBuffer));
+ (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
+ }
+}
+
+void Java_com_example_android_nativemididemo_NativeMidi_shutdownAudio(
+ JNIEnv*, jobject) {
+ if (playerObject != NULL) {
+ (*playerObject)->Destroy(playerObject);
+ playerObject = NULL;
+ playerPlay = NULL;
+ playerBufferQueue = NULL;
+ }
+
+ if (outputMixObject != NULL) {
+ (*outputMixObject)->Destroy(outputMixObject);
+ outputMixObject = NULL;
+ }
+
+ if (engineObject != NULL) {
+ (*engineObject)->Destroy(engineObject);
+ engineObject = NULL;
+ engineEngine = NULL;
+ }
+}
+
+jlong Java_com_example_android_nativemididemo_NativeMidi_getPlaybackCounter(JNIEnv*, jobject) {
+ return sharedCounter.load();
+}
+
+jobjectArray Java_com_example_android_nativemididemo_NativeMidi_getRecentMessages(
+ JNIEnv* env, jobject thiz) {
+ return nativemididemo::getRecentMessagesForJava(env, thiz);
+}
+
+void Java_com_example_android_nativemididemo_NativeMidi_startReadingMidi(
+ JNIEnv*, jobject, jint deviceId, jint portNumber) {
+ char buffer[1024];
+
+ int result = AMIDI_getDeviceById(deviceId, &midiDevice);
+ if (result == 0) {
+ snprintf(buffer, sizeof(buffer), "Obtained device token for uid %d: token %d", deviceId, midiDevice);
+ } else {
+ snprintf(buffer, sizeof(buffer), "Could not obtain device token for uid %d: %d", deviceId, result);
+ }
+ nativemididemo::writeMessage(buffer);
+ if (result) return;
+
+ AMIDI_DeviceInfo deviceInfo;
+ result = AMIDI_getDeviceInfo(midiDevice, &deviceInfo);
+ if (result == 0) {
+ snprintf(buffer, sizeof(buffer), "Device info: uid %d, type %d, priv %d, ports %d I / %d O",
+ deviceInfo.uid, deviceInfo.type, deviceInfo.isPrivate,
+ (int)deviceInfo.inputPortCount, (int)deviceInfo.outputPortCount);
+ } else {
+ snprintf(buffer, sizeof(buffer), "Could not obtain device info %d", result);
+ }
+ nativemididemo::writeMessage(buffer);
+ if (result) return;
+
+ AMIDI_OutputPort outputPort;
+ result = AMIDI_openOutputPort(midiDevice, portNumber, &outputPort);
+ if (result == 0) {
+ snprintf(buffer, sizeof(buffer), "Opened port %d: token %d", portNumber, outputPort);
+ midiOutputPort.store(outputPort);
+ } else {
+ snprintf(buffer, sizeof(buffer), "Could not open port %d: %d", deviceId, result);
+ }
+ nativemididemo::writeMessage(buffer);
+}
+
+void Java_com_example_android_nativemididemo_NativeMidi_stopReadingMidi(
+ JNIEnv*, jobject) {
+ AMIDI_OutputPort outputPort = midiOutputPort.exchange(AMIDI_INVALID_HANDLE);
+ if (outputPort == AMIDI_INVALID_HANDLE) return;
+ int result = AMIDI_closeOutputPort(outputPort);
+ char buffer[1024];
+ if (result == 0) {
+ snprintf(buffer, sizeof(buffer), "Closed port by token %d", outputPort);
+ } else {
+ snprintf(buffer, sizeof(buffer), "Could not close port by token %d: %d", outputPort, result);
+ }
+ nativemididemo::writeMessage(buffer);
+}
diff --git a/media/tests/NativeMidiDemo/res/layout/main.xml b/media/tests/NativeMidiDemo/res/layout/main.xml
new file mode 100644
index 000000000000..465d471a2de7
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/layout/main.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
+ <TextView
+ android:id="@+id/callback_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="13sp"
+ android:typeface="monospace"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="13sp"
+ android:typeface="monospace"
+ android:text=" | "
+ />
+ <TextView
+ android:id="@+id/java_midi_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="13sp"
+ android:typeface="monospace"
+ />
+ <TextView
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="13sp"
+ android:typeface="monospace"
+ android:text=" "
+ />
+ </LinearLayout>
+ <HorizontalScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="6sp"
+ android:paddingBottom="6sp"
+ >
+ <RadioGroup
+ android:id="@+id/devices"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:checkedButton="@+id/device_none"
+ >
+ <RadioButton
+ android:id="@+id/device_none"
+ android:text="None"
+ />
+ </RadioGroup>
+ </HorizontalScrollView>
+ <com.example.android.nativemididemo.TouchableScrollView android:id="@+id/messages_scroll"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"
+ >
+ <TextView android:id="@+id/messages"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="13sp"
+ android:typeface="monospace"
+ />
+ </com.example.android.nativemididemo.TouchableScrollView>
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
+ <Button android:id="@+id/clear_messages"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/clear_messages"
+ android:onClick="onClearMessages"
+ />
+ <Button android:id="@+id/close_port"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/close_port"
+ android:onClick="onClosePort"
+ />
+ </LinearLayout>
+</LinearLayout>
diff --git a/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000000..cde69bcccec6
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000000..c133a0cbd379
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000000..bfa42f0e7b91
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000000..324e72cdd748
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/media/tests/NativeMidiDemo/res/values/strings.xml b/media/tests/NativeMidiDemo/res/values/strings.xml
new file mode 100644
index 000000000000..5b69b52a5836
--- /dev/null
+++ b/media/tests/NativeMidiDemo/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">NativeMidiDemo</string>
+ <string name="clear_messages">Clear Messages</string>
+ <string name="close_port">Close Port</string>
+</resources>