Move USB device to SynthesizerService

This patch moves the connection to the USB MIDI keyboard from a single
activity to the SynthesizerService so that it can run while the user
switches between more than one activity.

Note that in the current state, there are problems when the device is
detached when the service isn't running.
master
Raph Levien 11 years ago
parent 9f1b753f81
commit a762514187
  1. 85
      android/src/com/levien/synthesizer/android/service/SynthesizerService.java
  2. 96
      android/src/com/levien/synthesizer/android/ui/PianoActivity2.java
  3. 30
      android/src/com/levien/synthesizer/android/usb/UsbMidiDevice.java

@ -23,8 +23,14 @@ import java.util.List;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Binder; import android.os.Binder;
import android.os.Build; import android.os.Build;
@ -33,6 +39,7 @@ import android.util.Log;
import com.levien.synthesizer.R; import com.levien.synthesizer.R;
import com.levien.synthesizer.android.AndroidGlue; import com.levien.synthesizer.android.AndroidGlue;
import com.levien.synthesizer.android.usb.UsbMidiDevice;
import com.levien.synthesizer.core.midi.MidiListener; import com.levien.synthesizer.core.midi.MidiListener;
import com.levien.synthesizer.core.model.composite.MultiChannelSynthesizer; import com.levien.synthesizer.core.model.composite.MultiChannelSynthesizer;
@ -91,6 +98,13 @@ public class SynthesizerService extends Service {
} }
} }
androidGlue_.setPlayState(true); androidGlue_.setPlayState(true);
if (usbDevice_ != null && usbMidiConnection_ == null) {
connectUsbMidi(usbDevice_);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(usbReceiver_, filter);
}
} }
/** /**
@ -99,6 +113,10 @@ public class SynthesizerService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
androidGlue_.setPlayState(false); androidGlue_.setPlayState(false);
setMidiInterface(null, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
unregisterReceiver(usbReceiver_);
}
} }
/** /**
@ -150,10 +168,77 @@ public class SynthesizerService extends Service {
params.bufferSize = Integer.parseInt(bs); params.bufferSize = Integer.parseInt(bs);
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private boolean setMidiInterface(UsbDevice device, UsbInterface intf) {
if (usbMidiConnection_ != null) {
if (usbMidiDevice_ != null) {
usbMidiDevice_.stop();
usbMidiDevice_ = null;
}
if (usbMidiInterface_ != null) {
// Note: releasing the interface seems to trigger bugs, so
// based on experimentation just closing seems more robust
//usbMidiConnection_.releaseInterface(usbMidiInterface_);
}
usbMidiConnection_.close();
usbMidiConnection_ = null;
}
if (device != null && intf != null) {
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection != null) {
if (connection.claimInterface(intf, true)) {
usbDevice_ = device;
usbMidiConnection_ = connection;
usbMidiInterface_ = intf;
usbMidiDevice_ = new UsbMidiDevice(androidGlue_, usbMidiConnection_, intf);
usbMidiDevice_.start();
return true;
} else {
Log.e("synth", "failed to claim USB interface");
connection.close();
}
} else {
Log.e("synth", "open device failed");
}
}
return false;
}
// Handles both connect and disconnect actions
public boolean connectUsbMidi(UsbDevice device) {
UsbInterface intf = device != null ? UsbMidiDevice.findMidiInterface(device) : null;
Log.d("synth", "connecting USB");
boolean success = setMidiInterface(device, intf);
Log.d("synth", "connecting USB done");
usbDevice_ = success ? device : null;
return success;
}
private final BroadcastReceiver usbReceiver_ = new BroadcastReceiver() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public void onReceive(Context context, Intent intent) {
Log.d("synth", "service broadcast receiver got " + intent);
String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device.equals(usbDevice_)) {
connectUsbMidi(null);
}
}
}
};
// Binder to use for Activities in this process. // Binder to use for Activities in this process.
private final IBinder binder_ = new LocalBinder(); private final IBinder binder_ = new LocalBinder();
private static AndroidGlue androidGlue_; private static AndroidGlue androidGlue_;
private static List<String> patchNames_; private static List<String> patchNames_;
// State for USB MIDI keyboard connection
private static UsbDevice usbDevice_;
private UsbDeviceConnection usbMidiConnection_;
private UsbMidiDevice usbMidiDevice_;
private UsbInterface usbMidiInterface_;
} }

@ -117,16 +117,12 @@ public class PianoActivity2 extends Activity {
@Override @Override
protected void onPause() { protected void onPause() {
Log.d("synth", "activity onPause"); Log.d("synth", "activity onPause");
setMidiInterface(null, null);
super.onPause(); super.onPause();
} }
@Override @Override
protected void onResume() { protected void onResume() {
Log.d("synth", "activity onResume " + getIntent()); Log.d("synth", "activity onResume " + getIntent());
if (usbDevice_ != null && usbMidiConnection_ == null) {
connectUsbMidi(usbDevice_);
}
super.onResume(); super.onResume();
} }
@ -149,62 +145,6 @@ public class PianoActivity2 extends Activity {
connectUsbFromIntent(intent); connectUsbFromIntent(intent);
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
static private UsbInterface findMidiInterface(UsbDevice device) {
int count = device.getInterfaceCount();
for (int i = 0; i < count; i++) {
UsbInterface usbIf = device.getInterface(i);
if (usbIf.getInterfaceClass() == 1 && usbIf.getInterfaceSubclass() == 3) {
return usbIf;
}
}
return null;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private boolean setMidiInterface(UsbDevice device, UsbInterface intf) {
if (usbMidiConnection_ != null) {
if (usbMidiDevice_ != null) {
usbMidiDevice_.stop();
usbMidiDevice_ = null;
}
if (usbMidiInterface_ != null) {
// Note: releasing the interface seems to trigger bugs, so
// based on experimentation just closing seems more robust
//usbMidiConnection_.releaseInterface(usbMidiInterface_);
}
usbMidiConnection_.close();
usbMidiConnection_ = null;
}
if (device != null && intf != null) {
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection != null) {
if (connection.claimInterface(intf, true)) {
usbDevice_ = device;
usbMidiConnection_ = connection;
usbMidiInterface_ = intf;
usbMidiDevice_ = new UsbMidiDevice(this, usbMidiConnection_, intf);
usbMidiDevice_.start();
return true;
} else {
Log.e("synth", "failed to claim USB interface");
connection.close();
}
} else {
Log.e("synth", "open device failed");
}
}
return false;
}
private boolean connectUsbMidi(UsbDevice device) {
UsbInterface intf = device != null ? findMidiInterface(device) : null;
boolean success = setMidiInterface(device, intf);
usbDevice_ = success ? device : null;
return success;
}
// scan for MIDI devices on the USB bus // scan for MIDI devices on the USB bus
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private void scanUsbMidi() { private void scanUsbMidi() {
@ -212,11 +152,10 @@ public class PianoActivity2 extends Activity {
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
Log.i("synth", "USB device count=" + deviceList.size()); Log.i("synth", "USB device count=" + deviceList.size());
for (UsbDevice device : deviceList.values()) { for (UsbDevice device : deviceList.values()) {
UsbInterface intf = findMidiInterface(device); UsbInterface intf = UsbMidiDevice.findMidiInterface(device);
if (intf != null) { if (intf != null) {
if (usbManager.hasPermission(device)) { if (usbManager.hasPermission(device)) {
if (setMidiInterface(device, intf)) { if (connectUsbMidi(device)) {
usbDevice_ = device;
break; break;
} }
} else { } else {
@ -232,6 +171,14 @@ public class PianoActivity2 extends Activity {
} }
} }
boolean connectUsbMidi(UsbDevice device) {
if (synthesizerService_ != null) {
return synthesizerService_.connectUsbMidi(device);
}
usbDevicePending_ = device;
return true;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
boolean connectUsbFromIntent(Intent intent) { boolean connectUsbFromIntent(Intent intent) {
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) { if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
@ -247,9 +194,6 @@ public class PianoActivity2 extends Activity {
permissionIntent_ = PendingIntent.getBroadcast(this, 0, new Intent( permissionIntent_ = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0); ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
// Attach intent is handled in manifest, don't want to get it twice.
//filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(ACTION_USB_PERMISSION); filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver_, filter); registerReceiver(usbReceiver_, filter);
if (!connectUsbFromIntent(intent)) { if (!connectUsbFromIntent(intent)) {
@ -262,16 +206,7 @@ public class PianoActivity2 extends Activity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String action = intent.getAction(); String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { if (ACTION_USB_PERMISSION.equals(action)) {
Log.i("synth", "broadcast receiver: attach");
connectUsbFromIntent(intent);
} else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
Log.i("synth", "broadcast receiver: detach");
UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (usbDevice_ != null && usbDevice_.equals(device)) {
connectUsbMidi(null);
}
} else if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) { synchronized (this) {
UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
@ -300,6 +235,10 @@ public class PianoActivity2 extends Activity {
PianoActivity2.this, android.R.layout.simple_spinner_item, patchNames); PianoActivity2.this, android.R.layout.simple_spinner_item, patchNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
presetSpinner_.setAdapter(adapter); presetSpinner_.setAdapter(adapter);
if (usbDevicePending_ != null) {
synthesizerService_.connectUsbMidi(usbDevicePending_);
usbDevicePending_ = null;
}
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
synthesizerService_ = null; synthesizerService_ = null;
@ -313,10 +252,7 @@ public class PianoActivity2 extends Activity {
private KnobView resonanceKnob_; private KnobView resonanceKnob_;
private KnobView overdriveKnob_; private KnobView overdriveKnob_;
private Spinner presetSpinner_; private Spinner presetSpinner_;
private UsbDevice usbDevice_;
private UsbDeviceConnection usbMidiConnection_;
private UsbMidiDevice usbMidiDevice_;
private UsbInterface usbMidiInterface_;
private PendingIntent permissionIntent_; private PendingIntent permissionIntent_;
private boolean permissionRequestPending_; private boolean permissionRequestPending_;
private UsbDevice usbDevicePending_;
} }

@ -1,12 +1,12 @@
/* /*
* Copyright 2013 Google Inc. * Copyright 2013 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,24 +20,27 @@ package com.levien.synthesizer.android.usb;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbInterface;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import com.levien.synthesizer.android.AndroidGlue;
import com.levien.synthesizer.android.ui.PianoActivity2; import com.levien.synthesizer.android.ui.PianoActivity2;
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public class UsbMidiDevice { public class UsbMidiDevice {
private final PianoActivity2 mActivity; // probably want to change this to MessageOutputProcessor
private final AndroidGlue mReceiver;
private final UsbDeviceConnection mDeviceConnection; private final UsbDeviceConnection mDeviceConnection;
private final UsbEndpoint mEndpoint; private final UsbEndpoint mEndpoint;
private final WaiterThread mWaiterThread = new WaiterThread(); private final WaiterThread mWaiterThread = new WaiterThread();
public UsbMidiDevice(PianoActivity2 activity, UsbDeviceConnection connection, UsbInterface intf) { public UsbMidiDevice(AndroidGlue receiver, UsbDeviceConnection connection, UsbInterface intf) {
mActivity = activity; mReceiver = receiver;
mDeviceConnection = connection; mDeviceConnection = connection;
mEndpoint = getInputEndpoint(intf); mEndpoint = getInputEndpoint(intf);
@ -67,6 +70,19 @@ public class UsbMidiDevice {
} }
} }
// A helper function for clients that might want to query whether a
// device supports MIDI
public static UsbInterface findMidiInterface(UsbDevice device) {
int count = device.getInterfaceCount();
for (int i = 0; i < count; i++) {
UsbInterface usbIf = device.getInterface(i);
if (usbIf.getInterfaceClass() == 1 && usbIf.getInterfaceSubclass() == 3) {
return usbIf;
}
}
return null;
}
private class WaiterThread extends Thread { private class WaiterThread extends Thread {
public boolean mStop; public boolean mStop;
@ -98,7 +114,7 @@ public class UsbMidiDevice {
byte[] newBuf = new byte[payloadBytes]; byte[] newBuf = new byte[payloadBytes];
System.arraycopy(buf, i + 1, newBuf, 0, payloadBytes); System.arraycopy(buf, i + 1, newBuf, 0, payloadBytes);
Log.d("synth", "sending midi"); Log.d("synth", "sending midi");
mActivity.sendMidiBytes(newBuf); mReceiver.sendMidi(newBuf);
} }
} }
} }

Loading…
Cancel
Save