/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ package com.juce.jucedemo; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Looper; import android.os.Handler; import android.os.ParcelUuid; import android.os.Environment; import android.view.*; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.graphics.*; import android.text.ClipboardManager; import android.text.InputType; import android.util.DisplayMetrics; import android.util.Log; import java.lang.Runnable; import java.util.*; import java.io.*; import java.net.URL; import java.net.HttpURLConnection; import android.media.AudioManager; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.support.v4.content.ContextCompat; import android.support.v4.app.ActivityCompat; import android.Manifest; import android.media.midi.*; import android.bluetooth.*; import android.bluetooth.le.*; //============================================================================== public class JuceDemo extends Activity { //============================================================================== static { System.loadLibrary ("juce_jni"); } //============================================================================== public boolean isPermissionDeclaredInManifest (int permissionID) { String permissionToCheck = getAndroidPermissionName(permissionID); try { PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); if (info.requestedPermissions != null) for (String permission : info.requestedPermissions) if (permission.equals (permissionToCheck)) return true; } catch (PackageManager.NameNotFoundException e) { Log.d ("JUCE", "isPermissionDeclaredInManifest: PackageManager.NameNotFoundException = " + e.toString()); } Log.d ("JUCE", "isPermissionDeclaredInManifest: could not find requested permission " + permissionToCheck); return false; } //============================================================================== // these have to match the values of enum PermissionID in C++ class RuntimePermissions: private static final int JUCE_PERMISSIONS_RECORD_AUDIO = 1; private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; private static String getAndroidPermissionName (int permissionID) { switch (permissionID) { case JUCE_PERMISSIONS_RECORD_AUDIO: return Manifest.permission.RECORD_AUDIO; case JUCE_PERMISSIONS_BLUETOOTH_MIDI: return Manifest.permission.ACCESS_COARSE_LOCATION; } // unknown permission ID! assert false; return new String(); } public boolean isPermissionGranted (int permissionID) { return ContextCompat.checkSelfPermission (this, getAndroidPermissionName (permissionID)) == PackageManager.PERMISSION_GRANTED; } private Map permissionCallbackPtrMap; public void requestRuntimePermission (int permissionID, long ptrToCallback) { String permissionName = getAndroidPermissionName (permissionID); if (ContextCompat.checkSelfPermission (this, permissionName) != PackageManager.PERMISSION_GRANTED) { // remember callbackPtr, request permissions, and let onRequestPermissionResult call callback asynchronously permissionCallbackPtrMap.put (permissionID, ptrToCallback); ActivityCompat.requestPermissions (this, new String[]{permissionName}, permissionID); } else { // permissions were already granted before, we can call callback directly androidRuntimePermissionsCallback (true, ptrToCallback); } } private native void androidRuntimePermissionsCallback (boolean permissionWasGranted, long ptrToCallback); @Override public void onRequestPermissionsResult (int permissionID, String permissions[], int[] grantResults) { boolean permissionsGranted = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); if (! permissionsGranted) Log.d ("JUCE", "onRequestPermissionsResult: runtime permission was DENIED: " + getAndroidPermissionName (permissionID)); Long ptrToCallback = permissionCallbackPtrMap.get (permissionID); permissionCallbackPtrMap.remove (permissionID); androidRuntimePermissionsCallback (permissionsGranted, ptrToCallback); } //============================================================================== public static class MidiPortID extends Object { public MidiPortID (int index, boolean direction) { androidIndex = index; isInput = direction; } public int androidIndex; public boolean isInput; @Override public int hashCode() { Integer i = new Integer (androidIndex); return i.hashCode() * (isInput ? -1 : 1); } @Override public boolean equals (Object obj) { if (obj == null) return false; if (getClass() != obj.getClass()) return false; MidiPortID other = (MidiPortID) obj; return (androidIndex == other.androidIndex && isInput == other.isInput); } } public interface JuceMidiPort { boolean isInputPort(); // start, stop does nothing on an output port void start(); void stop(); void close(); MidiPortID getPortId(); // send will do nothing on an input port void sendMidi (byte[] msg, int offset, int count); } //============================================================================== //============================================================================== public class BluetoothManager extends ScanCallback { BluetoothManager() { ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder(); scanFilterBuilder.setServiceUuid (ParcelUuid.fromString (bluetoothLEMidiServiceUUID)); ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(); scanSettingsBuilder.setCallbackType (ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setScanMode (ScanSettings.SCAN_MODE_LOW_POWER) .setScanMode (ScanSettings.MATCH_MODE_STICKY); BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { Log.d ("JUCE", "BluetoothManager error: could not get default Bluetooth adapter"); return; } BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); if (bluetoothLeScanner == null) { Log.d ("JUCE", "BluetoothManager error: could not get Bluetooth LE scanner"); return; } bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()), scanSettingsBuilder.build(), this); } public String[] getMidiBluetoothAddresses() { return bluetoothMidiDevices.toArray (new String[bluetoothMidiDevices.size()]); } public String getHumanReadableStringForBluetoothAddress (String address) { BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address); return btDevice.getName(); } public boolean isBluetoothDevicePaired (String address) { return getAndroidMidiDeviceManager().isBluetoothDevicePaired (address); } public boolean pairBluetoothMidiDevice(String address) { BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address); if (btDevice == null) { Log.d ("JUCE", "failed to create buletooth device from address"); return false; } MidiManager mm = (MidiManager) getSystemService (MIDI_SERVICE); PhysicalMidiDevice midiDevice = PhysicalMidiDevice.fromBluetoothLeDevice (btDevice, mm); if (midiDevice != null) { getAndroidMidiDeviceManager().addDeviceToList (midiDevice); return true; } return false; } public void unpairBluetoothMidiDevice (String address) { getAndroidMidiDeviceManager().unpairBluetoothDevice (address); } public void onScanFailed (int errorCode) { } public void onScanResult (int callbackType, ScanResult result) { if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES || callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH) { BluetoothDevice device = result.getDevice(); if (device != null) bluetoothMidiDevices.add (device.getAddress()); } if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { Log.d ("JUCE", "ScanSettings.CALLBACK_TYPE_MATCH_LOST"); BluetoothDevice device = result.getDevice(); if (device != null) { bluetoothMidiDevices.remove (device.getAddress()); unpairBluetoothMidiDevice (device.getAddress()); } } } public void onBatchScanResults (List results) { for (ScanResult result : results) onScanResult (ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result); } private BluetoothLeScanner scanner; private static final String bluetoothLEMidiServiceUUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"; private HashSet bluetoothMidiDevices = new HashSet(); } public static class JuceMidiInputPort extends MidiReceiver implements JuceMidiPort { private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp); public JuceMidiInputPort (PhysicalMidiDevice device, long host, MidiOutputPort midiPort) { parent = device; juceHost = host; port = midiPort; } @Override public boolean isInputPort() { return true; } @Override public void start() { port.connect (this); } @Override public void stop() { port.disconnect (this); } @Override public void close() { stop(); try { port.close(); } catch (IOException e) { Log.d ("JUCE", "JuceMidiInputPort::close: IOException = " + e.toString()); } if (parent != null) { parent.removePort (port.getPortNumber(), true); parent = null; } } public void onSend (byte[] msg, int offset, int count, long timestamp) { if (count > 0) handleReceive (juceHost, msg, offset, count, timestamp); } @Override public MidiPortID getPortId() { return new MidiPortID (port.getPortNumber(), true); } @Override public void sendMidi (byte[] msg, int offset, int count) { } private PhysicalMidiDevice parent = null; private long juceHost = 0; private MidiOutputPort port; } public static class JuceMidiOutputPort implements JuceMidiPort { public JuceMidiOutputPort (PhysicalMidiDevice device, MidiInputPort midiPort) { parent = device; port = midiPort; } @Override public boolean isInputPort() { return false; } @Override public void start() { } @Override public void stop() { } @Override public void sendMidi (byte[] msg, int offset, int count) { try { port.send(msg, offset, count); } catch (IOException e) { Log.d ("JUCE", "JuceMidiOutputPort::sendMidi: IOException = " + e.toString()); } } @Override public void close() { try { port.close(); } catch (IOException e) { Log.d ("JUCE", "JuceMidiOutputPort::close: IOException = " + e.toString()); } if (parent != null) { parent.removePort (port.getPortNumber(), false); parent = null; } } @Override public MidiPortID getPortId() { return new MidiPortID (port.getPortNumber(), false); } private PhysicalMidiDevice parent = null; private MidiInputPort port; } public static class PhysicalMidiDevice { private static class MidiDeviceThread extends Thread { public Handler handler = null; public Object sync = null; public MidiDeviceThread (Object syncrhonization) { sync = syncrhonization; } public void run() { Looper.prepare(); synchronized (sync) { handler = new Handler(); sync.notifyAll(); } Looper.loop(); } } private static class MidiDeviceOpenCallback implements MidiManager.OnDeviceOpenedListener { public Object sync = null; public boolean isWaiting = true; public android.media.midi.MidiDevice theDevice = null; public MidiDeviceOpenCallback (Object waiter) { sync = waiter; } public void onDeviceOpened (MidiDevice device) { synchronized (sync) { theDevice = device; isWaiting = false; sync.notifyAll(); } } } public static PhysicalMidiDevice fromBluetoothLeDevice (BluetoothDevice bluetoothDevice, MidiManager mm) { Object waitForCreation = new Object(); MidiDeviceThread thread = new MidiDeviceThread (waitForCreation); thread.start(); synchronized (waitForCreation) { while (thread.handler == null) { try { waitForCreation.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "Wait was interrupted but we don't care"); } } } Object waitForDevice = new Object(); MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice); synchronized (waitForDevice) { mm.openBluetoothDevice (bluetoothDevice, openCallback, thread.handler); while (openCallback.isWaiting) { try { waitForDevice.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "Wait was interrupted but we don't care"); } } } if (openCallback.theDevice == null) { Log.d ("JUCE", "openBluetoothDevice failed"); return null; } PhysicalMidiDevice device = new PhysicalMidiDevice(); device.handle = openCallback.theDevice; device.info = device.handle.getInfo(); device.bluetoothAddress = bluetoothDevice.getAddress(); device.midiManager = mm; return device; } public void unpair() { if (! bluetoothAddress.equals ("") && handle != null) { JuceMidiPort ports[] = new JuceMidiPort[0]; ports = juceOpenedPorts.values().toArray(ports); for (int i = 0; i < ports.length; ++i) ports[i].close(); juceOpenedPorts.clear(); try { handle.close(); } catch (IOException e) { Log.d ("JUCE", "handle.close(): IOException = " + e.toString()); } handle = null; } } public static PhysicalMidiDevice fromMidiDeviceInfo (MidiDeviceInfo info, MidiManager mm) { PhysicalMidiDevice device = new PhysicalMidiDevice(); device.info = info; device.midiManager = mm; return device; } public PhysicalMidiDevice() { bluetoothAddress = ""; juceOpenedPorts = new Hashtable(); handle = null; } public MidiDeviceInfo.PortInfo[] getPorts() { return info.getPorts(); } public String getHumanReadableNameForPort (MidiDeviceInfo.PortInfo port, int portIndexToUseInName) { String portName = port.getName(); if (portName.equals ("")) portName = ((port.getType() == MidiDeviceInfo.PortInfo.TYPE_OUTPUT) ? "Out " : "In ") + Integer.toString (portIndexToUseInName); return getHumanReadableDeviceName() + " " + portName; } public String getHumanReadableNameForPort (int portType, int androidPortID, int portIndexToUseInName) { MidiDeviceInfo.PortInfo[] ports = info.getPorts(); for (MidiDeviceInfo.PortInfo port : ports) { if (port.getType() == portType) { if (port.getPortNumber() == androidPortID) return getHumanReadableNameForPort (port, portIndexToUseInName); } } return "Unknown"; } public String getHumanReadableDeviceName() { Bundle bundle = info.getProperties(); return bundle.getString (MidiDeviceInfo.PROPERTY_NAME , "Unknown device"); } public void checkIfDeviceCanBeClosed() { if (juceOpenedPorts.size() == 0) { // never close bluetooth LE devices, otherwise they unpair and we have // no idea how many ports they have. // Only remove bluetooth devices when we specifically unpair if (bluetoothAddress.equals ("")) { try { handle.close(); handle = null; } catch (IOException e) { Log.d ("JUCE", "PhysicalMidiDevice::checkIfDeviceCanBeClosed: IOException = " + e.toString()); } } } } public void removePort (int portIdx, boolean isInput) { MidiPortID portID = new MidiPortID (portIdx, isInput); JuceMidiPort port = juceOpenedPorts.get (portID); if (port != null) { juceOpenedPorts.remove (portID); checkIfDeviceCanBeClosed(); return; } // tried to remove a port that was never added assert false; } public JuceMidiPort openPort (int portIdx, boolean isInput, long host) { open(); if (handle == null) { Log.d ("JUCE", "PhysicalMidiDevice::openPort: handle = null, device not open"); return null; } // make sure that the port is not already open if (findPortForIdx (portIdx, isInput) != null) { Log.d ("JUCE", "PhysicalMidiDevice::openInputPort: port already open, not opening again!"); return null; } JuceMidiPort retval = null; if (isInput) { MidiOutputPort androidPort = handle.openOutputPort (portIdx); if (androidPort == null) { Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openOutputPort (portIdx = " + Integer.toString (portIdx) + ") failed!"); return null; } retval = new JuceMidiInputPort (this, host, androidPort); } else { MidiInputPort androidPort = handle.openInputPort (portIdx); if (androidPort == null) { Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openInputPort (portIdx = " + Integer.toString (portIdx) + ") failed!"); return null; } retval = new JuceMidiOutputPort (this, androidPort); } juceOpenedPorts.put (new MidiPortID (portIdx, isInput), retval); return retval; } private JuceMidiPort findPortForIdx (int idx, boolean isInput) { return juceOpenedPorts.get (new MidiPortID (idx, isInput)); } // opens the device private synchronized void open() { if (handle != null) return; Object waitForCreation = new Object(); MidiDeviceThread thread = new MidiDeviceThread (waitForCreation); thread.start(); synchronized(waitForCreation) { while (thread.handler == null) { try { waitForCreation.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "wait was interrupted but we don't care"); } } } Object waitForDevice = new Object(); MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice); synchronized (waitForDevice) { midiManager.openDevice (info, openCallback, thread.handler); while (openCallback.isWaiting) { try { waitForDevice.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "wait was interrupted but we don't care"); } } } handle = openCallback.theDevice; } private MidiDeviceInfo info; private Hashtable juceOpenedPorts; public MidiDevice handle; public String bluetoothAddress; private MidiManager midiManager; } //============================================================================== public class MidiDeviceManager extends MidiManager.DeviceCallback { public class MidiPortPath { public PhysicalMidiDevice midiDevice; public int androidMidiPortID; public int portIndexToUseInName; } public class JuceDeviceList { public ArrayList inPorts = new ArrayList(); public ArrayList outPorts = new ArrayList(); } // We need to keep a thread local copy of the devices // which we returned the last time // getJuceAndroidMidiIn/OutputDevices() was called private final ThreadLocal lastDevicesReturned = new ThreadLocal() { @Override protected JuceDeviceList initialValue() { return new JuceDeviceList(); } }; public MidiDeviceManager() { physicalMidiDevices = new ArrayList(); manager = (MidiManager) getSystemService (MIDI_SERVICE); if (manager == null) { Log.d ("JUCE", "MidiDeviceManager error: could not get MidiManager system service"); return; } manager.registerDeviceCallback (this, null); MidiDeviceInfo[] foundDevices = manager.getDevices(); for (MidiDeviceInfo info : foundDevices) physicalMidiDevices.add (PhysicalMidiDevice.fromMidiDeviceInfo (info, manager)); } // specifically add a device to the list public void addDeviceToList (PhysicalMidiDevice device) { physicalMidiDevices.add (device); } public void unpairBluetoothDevice (String address) { for (int i = 0; i < physicalMidiDevices.size(); ++i) { PhysicalMidiDevice device = physicalMidiDevices.get(i); if (device.bluetoothAddress.equals (address)) { physicalMidiDevices.remove (i); device.unpair(); return; } } } public boolean isBluetoothDevicePaired (String address) { for (int i = 0; i < physicalMidiDevices.size(); ++i) { PhysicalMidiDevice device = physicalMidiDevices.get(i); if (device.bluetoothAddress.equals (address)) return true; } return false; } public String[] getJuceAndroidMidiInputDevices() { return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_INPUT); } public String[] getJuceAndroidMidiOutputDevices() { return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_OUTPUT); } private String[] getJuceAndroidMidiDevices (int portType) { ArrayList listOfReturnedDevices = new ArrayList(); List deviceNames = new ArrayList(); for (PhysicalMidiDevice physicalMidiDevice : physicalMidiDevices) { int portIdx = 0; MidiDeviceInfo.PortInfo[] ports = physicalMidiDevice.getPorts(); for (MidiDeviceInfo.PortInfo port : ports) { if (port.getType() == portType) { MidiPortPath path = new MidiPortPath(); path.midiDevice = physicalMidiDevice; path.androidMidiPortID = port.getPortNumber(); path.portIndexToUseInName = ++portIdx; listOfReturnedDevices.add (path); deviceNames.add (physicalMidiDevice.getHumanReadableNameForPort (port, path.portIndexToUseInName)); } } } String[] deviceNamesArray = new String[deviceNames.size()]; if (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT) { lastDevicesReturned.get().inPorts.clear(); lastDevicesReturned.get().inPorts.addAll (listOfReturnedDevices); } else { lastDevicesReturned.get().outPorts.clear(); lastDevicesReturned.get().outPorts.addAll (listOfReturnedDevices); } return deviceNames.toArray(deviceNamesArray); } public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host) { ArrayList lastDevices = lastDevicesReturned.get().inPorts; if (index >= lastDevices.size() || index < 0) return null; MidiPortPath path = lastDevices.get (index); return path.midiDevice.openPort (path.androidMidiPortID, true, host); } public JuceMidiPort openMidiOutputPortWithJuceIndex (int index) { ArrayList lastDevices = lastDevicesReturned.get().outPorts; if (index >= lastDevices.size() || index < 0) return null; MidiPortPath path = lastDevices.get (index); return path.midiDevice.openPort (path.androidMidiPortID, false, 0); } public String getInputPortNameForJuceIndex (int index) { ArrayList lastDevices = lastDevicesReturned.get().inPorts; if (index >= lastDevices.size() || index < 0) return ""; MidiPortPath path = lastDevices.get (index); return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_INPUT, path.androidMidiPortID, path.portIndexToUseInName); } public String getOutputPortNameForJuceIndex (int index) { ArrayList lastDevices = lastDevicesReturned.get().outPorts; if (index >= lastDevices.size() || index < 0) return ""; MidiPortPath path = lastDevices.get (index); return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_OUTPUT, path.androidMidiPortID, path.portIndexToUseInName); } public void onDeviceAdded (MidiDeviceInfo info) { PhysicalMidiDevice device = PhysicalMidiDevice.fromMidiDeviceInfo (info, manager); // Do not add bluetooth devices as they are already added by the native bluetooth dialog if (info.getType() != MidiDeviceInfo.TYPE_BLUETOOTH) physicalMidiDevices.add (device); } public void onDeviceRemoved (MidiDeviceInfo info) { for (int i = 0; i < physicalMidiDevices.size(); ++i) { if (physicalMidiDevices.get(i).info.getId() == info.getId()) { physicalMidiDevices.remove (i); return; } } // Don't assert here as this may be called again after a bluetooth device is unpaired } public void onDeviceStatusChanged (MidiDeviceStatus status) { } private ArrayList physicalMidiDevices; private MidiManager manager; } public MidiDeviceManager getAndroidMidiDeviceManager() { if (getSystemService (MIDI_SERVICE) == null) return null; synchronized (JuceDemo.class) { if (midiDeviceManager == null) midiDeviceManager = new MidiDeviceManager(); } return midiDeviceManager; } public BluetoothManager getAndroidBluetoothManager() { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter == null) return null; if (adapter.getBluetoothLeScanner() == null) return null; synchronized (JuceDemo.class) { if (bluetoothManager == null) bluetoothManager = new BluetoothManager(); } return bluetoothManager; } //============================================================================== @Override public void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); isScreenSaverEnabled = true; hideActionBar(); viewHolder = new ViewHolder (this); setContentView (viewHolder); setVolumeControlStream (AudioManager.STREAM_MUSIC); permissionCallbackPtrMap = new HashMap(); } @Override protected void onDestroy() { quitApp(); super.onDestroy(); clearDataCache(); } @Override protected void onPause() { suspendApp(); super.onPause(); } @Override protected void onResume() { super.onResume(); resumeApp(); } @Override public void onConfigurationChanged (Configuration cfg) { super.onConfigurationChanged (cfg); setContentView (viewHolder); } private void callAppLauncher() { launchApp (getApplicationInfo().publicSourceDir, getApplicationInfo().dataDir); } private void hideActionBar() { // get "getActionBar" method java.lang.reflect.Method getActionBarMethod = null; try { getActionBarMethod = this.getClass().getMethod ("getActionBar"); } catch (SecurityException e) { return; } catch (NoSuchMethodException e) { return; } if (getActionBarMethod == null) return; // invoke "getActionBar" method Object actionBar = null; try { actionBar = getActionBarMethod.invoke (this); } catch (java.lang.IllegalArgumentException e) { return; } catch (java.lang.IllegalAccessException e) { return; } catch (java.lang.reflect.InvocationTargetException e) { return; } if (actionBar == null) return; // get "hide" method java.lang.reflect.Method actionBarHideMethod = null; try { actionBarHideMethod = actionBar.getClass().getMethod ("hide"); } catch (SecurityException e) { return; } catch (NoSuchMethodException e) { return; } if (actionBarHideMethod == null) return; // invoke "hide" method try { actionBarHideMethod.invoke (actionBar); } catch (java.lang.IllegalArgumentException e) {} catch (java.lang.IllegalAccessException e) {} catch (java.lang.reflect.InvocationTargetException e) {} } //============================================================================== private native void launchApp (String appFile, String appDataDir); private native void quitApp(); private native void suspendApp(); private native void resumeApp(); private native void setScreenSize (int screenWidth, int screenHeight, int dpi); //============================================================================== public native void deliverMessage (long value); private android.os.Handler messageHandler = new android.os.Handler(); public final void postMessage (long value) { messageHandler.post (new MessageCallback (value)); } private final class MessageCallback implements Runnable { public MessageCallback (long value_) { value = value_; } public final void run() { deliverMessage (value); } private long value; } //============================================================================== private ViewHolder viewHolder; private MidiDeviceManager midiDeviceManager = null; private BluetoothManager bluetoothManager = null; private boolean isScreenSaverEnabled; private java.util.Timer keepAliveTimer; public final ComponentPeerView createNewView (boolean opaque, long host) { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); return v; } public final void deleteView (ComponentPeerView view) { ViewGroup group = (ViewGroup) (view.getParent()); if (group != null) group.removeView (view); } public final void deleteNativeSurfaceView (NativeSurfaceView view) { ViewGroup group = (ViewGroup) (view.getParent()); if (group != null) group.removeView (view); } final class ViewHolder extends ViewGroup { public ViewHolder (Context context) { super (context); setDescendantFocusability (ViewGroup.FOCUS_AFTER_DESCENDANTS); setFocusable (false); } protected final void onLayout (boolean changed, int left, int top, int right, int bottom) { setScreenSize (getWidth(), getHeight(), getDPI()); if (isFirstResize) { isFirstResize = false; callAppLauncher(); } } private final int getDPI() { DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics (metrics); return metrics.densityDpi; } private boolean isFirstResize = true; } public final void excludeClipRegion (android.graphics.Canvas canvas, float left, float top, float right, float bottom) { canvas.clipRect (left, top, right, bottom, android.graphics.Region.Op.DIFFERENCE); } //============================================================================== public final void setScreenSaver (boolean enabled) { if (isScreenSaverEnabled != enabled) { isScreenSaverEnabled = enabled; if (keepAliveTimer != null) { keepAliveTimer.cancel(); keepAliveTimer = null; } if (enabled) { getWindow().clearFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getWindow().addFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // If no user input is received after about 3 seconds, the OS will lower the // task's priority, so this timer forces it to be kept active. keepAliveTimer = new java.util.Timer(); keepAliveTimer.scheduleAtFixedRate (new TimerTask() { @Override public void run() { android.app.Instrumentation instrumentation = new android.app.Instrumentation(); try { instrumentation.sendKeyDownUpSync (KeyEvent.KEYCODE_UNKNOWN); } catch (Exception e) { } } }, 2000, 2000); } } } public final boolean getScreenSaver() { return isScreenSaverEnabled; } //============================================================================== public final String getClipboardContent() { ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE); return clipboard.getText().toString(); } public final void setClipboardContent (String newText) { ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE); clipboard.setText (newText); } //============================================================================== public final void showMessageBox (String title, String message, final long callback) { AlertDialog.Builder builder = new AlertDialog.Builder (this); builder.setTitle (title) .setMessage (message) .setCancelable (true) .setPositiveButton ("OK", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceDemo.this.alertDismissed (callback, 0); } }); builder.create().show(); } public final void showOkCancelBox (String title, String message, final long callback) { AlertDialog.Builder builder = new AlertDialog.Builder (this); builder.setTitle (title) .setMessage (message) .setCancelable (true) .setPositiveButton ("OK", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceDemo.this.alertDismissed (callback, 1); } }) .setNegativeButton ("Cancel", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceDemo.this.alertDismissed (callback, 0); } }); builder.create().show(); } public final void showYesNoCancelBox (String title, String message, final long callback) { AlertDialog.Builder builder = new AlertDialog.Builder (this); builder.setTitle (title) .setMessage (message) .setCancelable (true) .setPositiveButton ("Yes", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceDemo.this.alertDismissed (callback, 1); } }) .setNegativeButton ("No", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceDemo.this.alertDismissed (callback, 2); } }) .setNeutralButton ("Cancel", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceDemo.this.alertDismissed (callback, 0); } }); builder.create().show(); } public native void alertDismissed (long callback, int id); //============================================================================== public final class ComponentPeerView extends ViewGroup implements View.OnFocusChangeListener { public ComponentPeerView (Context context, boolean opaque_, long host) { super (context); this.host = host; setWillNotDraw (false); opaque = opaque_; setFocusable (true); setFocusableInTouchMode (true); setOnFocusChangeListener (this); requestFocus(); // swap red and blue colours to match internal opengl texture format ColorMatrix colorMatrix = new ColorMatrix(); float[] colorTransform = { 0, 0, 1.0f, 0, 0, 0, 1.0f, 0, 0, 0, 1.0f, 0, 0, 0, 0, 0, 0, 0, 1.0f, 0 }; colorMatrix.set (colorTransform); paint.setColorFilter (new ColorMatrixColorFilter (colorMatrix)); } //============================================================================== private native void handlePaint (long host, Canvas canvas, Paint paint); @Override public void onDraw (Canvas canvas) { handlePaint (host, canvas, paint); } @Override public boolean isOpaque() { return opaque; } private boolean opaque; private long host; private Paint paint = new Paint(); //============================================================================== private native void handleMouseDown (long host, int index, float x, float y, long time); private native void handleMouseDrag (long host, int index, float x, float y, long time); private native void handleMouseUp (long host, int index, float x, float y, long time); @Override public boolean onTouchEvent (MotionEvent event) { int action = event.getAction(); long time = event.getEventTime(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: handleMouseDown (host, event.getPointerId(0), event.getX(), event.getY(), time); return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: handleMouseUp (host, event.getPointerId(0), event.getX(), event.getY(), time); return true; case MotionEvent.ACTION_MOVE: { int n = event.getPointerCount(); for (int i = 0; i < n; ++i) handleMouseDrag (host, event.getPointerId(i), event.getX(i), event.getY(i), time); return true; } case MotionEvent.ACTION_POINTER_UP: { int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; handleMouseUp (host, event.getPointerId(i), event.getX(i), event.getY(i), time); return true; } case MotionEvent.ACTION_POINTER_DOWN: { int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; handleMouseDown (host, event.getPointerId(i), event.getX(i), event.getY(i), time); return true; } default: break; } return false; } //============================================================================== private native void handleKeyDown (long host, int keycode, int textchar); private native void handleKeyUp (long host, int keycode, int textchar); public void showKeyboard (String type) { InputMethodManager imm = (InputMethodManager) getSystemService (Context.INPUT_METHOD_SERVICE); if (imm != null) { if (type.length() > 0) { imm.showSoftInput (this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); imm.setInputMethod (getWindowToken(), type); } else { imm.hideSoftInputFromWindow (getWindowToken(), 0); } } } @Override public boolean onKeyDown (int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: return super.onKeyDown (keyCode, event); default: break; } handleKeyDown (host, keyCode, event.getUnicodeChar()); return true; } @Override public boolean onKeyUp (int keyCode, KeyEvent event) { handleKeyUp (host, keyCode, event.getUnicodeChar()); return true; } @Override public boolean onKeyMultiple (int keyCode, int count, KeyEvent event) { if (keyCode != KeyEvent.KEYCODE_UNKNOWN || event.getAction() != KeyEvent.ACTION_MULTIPLE) return super.onKeyMultiple (keyCode, count, event); if (event.getCharacters() != null) { int utf8Char = event.getCharacters().codePointAt (0); handleKeyDown (host, utf8Char, utf8Char); return true; } return false; } // this is here to make keyboard entry work on a Galaxy Tab2 10.1 @Override public InputConnection onCreateInputConnection (EditorInfo outAttrs) { outAttrs.actionLabel = ""; outAttrs.hintText = ""; outAttrs.initialCapsMode = 0; outAttrs.initialSelEnd = outAttrs.initialSelStart = -1; outAttrs.label = ""; outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.inputType = InputType.TYPE_NULL; return new BaseInputConnection (this, false); } //============================================================================== @Override protected void onSizeChanged (int w, int h, int oldw, int oldh) { super.onSizeChanged (w, h, oldw, oldh); viewSizeChanged (host); } @Override protected void onLayout (boolean changed, int left, int top, int right, int bottom) { for (int i = getChildCount(); --i >= 0;) requestTransparentRegion (getChildAt (i)); } private native void viewSizeChanged (long host); @Override public void onFocusChange (View v, boolean hasFocus) { if (v == this) focusChanged (host, hasFocus); } private native void focusChanged (long host, boolean hasFocus); public void setViewName (String newName) {} public boolean isVisible() { return getVisibility() == VISIBLE; } public void setVisible (boolean b) { setVisibility (b ? VISIBLE : INVISIBLE); } public boolean containsPoint (int x, int y) { return true; //xxx needs to check overlapping views } } //============================================================================== public static class NativeSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private long nativeContext = 0; NativeSurfaceView (Context context, long nativeContextPtr) { super (context); nativeContext = nativeContextPtr; } public Surface getNativeSurface() { Surface retval = null; SurfaceHolder holder = getHolder(); if (holder != null) retval = holder.getSurface(); return retval; } //============================================================================== @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { surfaceChangedNative (nativeContext, holder, format, width, height); } @Override public void surfaceCreated (SurfaceHolder holder) { surfaceCreatedNative (nativeContext, holder); } @Override public void surfaceDestroyed (SurfaceHolder holder) { surfaceDestroyedNative (nativeContext, holder); } @Override protected void dispatchDraw (Canvas canvas) { super.dispatchDraw (canvas); dispatchDrawNative (nativeContext, canvas); } //============================================================================== @Override protected void onAttachedToWindow () { super.onAttachedToWindow(); getHolder().addCallback (this); } @Override protected void onDetachedFromWindow () { super.onDetachedFromWindow(); getHolder().removeCallback (this); } //============================================================================== private native void dispatchDrawNative (long nativeContextPtr, Canvas canvas); private native void surfaceCreatedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder, int format, int width, int height); } public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr) { return new NativeSurfaceView (this, nativeSurfacePtr); } //============================================================================== public final int[] renderGlyph (char glyph, Paint paint, android.graphics.Matrix matrix, Rect bounds) { Path p = new Path(); paint.getTextPath (String.valueOf (glyph), 0, 1, 0.0f, 0.0f, p); RectF boundsF = new RectF(); p.computeBounds (boundsF, true); matrix.mapRect (boundsF); boundsF.roundOut (bounds); bounds.left--; bounds.right++; final int w = bounds.width(); final int h = Math.max (1, bounds.height()); Bitmap bm = Bitmap.createBitmap (w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas (bm); matrix.postTranslate (-bounds.left, -bounds.top); c.setMatrix (matrix); c.drawPath (p, paint); final int sizeNeeded = w * h; if (cachedRenderArray.length < sizeNeeded) cachedRenderArray = new int [sizeNeeded]; bm.getPixels (cachedRenderArray, 0, w, 0, 0, w, h); bm.recycle(); return cachedRenderArray; } private int[] cachedRenderArray = new int [256]; //============================================================================== public static class HTTPStream { public HTTPStream (HttpURLConnection connection_, int[] statusCode, StringBuffer responseHeaders) throws IOException { connection = connection_; try { inputStream = new BufferedInputStream (connection.getInputStream()); } catch (IOException e) { if (connection.getResponseCode() < 400) throw e; } finally { statusCode[0] = connection.getResponseCode(); } if (statusCode[0] >= 400) inputStream = connection.getErrorStream(); else inputStream = connection.getInputStream(); for (java.util.Map.Entry> entry : connection.getHeaderFields().entrySet()) if (entry.getKey() != null && entry.getValue() != null) responseHeaders.append (entry.getKey() + ": " + android.text.TextUtils.join (",", entry.getValue()) + "\n"); } public final void release() { try { inputStream.close(); } catch (IOException e) {} connection.disconnect(); } public final int read (byte[] buffer, int numBytes) { int num = 0; try { num = inputStream.read (buffer, 0, numBytes); } catch (IOException e) {} if (num > 0) position += num; return num; } public final long getPosition() { return position; } public final long getTotalLength() { return -1; } public final boolean isExhausted() { return false; } public final boolean setPosition (long newPos) { return false; } private HttpURLConnection connection; private InputStream inputStream; private long position; } public static final HTTPStream createHTTPStream (String address, boolean isPost, byte[] postData, String headers, int timeOutMs, int[] statusCode, StringBuffer responseHeaders, int numRedirectsToFollow, String httpRequestCmd) { // timeout parameter of zero for HttpUrlConnection is a blocking connect (negative value for juce::URL) if (timeOutMs < 0) timeOutMs = 0; else if (timeOutMs == 0) timeOutMs = 30000; // headers - if not empty, this string is appended onto the headers that are used for the request. It must therefore be a valid set of HTML header directives, separated by newlines. // So convert headers string to an array, with an element for each line String headerLines[] = headers.split("\\n"); for (;;) { try { HttpURLConnection connection = (HttpURLConnection) (new URL(address).openConnection()); if (connection != null) { try { connection.setInstanceFollowRedirects (false); connection.setConnectTimeout (timeOutMs); connection.setReadTimeout (timeOutMs); // Set request headers for (int i = 0; i < headerLines.length; ++i) { int pos = headerLines[i].indexOf (":"); if (pos > 0 && pos < headerLines[i].length()) { String field = headerLines[i].substring (0, pos); String value = headerLines[i].substring (pos + 1); if (value.length() > 0) connection.setRequestProperty (field, value); } } connection.setRequestMethod (httpRequestCmd); if (isPost) { connection.setDoOutput (true); if (postData != null) { OutputStream out = connection.getOutputStream(); out.write(postData); out.flush(); } } HTTPStream httpStream = new HTTPStream (connection, statusCode, responseHeaders); // Process redirect & continue as necessary int status = statusCode[0]; if (--numRedirectsToFollow >= 0 && (status == 301 || status == 302 || status == 303 || status == 307)) { // Assumes only one occurrence of "Location" int pos1 = responseHeaders.indexOf ("Location:") + 10; int pos2 = responseHeaders.indexOf ("\n", pos1); if (pos2 > pos1) { String newLocation = responseHeaders.substring(pos1, pos2); // Handle newLocation whether it's absolute or relative URL baseUrl = new URL (address); URL newUrl = new URL (baseUrl, newLocation); String transformedNewLocation = newUrl.toString(); if (transformedNewLocation != address) { address = transformedNewLocation; // Clear responseHeaders before next iteration responseHeaders.delete (0, responseHeaders.length()); continue; } } } return httpStream; } catch (Throwable e) { connection.disconnect(); } } } catch (Throwable e) {} return null; } } public final void launchURL (String url) { startActivity (new Intent (Intent.ACTION_VIEW, Uri.parse (url))); } public static final String getLocaleValue (boolean isRegion) { java.util.Locale locale = java.util.Locale.getDefault(); return isRegion ? locale.getDisplayCountry (java.util.Locale.US) : locale.getDisplayLanguage (java.util.Locale.US); } private static final String getFileLocation (String type) { return Environment.getExternalStoragePublicDirectory (type).getAbsolutePath(); } public static final String getDocumentsFolder() { return Environment.getDataDirectory().getAbsolutePath(); } public static final String getPicturesFolder() { return getFileLocation (Environment.DIRECTORY_PICTURES); } public static final String getMusicFolder() { return getFileLocation (Environment.DIRECTORY_MUSIC); } public static final String getMoviesFolder() { return getFileLocation (Environment.DIRECTORY_MOVIES); } public static final String getDownloadsFolder() { return getFileLocation (Environment.DIRECTORY_DOWNLOADS); } //============================================================================== private final class SingleMediaScanner implements MediaScannerConnectionClient { public SingleMediaScanner (Context context, String filename) { file = filename; msc = new MediaScannerConnection (context, this); msc.connect(); } @Override public void onMediaScannerConnected() { msc.scanFile (file, null); } @Override public void onScanCompleted (String path, Uri uri) { msc.disconnect(); } private MediaScannerConnection msc; private String file; } public final void scanFile (String filename) { new SingleMediaScanner (this, filename); } public final Typeface getTypeFaceFromAsset (String assetName) { try { return Typeface.createFromAsset (this.getResources().getAssets(), assetName); } catch (Throwable e) {} return null; } final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); public static String bytesToHex (byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; ++j) { int v = bytes[j] & 0xff; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0f]; } return new String (hexChars); } final private java.util.Map dataCache = new java.util.HashMap(); synchronized private final File getDataCacheFile (byte[] data) { try { java.security.MessageDigest digest = java.security.MessageDigest.getInstance ("MD5"); digest.update (data); String key = bytesToHex (digest.digest()); if (dataCache.containsKey (key)) return (File) dataCache.get (key); File f = new File (this.getCacheDir(), "bindata_" + key); f.delete(); FileOutputStream os = new FileOutputStream (f); os.write (data, 0, data.length); dataCache.put (key, f); return f; } catch (Throwable e) {} return null; } private final void clearDataCache() { java.util.Iterator it = dataCache.values().iterator(); while (it.hasNext()) { File f = (File) it.next(); f.delete(); } } public final Typeface getTypeFaceFromByteArray (byte[] data) { try { File f = getDataCacheFile (data); if (f != null) return Typeface.createFromFile (f); } catch (Exception e) { Log.e ("JUCE", e.toString()); } return null; } public final int getAndroidSDKVersion() { return android.os.Build.VERSION.SDK_INT; } public final String audioManagerGetProperty (String property) { Object obj = getSystemService (AUDIO_SERVICE); if (obj == null) return null; java.lang.reflect.Method method; try { method = obj.getClass().getMethod ("getProperty", String.class); } catch (SecurityException e) { return null; } catch (NoSuchMethodException e) { return null; } if (method == null) return null; try { return (String) method.invoke (obj, property); } catch (java.lang.IllegalArgumentException e) {} catch (java.lang.IllegalAccessException e) {} catch (java.lang.reflect.InvocationTargetException e) {} return null; } public final int setCurrentThreadPriority (int priority) { android.os.Process.setThreadPriority (android.os.Process.myTid(), priority); return android.os.Process.getThreadPriority (android.os.Process.myTid()); } public final boolean hasSystemFeature (String property) { return getPackageManager().hasSystemFeature (property); } private static class JuceThread extends Thread { public JuceThread (long host, String threadName, long threadStackSize) { super (null, null, threadName, threadStackSize); _this = host; } public void run() { runThread(_this); } private native void runThread (long host); private long _this; } public final Thread createNewThread(long host, String threadName, long threadStackSize) { return new JuceThread(host, threadName, threadStackSize); } }