com.juce.jucedemo.JuceDemo.java Source code

Java tutorial

Introduction

Here is the source code for com.juce.jucedemo.JuceDemo.java

Source

/*
  ==============================================================================
    
   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<Integer, Long> 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<ScanResult> 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<String> bluetoothMidiDevices = new HashSet<String>();
    }

    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<MidiPortID, JuceMidiPort>();
            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<MidiPortID, JuceMidiPort> 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<MidiPortPath> inPorts = new ArrayList<MidiPortPath>();
            public ArrayList<MidiPortPath> outPorts = new ArrayList<MidiPortPath>();
        }

        // We need to keep a thread local copy of the devices
        // which we returned the last time
        // getJuceAndroidMidiIn/OutputDevices() was called
        private final ThreadLocal<JuceDeviceList> lastDevicesReturned = new ThreadLocal<JuceDeviceList>() {
            @Override
            protected JuceDeviceList initialValue() {
                return new JuceDeviceList();
            }
        };

        public MidiDeviceManager() {
            physicalMidiDevices = new ArrayList<PhysicalMidiDevice>();
            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<MidiPortPath> listOfReturnedDevices = new ArrayList<MidiPortPath>();
            List<String> deviceNames = new ArrayList<String>();

            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<MidiPortPath> 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<MidiPortPath> 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<MidiPortPath> 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<MidiPortPath> 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<PhysicalMidiDevice> 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<Integer, Long>();
    }

    @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<String, java.util.List<String>> 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);
    }
}