summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--media/packages/BluetoothMidiService/Android.bp31
-rw-r--r--media/packages/BluetoothMidiService/AndroidManifest.xml23
-rw-r--r--media/packages/BluetoothMidiService/AndroidManifestBase.xml30
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/Android.bp38
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml32
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml32
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java47
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java257
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java247
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java244
-rw-r--r--media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java109
11 files changed, 1089 insertions, 1 deletions
diff --git a/media/packages/BluetoothMidiService/Android.bp b/media/packages/BluetoothMidiService/Android.bp
index f45114a277aa..77e6a14e1f00 100644
--- a/media/packages/BluetoothMidiService/Android.bp
+++ b/media/packages/BluetoothMidiService/Android.bp
@@ -1,6 +1,35 @@
+//
+// Copyright (C) 2019 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.
+//
+
+android_library {
+ name: "BluetoothMidiLib",
+ srcs: [
+ "src/**/*.java",
+ ],
+ platform_apis: true,
+ plugins: ["java_api_finder"],
+ manifest: "AndroidManifestBase.xml",
+}
+
android_app {
name: "BluetoothMidiService",
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ ],
platform_apis: true,
certificate: "platform",
+ manifest: "AndroidManifest.xml",
}
diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml
index 1cfd55d556a9..4042ce8b93df 100644
--- a/media/packages/BluetoothMidiService/AndroidManifest.xml
+++ b/media/packages/BluetoothMidiService/AndroidManifest.xml
@@ -1,12 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 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.
+ */
+-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.bluetoothmidiservice"
+ android:versionCode="1"
+ android:versionName="R-initial"
>
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-feature android:name="android.software.midi" android:required="true"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<application
+ tools:replace="android:label"
android:label="@string/app_name">
<service android:name=".BluetoothMidiService"
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
diff --git a/media/packages/BluetoothMidiService/AndroidManifestBase.xml b/media/packages/BluetoothMidiService/AndroidManifestBase.xml
new file mode 100644
index 000000000000..ebe62b039434
--- /dev/null
+++ b/media/packages/BluetoothMidiService/AndroidManifestBase.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2020 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.bluetoothmidiservice"
+ android:versionCode="1"
+ android:versionName="R-initial"
+ >
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+ <application
+ android:label="BluetoothMidi"
+ android:defaultToDeviceProtectedStorage="true"
+ android:directBootAware="true">
+ </application>
+</manifest>
diff --git a/media/packages/BluetoothMidiService/tests/unit/Android.bp b/media/packages/BluetoothMidiService/tests/unit/Android.bp
new file mode 100644
index 000000000000..4d4ae9e15532
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/Android.bp
@@ -0,0 +1,38 @@
+//
+// Copyright (C) 2019 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.
+//
+
+android_test {
+ name: "BluetoothMidiTests",
+ srcs: ["src/**/*.java"],
+ certificate: "platform",
+ static_libs: [
+ //"frameworks-base-testutils",
+ "android-support-test",
+ "androidx.test.core",
+ "androidx.test.ext.truth",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "platform-test-annotations",
+ "BluetoothMidiLib",
+ ],
+ test_suites: ["device-tests"],
+ libs: [
+ "framework-res",
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+}
diff --git a/media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml b/media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml
new file mode 100644
index 000000000000..4d27e1e7fe04
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.bluetoothmidiservice.tests.unit">
+ <uses-sdk
+ android:minSdkVersion="29"
+ android:targetSdkVersion="29" />
+
+ <application android:testOnly="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.bluetoothmidiservice.tests.unit"
+ android:label="Bluetooth MIDI Service tests">
+ </instrumentation>
+</manifest>
diff --git a/media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml b/media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml
new file mode 100644
index 000000000000..02e7f0d55349
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<configuration description="Runs Bluetooth MIDI Service Tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="BluetoothMidiTests.apk" />
+ </target_preparer>
+
+ <option name="test-tag" value="BLEMidiTests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.bluetoothmidiservice.tests.unit" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false" />
+ </test>
+</configuration>
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java
new file mode 100644
index 000000000000..57859bc114d8
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/AccumulatingMidiReceiver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 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.android.bluetoothmidiservice;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import com.android.internal.midi.MidiFramer;
+
+import java.util.ArrayList;
+
+class AccumulatingMidiReceiver extends MidiReceiver {
+ private static final String TAG = "AccumulatingMidiReceiver";
+ ArrayList<byte[]> mBuffers = new ArrayList<byte[]>();
+ ArrayList<Long> mTimestamps = new ArrayList<Long>();
+
+ public void onSend(byte[] buffer, int offset, int count, long timestamp) {
+ Log.d(TAG, "onSend() passed " + MidiFramer.formatMidiData(buffer, offset, count));
+ byte[] actualRow = new byte[count];
+ System.arraycopy(buffer, offset, actualRow, 0, count);
+ mBuffers.add(actualRow);
+ mTimestamps.add(timestamp);
+ }
+
+ byte[][] getBuffers() {
+ return mBuffers.toArray(new byte[mBuffers.size()][]);
+ }
+
+ Long[] getTimestamps() {
+ return mTimestamps.toArray(new Long[mTimestamps.size()]);
+ }
+}
+
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java
new file mode 100644
index 000000000000..3285f59e55c9
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2020 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.android.bluetoothmidiservice;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.midi.MidiFramer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * End to end testing of the Bluetooth Encoder and Decoder
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BluetoothMidiCodecTest {
+
+ private static final String TAG = "BluetoothMidiCodecTest";
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+ private static final long NANOS_PER_MSEC = 1000000L;
+
+ static class EncoderDecoderChecker implements PacketEncoder.PacketReceiver {
+ BluetoothPacketEncoder mEncoder;
+ BluetoothPacketDecoder mDecoder;
+ AccumulatingMidiReceiver mReceiver;
+ MidiFramer mFramer;
+ AccumulatingMidiReceiver mBypassReceiver;
+ MidiFramer mBypassFramer;
+ int mMaxPacketsPerConnection;
+ int mConnectionIntervalMillis;
+ BlockingQueue<byte[]> mPacketQueue;
+ ScheduledExecutorService mScheduler;
+
+ EncoderDecoderChecker() {
+ this(2, 15, 20);
+ }
+
+ EncoderDecoderChecker(
+ int maxPacketsPerConnection,
+ int connectionIntervalMillis,
+ int maxBytesPerPacket) {
+ mMaxPacketsPerConnection = maxPacketsPerConnection;
+ mConnectionIntervalMillis = connectionIntervalMillis;
+ mEncoder = new BluetoothPacketEncoder(this, maxBytesPerPacket);
+ mDecoder = new BluetoothPacketDecoder(maxBytesPerPacket);
+ mReceiver = new AccumulatingMidiReceiver();
+ mFramer = new MidiFramer(mReceiver);
+ mBypassReceiver = new AccumulatingMidiReceiver();
+ mBypassFramer = new MidiFramer(mBypassReceiver);
+ mScheduler = Executors.newSingleThreadScheduledExecutor();
+ mPacketQueue = new LinkedBlockingDeque<>(maxPacketsPerConnection);
+ }
+
+ void processQueue() throws InterruptedException {
+ for (int i = 0; i < mMaxPacketsPerConnection; i++) {
+ byte[] packet = mPacketQueue.poll(0, TimeUnit.SECONDS);
+ if (packet == null) break;
+ Log.d(TAG, "decode " + MidiFramer.formatMidiData(packet, 0, packet.length));
+ mDecoder.decodePacket(packet, mFramer);
+ }
+ Log.d(TAG, "call writeComplete()");
+ mEncoder.writeComplete();
+ }
+
+ public void start() {
+ mScheduler.scheduleAtFixedRate(
+ () -> {
+ Log.d(TAG, "run scheduled task");
+ try {
+ processQueue();
+ } catch (Exception e) {
+ assertEquals(null, e);
+ }
+ },
+ mConnectionIntervalMillis, // initial delay
+ mConnectionIntervalMillis, // period
+ TimeUnit.MILLISECONDS);
+ }
+
+ public void stop() {
+ // TODO wait for queue to empty
+ mScheduler.shutdown();
+ }
+
+ // TODO Should this block?
+ // Store the packets and then write them from a periodic task.
+ @Override
+ public void writePacket(byte[] buffer, int count) {
+ Log.d(TAG, "writePacket() passed " + MidiFramer.formatMidiData(buffer, 0, count));
+ byte[] packet = new byte[count];
+ System.arraycopy(buffer, 0, packet, 0, count);
+ try {
+ mPacketQueue.put(packet);
+ } catch (Exception e) {
+ assertEquals(null, e);
+ }
+ Log.d(TAG, "writePacket() returns");
+ }
+
+ void test(final byte[][] midi)
+ throws IOException, InterruptedException {
+ test(midi, 2);
+ }
+
+ // Send the MIDI messages through the encoder,
+ // then through the decoder,
+ // then gather the resulting MIDI and compare the results.
+ void test(final byte[][] midi, int intervalMillis)
+ throws IOException, InterruptedException {
+ start();
+ long timestamp = 0;
+ // Send all of the MIDI messages and gather the response.
+ for (int i = 0; i < midi.length; i++) {
+ byte[] outMessage = midi[i];
+ Log.d(TAG, "outMessage "
+ + MidiFramer.formatMidiData(outMessage, 0, outMessage.length));
+ mEncoder.send(outMessage, 0, outMessage.length, timestamp);
+ timestamp += 2 * NANOS_PER_MSEC;
+ // Also send a copy through a MidiFramer to align the messages.
+ mBypassFramer.send(outMessage, 0, outMessage.length, timestamp);
+ }
+ Thread.sleep(200);
+ stop();
+
+ // Compare the gathered rows with the expected rows.
+ byte[][] expectedMessages = mBypassReceiver.getBuffers();
+ byte[][] inMessages = mReceiver.getBuffers();
+ Log.d(TAG, "expectedMessage length = " + expectedMessages.length
+ + ", inMessages length = " + inMessages.length);
+ assertEquals(expectedMessages.length, inMessages.length);
+ Long[] actualTimestamps = mReceiver.getTimestamps();
+ long previousTime = 0;
+ for (int i = 0; i < expectedMessages.length; i++) {
+ byte[] expectedMessage = expectedMessages[i];
+ Log.d(TAG, "expectedMessage = "
+ + MidiFramer.formatMidiData(expectedMessage,
+ 0, expectedMessage.length));
+ byte[] actualMessage = inMessages[i];
+ Log.d(TAG, "actualMessage = "
+ + MidiFramer.formatMidiData(actualMessage, 0, actualMessage.length));
+ assertArrayEquals(expectedMessage, actualMessage);
+ // Are the timestamps monotonic?
+ long currentTime = actualTimestamps[i];
+ Log.d(TAG, "previousTime = " + previousTime
+ + ", currentTime = " + currentTime);
+ assertTrue(currentTime >= previousTime);
+ previousTime = currentTime;
+ }
+ }
+ }
+
+ @Test
+ public void testOneNoteOn() throws IOException, InterruptedException {
+ final byte[][] midi = {
+ {(byte) 0x90, 0x40, 0x64}
+ };
+ EncoderDecoderChecker checker = new EncoderDecoderChecker();
+ checker.test(midi);
+ }
+
+ @Test
+ public void testTwoNoteOnSameTime() throws IOException, InterruptedException {
+ final byte[][] midi = {
+ {(byte) 0x90, 0x40, 0x64, (byte) 0x90, 0x47, 0x70}
+ };
+ EncoderDecoderChecker checker = new EncoderDecoderChecker();
+ checker.test(midi);
+ }
+
+ @Test
+ public void testTwoNoteOnStaggered() throws IOException, InterruptedException {
+ final byte[][] midi = {
+ {(byte) 0x90, 0x40, 0x64},
+ {(byte) 0x90, 0x47, 0x70}
+ };
+ EncoderDecoderChecker checker = new EncoderDecoderChecker();
+ checker.test(midi);
+ }
+
+ public void checkNoteBurst(int maxPacketsPerConnection,
+ int period,
+ int maxBytesPerPacket) throws IOException, InterruptedException {
+ final int numNotes = 100;
+ final byte[][] midi = new byte[numNotes][];
+ int channel = 2;
+ for (int i = 0; i < numNotes; i++) {
+ byte[] message = {(byte) (0x90 + channel), (byte) (i + 1), 0x64};
+ midi[i] = message;
+ channel ^= 1;
+ }
+ EncoderDecoderChecker checker = new EncoderDecoderChecker(
+ maxPacketsPerConnection, 15, maxBytesPerPacket);
+ checker.test(midi, period);
+ }
+
+ @Test
+ public void testNoteBurstM1P6() throws IOException, InterruptedException {
+ checkNoteBurst(1, 6, 20);
+ }
+ @Test
+ public void testNoteBurstM1P2() throws IOException, InterruptedException {
+ checkNoteBurst(1, 2, 20);
+ }
+ @Test
+ public void testNoteBurstM2P6() throws IOException, InterruptedException {
+ checkNoteBurst(2, 6, 20);
+ }
+ @Test
+ public void testNoteBurstM2P2() throws IOException, InterruptedException {
+ checkNoteBurst(2, 2, 20);
+ }
+ @Test
+ public void testNoteBurstM2P0() throws IOException, InterruptedException {
+ checkNoteBurst(2, 0, 20);
+ }
+ @Test
+ public void testNoteBurstM2P6B21() throws IOException, InterruptedException {
+ checkNoteBurst(2, 6, 21);
+ }
+ @Test
+ public void testNoteBurstM2P2B21() throws IOException, InterruptedException {
+ checkNoteBurst(2, 2, 21);
+ }
+ @Test
+ public void testNoteBurstM2P0B21() throws IOException, InterruptedException {
+ checkNoteBurst(2, 0, 21);
+ }
+}
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java
new file mode 100644
index 000000000000..6ecc53957eaa
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiDecoderTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2020 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.android.bluetoothmidiservice;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.midi.MidiFramer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BluetoothMidiDecoderTest {
+
+ private static final String TAG = "BluetoothMidiDecoderTest";
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+ private static final long NANOS_PER_MSEC = 1000000L;
+
+ static class DecoderChecker {
+ AccumulatingMidiReceiver mReceiver;
+ BluetoothPacketDecoder mDecoder;
+
+ DecoderChecker() {
+ mReceiver = new AccumulatingMidiReceiver();
+ final int maxBytes = 20;
+ mDecoder = new BluetoothPacketDecoder(maxBytes);
+ }
+
+ void compareWithExpected(final byte[][] expectedMessages) {
+ byte[][] actualRows = mReceiver.getBuffers();
+ Long[] actualTimestamps = mReceiver.getTimestamps();
+ long previousTime = 0;
+ // Compare the gathered with the expected.
+ assertEquals(expectedMessages.length, actualRows.length);
+ for (int i = 0; i < expectedMessages.length; i++) {
+ byte[] expectedRow = expectedMessages[i];
+ Log.d(TAG, "expectedRow = "
+ + MidiFramer.formatMidiData(expectedRow, 0, expectedRow.length));
+ byte[] actualRow = actualRows[i];
+ Log.d(TAG, "actualRow = "
+ + MidiFramer.formatMidiData(actualRow, 0, actualRow.length));
+ assertArrayEquals(expectedRow, actualRow);
+ // Are the timestamps monotonic?
+ long currentTime = actualTimestamps[i];
+ Log.d(TAG, "previousTime = " + previousTime + ", currentTime = " + currentTime);
+ assertTrue(currentTime >= previousTime);
+ previousTime = currentTime;
+ }
+ }
+
+ void decodePacket(byte[] packet) throws IOException {
+ mDecoder.decodePacket(packet, mReceiver);
+ }
+
+ void decodePackets(byte[][] multiplePackets) throws IOException {
+ try {
+ for (int i = 0; i < multiplePackets.length; i++) {
+ byte[] packet = multiplePackets[i];
+ mDecoder.decodePacket(packet, mReceiver);
+ }
+ } catch (Exception e) {
+ assertEquals(null, e);
+ }
+ }
+
+ void test(byte[] encoded, byte[][] decoded) throws IOException {
+ decodePacket(encoded);
+ compareWithExpected(decoded);
+ }
+
+ void test(byte[][] encoded, byte[][] decoded) throws IOException {
+ decodePackets(encoded);
+ compareWithExpected(decoded);
+ }
+ }
+
+ @Test
+ public void testOneNoteOn() throws IOException {
+ final byte[] encoded = {
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x90, 0x40, 0x64
+ };
+ final byte[][] decoded = {
+ {(byte) 0x90, 0x40, 0x64}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testReservedHeaderBit() throws IOException {
+ final byte[] encoded = {
+ // Decoder should ignore the reserved bit.
+ (byte) (0x80 | 0x40), // set RESERVED bit in header!
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x90, 0x40, 0x64
+ };
+ final byte[][] decoded = {
+ {(byte) 0x90, 0x40, 0x64}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testTwoNotesOnRunning() throws IOException {
+ final byte[] encoded = {
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x90, 0x40, 0x64,
+ (byte) 0x85, // timestamp
+ (byte) 0x42, 0x70
+ };
+ final byte[][] decoded = {
+ {(byte) 0x90, 0x40, 0x64},
+ {(byte) 0x42, 0x70}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testTwoNoteOnsTwoChannels() throws IOException {
+ final byte[] encoded = {
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x93, 0x40, 0x60,
+ // two channels so no running status
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x95, 0x47, 0x64
+ };
+ final byte[][] decoded = {
+ {(byte) 0x93, 0x40, 0x60, (byte) 0x95, 0x47, 0x64}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testTwoNoteOnsOverTime() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x98, 0x45, 0x60
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x82, // timestamp advanced by 2 msec
+ (byte) 0x90, 0x40, 0x64,
+ (byte) 0x84, // timestamp needed because of time delay
+ // encoder uses running status
+ 0x47, 0x72
+ }};
+ final byte[][] decoded = {
+ {(byte) 0x98, 0x45, 0x60},
+ {(byte) 0x90, 0x40, 0x64},
+ {(byte) 0x47, 0x72}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testSysExBasic() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // timestamp
+ (byte) 0xF0, 0x7D, // Begin prototyping SysEx
+ 0x01, 0x02, 0x03, 0x04, 0x05,
+ (byte) 0x80, // timestamp
+ (byte) 0xF7 // End SysEx
+ }};
+ final byte[][] decoded = {
+ {(byte) 0xF0, 0x7D, // experimental SysEx
+ 0x01, 0x02, 0x03, 0x04, 0x05, (byte) 0xF7}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testSysExTwoPackets() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // timestamp
+ (byte) 0xF0, 0x7D, // Begin prototyping SysEx
+ 0x01, 0x02
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ 0x03, 0x04, 0x05,
+ (byte) 0x80, // timestamp
+ (byte) 0xF7 // End SysEx
+ }};
+ final byte[][] decoded = {
+ {(byte) 0xF0, 0x7D, 0x01, 0x02}, // experimental SysEx
+ {0x03, 0x04, 0x05, (byte) 0xF7}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+ @Test
+ public void testSysExThreePackets() throws IOException {
+ final byte[][] encoded = {
+ {(byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // timestamp
+ (byte) 0xF0, 0x7D, // Begin prototyping SysEx
+ 0x01, 0x02
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ 0x03, 0x04, 0x05,
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ 0x06, 0x07, 0x08,
+ (byte) 0x80, // timestamp
+ (byte) 0xF7 // End SysEx
+ }};
+ final byte[][] decoded = {
+ {(byte) 0xF0, 0x7D, 0x01, 0x02}, // experimental SysEx
+ {0x03, 0x04, 0x05},
+ {0x06, 0x07, 0x08, (byte) 0xF7}
+ };
+ new DecoderChecker().test(encoded, decoded);
+ }
+
+}
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java
new file mode 100644
index 000000000000..a169c0d7c7f9
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2020 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.android.bluetoothmidiservice;
+
+import static org.junit.Assert.assertEquals;
+
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.midi.MidiFramer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BluetoothMidiEncoderTest {
+
+ private static final String TAG = "BluetoothMidiEncoderTest";
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+ private static final long NANOS_PER_MSEC = 1000000L;
+
+ static class AccumulatingPacketReceiver implements PacketEncoder.PacketReceiver {
+ ArrayList<byte[]> mBuffers = new ArrayList<byte[]>();
+
+ public void writePacket(byte[] buffer, int count) {
+ byte[] actualRow = new byte[count];
+ Log.d(TAG, "writePacket() passed " + MidiFramer.formatMidiData(buffer, 0, count));
+ System.arraycopy(buffer, 0, actualRow, 0, count);
+ mBuffers.add(actualRow);
+ }
+
+ byte[][] getBuffers() {
+ return mBuffers.toArray(new byte[mBuffers.size()][]);
+ }
+ }
+
+ static class EncoderChecker {
+ AccumulatingPacketReceiver mReceiver;
+ BluetoothPacketEncoder mEncoder;
+
+ EncoderChecker() {
+ mReceiver = new AccumulatingPacketReceiver();
+ final int maxBytes = 20;
+ mEncoder = new BluetoothPacketEncoder(mReceiver, maxBytes);
+ }
+
+ void send(byte[] data) throws IOException {
+ send(data, 0);
+ }
+
+ void send(byte[] data, long timestamp) throws IOException {
+ Log.d(TAG, "send " + MidiFramer.formatMidiData(data, 0, data.length));
+ mEncoder.send(data, 0, data.length, timestamp);
+ }
+
+ void compareWithExpected(final byte[][] expected) {
+ byte[][] actualRows = mReceiver.getBuffers();
+ assertEquals(expected.length, actualRows.length);
+ // Compare the gathered rows with the expected rows.
+ for (int i = 0; i < expected.length; i++) {
+ byte[] expectedRow = expected[i];
+ Log.d(TAG, "expectedRow = "
+ + MidiFramer.formatMidiData(expectedRow, 0, expectedRow.length));
+ byte[] actualRow = actualRows[i];
+ Log.d(TAG, "actualRow = "
+ + MidiFramer.formatMidiData(actualRow, 0, actualRow.length));
+ assertEquals(expectedRow.length, actualRow.length);
+ for (int k = 0; k < expectedRow.length; k++) {
+ assertEquals(expectedRow[k], actualRow[k]);
+ }
+ }
+ }
+
+ void writeComplete() {
+ mEncoder.writeComplete();
+ }
+
+ }
+
+ @Test
+ public void testOneNoteOn() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x90, 0x40, 0x64
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ checker.send(new byte[] {(byte) 0x90, 0x40, 0x64});
+ checker.compareWithExpected(encoded);
+ }
+
+ @Test
+ public void testTwoNoteOnsSameChannel() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x90, 0x40, 0x64,
+ // encoder converts to running status
+ 0x47, 0x72
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ checker.send(new byte[] {(byte) 0x90, 0x40, 0x64, (byte) 0x90, 0x47, 0x72});
+ checker.compareWithExpected(encoded);
+ }
+
+ @Test
+ public void testTwoNoteOnsTwoChannels() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x93, 0x40, 0x60,
+ // two channels so no running status
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x95, 0x47, 0x64
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ checker.send(new byte[] {(byte) 0x93, 0x40, 0x60, (byte) 0x95, 0x47, 0x64});
+ checker.compareWithExpected(encoded);
+ }
+
+ @Test
+ public void testTwoNoteOnsOverTime() throws IOException {
+ final byte[][] encoded = {
+ {
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // high bit of timestamp
+ (byte) 0x98, 0x45, 0x60
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x82, // timestamp advanced by 2 msec
+ (byte) 0x90, 0x40, 0x64,
+ (byte) 0x84, // timestamp needed because of time delay
+ // encoder converts to running status
+ 0x47, 0x72
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ long timestamp = 0;
+ // Send one note. This will cause an immediate packet write
+ // because we don't know when the next one will arrive.
+ checker.send(new byte[] {(byte) 0x98, 0x45, 0x60}, timestamp);
+
+ // Send two notes. These should accumulate into the
+ // same packet because we do not yet have a writeComplete.
+ timestamp += 2 * NANOS_PER_MSEC;
+ checker.send(new byte[] {(byte) 0x90, 0x40, 0x64}, timestamp);
+ timestamp += 2 * NANOS_PER_MSEC;
+ checker.send(new byte[] {(byte) 0x90, 0x47, 0x72}, timestamp);
+ // Tell the encoder that the first packet has been written to the
+ // hardware. So it can flush the two pending notes.
+ checker.writeComplete();
+ checker.compareWithExpected(encoded);
+ }
+
+ @Test
+ public void testSysExBasic() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // timestamp
+ (byte) 0xF0, 0x7D, // Begin prototyping SysEx
+ 0x01, 0x02, 0x03, 0x04, 0x05,
+ (byte) 0x80, // timestamp
+ (byte) 0xF7 // End SysEx
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ checker.send(new byte[] {(byte) 0xF0, 0x7D, // experimental SysEx
+ 0x01, 0x02, 0x03, 0x04, 0x05, (byte) 0xF7});
+ checker.compareWithExpected(encoded);
+ }
+
+ @Test
+ public void testSysExTwoPackets() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // timestamp
+ (byte) 0xF0, 0x7D, // Begin prototyping SysEx
+ 0x01, 0x02
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ 0x03, 0x04, 0x05,
+ (byte) 0x80, // timestamp
+ (byte) 0xF7 // End SysEx
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ // Send in two messages.
+ checker.send(new byte[] {(byte) 0xF0, 0x7D, // experimental SysEx
+ 0x01, 0x02});
+ checker.send(new byte[] {0x03, 0x04, 0x05, (byte) 0xF7});
+ // Tell the encoder that the first packet has been written to the
+ // hardware. So it can flush the remaining data.
+ checker.writeComplete();
+ checker.compareWithExpected(encoded);
+ }
+
+ @Test
+ public void testSysExThreePackets() throws IOException {
+ final byte[][] encoded = {{
+ (byte) 0x80, // high bit of header must be set
+ (byte) 0x80, // timestamp
+ (byte) 0xF0, 0x7D, // Begin prototyping SysEx
+ 0x01, 0x02
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ 0x03, 0x04, 0x05,
+ },
+ {
+ (byte) 0x80, // high bit of header must be set
+ 0x06, 0x07, 0x08,
+ (byte) 0x80, // timestamp
+ (byte) 0xF7 // End SysEx
+ }};
+ EncoderChecker checker = new EncoderChecker();
+ // Send in three messages.
+ checker.send(new byte[] {(byte) 0xF0, 0x7D, // experimental SysEx
+ 0x01, 0x02});
+ checker.send(new byte[] {0x03, 0x04, 0x05});
+ checker.writeComplete();
+ checker.send(new byte[] {0x06, 0x07, 0x08, (byte) 0xF7});
+ checker.writeComplete();
+ checker.compareWithExpected(encoded);
+ }
+}
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java
new file mode 100644
index 000000000000..8cda6eb3a3ea
--- /dev/null
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/MidiFramerTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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.android.bluetoothmidiservice;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.midi.MidiFramer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MidiFramerTest {
+
+ private static final String TAG = "MidiFramerTest";
+ private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+
+ // For testing MidiFramer
+ // TODO move MidiFramer tests to their own file
+ static class FramerChecker {
+ AccumulatingMidiReceiver mReceiver;
+ MidiFramer mFramer;
+
+ FramerChecker() {
+ mReceiver = new AccumulatingMidiReceiver();
+ mFramer = new MidiFramer(mReceiver);
+ }
+
+ void compareWithExpected(final byte[][] expected) {
+ byte[][] actualRows = mReceiver.getBuffers();
+ assertEquals(expected.length, actualRows.length);
+ // Compare the gathered rows with the expected rows.
+ for (int i = 0; i < expected.length; i++) {
+ byte[] expectedRow = expected[i];
+ Log.d(TAG, "expectedRow = "
+ + MidiFramer.formatMidiData(expectedRow, 0, expectedRow.length));
+ byte[] actualRow = actualRows[i];
+ Log.d(TAG, "actualRow = "
+ + MidiFramer.formatMidiData(actualRow, 0, actualRow.length));
+ assertArrayEquals(expectedRow, actualRow);
+ }
+ }
+
+ void send(byte[] data) throws IOException {
+ Log.d(TAG, "send " + MidiFramer.formatMidiData(data, 0, data.length));
+ mFramer.send(data, 0, data.length, 0);
+ }
+ }
+
+ @Test
+ public void testFramerTwoNoteOns() throws IOException {
+ final byte[][] expected = {
+ {(byte) 0x90, 0x40, 0x64},
+ {(byte) 0x90, 0x47, 0x50}
+ };
+ FramerChecker checker = new FramerChecker();
+ checker.send(new byte[] {(byte) 0x90, 0x40, 0x64, (byte) 0x90, 0x47, 0x50});
+ checker.compareWithExpected(expected);
+ }
+
+ @Test
+ public void testFramerTwoNoteOnsRunning() throws IOException {
+ final byte[][] expected = {
+ {(byte) 0x90, 0x40, 0x64},
+ {(byte) 0x90, 0x47, 0x70}
+ };
+ FramerChecker checker = new FramerChecker();
+ // Two notes with running status
+ checker.send(new byte[] {(byte) 0x90, 0x40, 0x64, 0x47, 0x70});
+ checker.compareWithExpected(expected);
+ }
+
+ @Test
+ public void testFramerPreGarbage() throws IOException {
+ final byte[][] expected = {
+ {(byte) 0x90, 0x40, 0x64},
+ {(byte) 0x90, 0x47, 0x70}
+ };
+ FramerChecker checker = new FramerChecker();
+ // Garbage can come before the first status byte if you connect
+ // a MIDI cable in the middle of a message.
+ checker.send(new byte[] {0x01, 0x02, // garbage bytes
+ (byte) 0x90, 0x40, 0x64, 0x47, 0x70});
+ checker.compareWithExpected(expected);
+ }
+}