a7e2360225a243c5e0287bc114163d463aec2c66
[juce.git] /
1 /*\r
2   ==============================================================================\r
3 \r
4    This file is part of the JUCE library.\r
5    Copyright (c) 2017 - ROLI Ltd.\r
6 \r
7    JUCE is an open source library subject to commercial or open-source\r
8    licensing.\r
9 \r
10    The code included in this file is provided under the terms of the ISC license\r
11    http://www.isc.org/downloads/software-support-policy/isc-license. Permission\r
12    To use, copy, modify, and/or distribute this software for any purpose with or\r
13    without fee is hereby granted provided that the above copyright notice and\r
14    this permission notice appear in all copies.\r
15 \r
16    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER\r
17    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE\r
18    DISCLAIMED.\r
19 \r
20   ==============================================================================\r
21 */\r
22 \r
23 package com.juce.audioperformancetest;\r
24 \r
25 import android.app.Activity;\r
26 import android.app.AlertDialog;\r
27 import android.content.DialogInterface;\r
28 import android.content.Context;\r
29 import android.content.Intent;\r
30 import android.content.res.Configuration;\r
31 import android.content.pm.PackageInfo;\r
32 import android.content.pm.PackageManager;\r
33 import android.net.http.SslError;\r
34 import android.net.Uri;\r
35 import android.os.Bundle;\r
36 import android.os.Looper;\r
37 import android.os.Handler;\r
38 import android.os.Message;\r
39 import android.os.ParcelUuid;\r
40 import android.os.Environment;\r
41 import android.view.*;\r
42 import android.view.inputmethod.BaseInputConnection;\r
43 import android.view.inputmethod.EditorInfo;\r
44 import android.view.inputmethod.InputConnection;\r
45 import android.view.inputmethod.InputMethodManager;\r
46 import android.graphics.*;\r
47 import android.text.ClipboardManager;\r
48 import android.text.InputType;\r
49 import android.util.DisplayMetrics;\r
50 import android.util.Log;\r
51 import android.util.Pair;\r
52 import android.webkit.SslErrorHandler;\r
53 import android.webkit.WebChromeClient;\r
54 import android.webkit.WebResourceError;\r
55 import android.webkit.WebResourceRequest;\r
56 import android.webkit.WebResourceResponse;\r
57 import android.webkit.WebView;\r
58 import android.webkit.WebViewClient;\r
59 import java.lang.Runnable;\r
60 import java.lang.ref.WeakReference;\r
61 import java.lang.reflect.*;\r
62 import java.util.*;\r
63 import java.io.*;\r
64 import java.net.URL;\r
65 import java.net.HttpURLConnection;\r
66 import android.media.AudioManager;\r
67 import android.Manifest;\r
68 import java.util.concurrent.CancellationException;\r
69 import java.util.concurrent.Future;\r
70 import java.util.concurrent.Executors;\r
71 import java.util.concurrent.ExecutorService;\r
72 import java.util.concurrent.ExecutionException;\r
73 import java.util.concurrent.TimeUnit;\r
74 import java.util.concurrent.Callable;\r
75 import java.util.concurrent.TimeoutException;\r
76 import java.util.concurrent.locks.ReentrantLock;\r
77 import java.util.concurrent.atomic.*;\r
78 \r
79 import android.media.midi.*;\r
80 import android.bluetooth.*;\r
81 import android.bluetooth.le.*;\r
82 \r
83 \r
84 //==============================================================================\r
85 public class AudioPerformanceTest   extends Activity\r
86 {\r
87     //==============================================================================\r
88     static\r
89     {\r
90         System.loadLibrary ("juce_jni");\r
91     }\r
92 \r
93     //==============================================================================\r
94     public boolean isPermissionDeclaredInManifest (int permissionID)\r
95     {\r
96         String permissionToCheck = getAndroidPermissionName(permissionID);\r
97 \r
98         try\r
99         {\r
100             PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS);\r
101 \r
102             if (info.requestedPermissions != null)\r
103                 for (String permission : info.requestedPermissions)\r
104                     if (permission.equals (permissionToCheck))\r
105                         return true;\r
106         }\r
107         catch (PackageManager.NameNotFoundException e)\r
108         {\r
109             Log.d ("JUCE", "isPermissionDeclaredInManifest: PackageManager.NameNotFoundException = " + e.toString());\r
110         }\r
111 \r
112         Log.d ("JUCE", "isPermissionDeclaredInManifest: could not find requested permission " + permissionToCheck);\r
113         return false;\r
114     }\r
115 \r
116     //==============================================================================\r
117     // these have to match the values of enum PermissionID in C++ class RuntimePermissions:\r
118     private static final int JUCE_PERMISSIONS_RECORD_AUDIO = 1;\r
119     private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2;\r
120     private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3;\r
121     private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4;\r
122 \r
123     private static String getAndroidPermissionName (int permissionID)\r
124     {\r
125         switch (permissionID)\r
126         {\r
127             case JUCE_PERMISSIONS_RECORD_AUDIO:           return Manifest.permission.RECORD_AUDIO;\r
128             case JUCE_PERMISSIONS_BLUETOOTH_MIDI:         return Manifest.permission.ACCESS_COARSE_LOCATION;\r
129                                                           // use string value as this is not defined in SDKs < 16\r
130             case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE:  return "android.permission.READ_EXTERNAL_STORAGE";\r
131             case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE;\r
132         }\r
133 \r
134         // unknown permission ID!\r
135         assert false;\r
136         return new String();\r
137     }\r
138 \r
139     public boolean isPermissionGranted (int permissionID)\r
140     {\r
141         return getApplicationContext().checkCallingOrSelfPermission (getAndroidPermissionName (permissionID)) == PackageManager.PERMISSION_GRANTED;\r
142     }\r
143 \r
144     private Map<Integer, Long> permissionCallbackPtrMap;\r
145 \r
146     public void requestRuntimePermission (int permissionID, long ptrToCallback)\r
147     {\r
148         String permissionName = getAndroidPermissionName (permissionID);\r
149 \r
150         if (getApplicationContext().checkCallingOrSelfPermission (permissionName) != PackageManager.PERMISSION_GRANTED)\r
151         {\r
152             // remember callbackPtr, request permissions, and let onRequestPermissionResult call callback asynchronously\r
153             permissionCallbackPtrMap.put (permissionID, ptrToCallback);\r
154             requestPermissionsCompat (new String[]{permissionName}, permissionID);\r
155         }\r
156         else\r
157         {\r
158             // permissions were already granted before, we can call callback directly\r
159             androidRuntimePermissionsCallback (true, ptrToCallback);\r
160         }\r
161     }\r
162 \r
163     private native void androidRuntimePermissionsCallback (boolean permissionWasGranted, long ptrToCallback);\r
164 \r
165     @Override\r
166     public void onRequestPermissionsResult (int permissionID, String permissions[], int[] grantResults)\r
167     {\r
168         boolean permissionsGranted = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED);\r
169 \r
170         if (! permissionsGranted)\r
171             Log.d ("JUCE", "onRequestPermissionsResult: runtime permission was DENIED: " + getAndroidPermissionName (permissionID));\r
172 \r
173         Long ptrToCallback = permissionCallbackPtrMap.get (permissionID);\r
174         permissionCallbackPtrMap.remove (permissionID);\r
175         androidRuntimePermissionsCallback (permissionsGranted, ptrToCallback);\r
176     }\r
177 \r
178     //==============================================================================\r
179     public interface JuceMidiPort\r
180     {\r
181         boolean isInputPort();\r
182 \r
183         // start, stop does nothing on an output port\r
184         void start();\r
185         void stop();\r
186 \r
187         void close();\r
188 \r
189         // send will do nothing on an input port\r
190         void sendMidi (byte[] msg, int offset, int count);\r
191     }\r
192 \r
193     //==============================================================================\r
194     //==============================================================================\r
195     public class BluetoothManager extends ScanCallback\r
196     {\r
197         BluetoothManager()\r
198         {\r
199         }\r
200 \r
201         public String[] getMidiBluetoothAddresses()\r
202         {\r
203             return bluetoothMidiDevices.toArray (new String[bluetoothMidiDevices.size()]);\r
204         }\r
205 \r
206         public String getHumanReadableStringForBluetoothAddress (String address)\r
207         {\r
208             BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address);\r
209             return btDevice.getName();\r
210         }\r
211 \r
212         public int getBluetoothDeviceStatus (String address)\r
213         {\r
214             return getAndroidMidiDeviceManager().getBluetoothDeviceStatus (address);\r
215         }\r
216 \r
217         public void startStopScan (boolean shouldStart)\r
218         {\r
219             BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();\r
220 \r
221             if (bluetoothAdapter == null)\r
222             {\r
223                 Log.d ("JUCE", "BluetoothManager error: could not get default Bluetooth adapter");\r
224                 return;\r
225             }\r
226 \r
227             BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();\r
228 \r
229             if (bluetoothLeScanner == null)\r
230             {\r
231                 Log.d ("JUCE", "BluetoothManager error: could not get Bluetooth LE scanner");\r
232                 return;\r
233             }\r
234 \r
235             if (shouldStart)\r
236             {\r
237                 ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder();\r
238                 scanFilterBuilder.setServiceUuid (ParcelUuid.fromString (bluetoothLEMidiServiceUUID));\r
239 \r
240                 ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();\r
241                 scanSettingsBuilder.setCallbackType (ScanSettings.CALLBACK_TYPE_ALL_MATCHES)\r
242                                    .setScanMode (ScanSettings.SCAN_MODE_LOW_POWER)\r
243                                    .setScanMode (ScanSettings.MATCH_MODE_STICKY);\r
244 \r
245                 bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()),\r
246                                               scanSettingsBuilder.build(),\r
247                                               this);\r
248             }\r
249             else\r
250             {\r
251                 bluetoothLeScanner.stopScan (this);\r
252             }\r
253         }\r
254 \r
255         public boolean pairBluetoothMidiDevice(String address)\r
256         {\r
257             BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address);\r
258 \r
259             if (btDevice == null)\r
260             {\r
261                 Log.d ("JUCE", "failed to create buletooth device from address");\r
262                 return false;\r
263             }\r
264 \r
265             return getAndroidMidiDeviceManager().pairBluetoothDevice (btDevice);\r
266         }\r
267 \r
268         public void unpairBluetoothMidiDevice (String address)\r
269         {\r
270             getAndroidMidiDeviceManager().unpairBluetoothDevice (address);\r
271         }\r
272 \r
273         public void onScanFailed (int errorCode)\r
274         {\r
275         }\r
276 \r
277         public void onScanResult (int callbackType, ScanResult result)\r
278         {\r
279             if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES\r
280                  || callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH)\r
281             {\r
282                 BluetoothDevice device = result.getDevice();\r
283 \r
284                 if (device != null)\r
285                     bluetoothMidiDevices.add (device.getAddress());\r
286             }\r
287 \r
288             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST)\r
289             {\r
290                 Log.d ("JUCE", "ScanSettings.CALLBACK_TYPE_MATCH_LOST");\r
291                 BluetoothDevice device = result.getDevice();\r
292 \r
293                 if (device != null)\r
294                 {\r
295                     bluetoothMidiDevices.remove (device.getAddress());\r
296                     unpairBluetoothMidiDevice (device.getAddress());\r
297                 }\r
298             }\r
299         }\r
300 \r
301         public void onBatchScanResults (List<ScanResult> results)\r
302         {\r
303             for (ScanResult result : results)\r
304                 onScanResult (ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result);\r
305         }\r
306 \r
307         private BluetoothLeScanner scanner;\r
308         private static final String bluetoothLEMidiServiceUUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700";\r
309 \r
310         private HashSet<String> bluetoothMidiDevices = new HashSet<String>();\r
311     }\r
312 \r
313     public static class JuceMidiInputPort extends MidiReceiver implements JuceMidiPort\r
314     {\r
315         private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp);\r
316 \r
317         public JuceMidiInputPort (MidiDeviceManager mm, MidiOutputPort actualPort, MidiPortPath portPathToUse, long hostToUse)\r
318         {\r
319             owner = mm;\r
320             androidPort = actualPort;\r
321             portPath = portPathToUse;\r
322             juceHost = hostToUse;\r
323             isConnected = false;\r
324         }\r
325 \r
326         @Override\r
327         protected void finalize() throws Throwable\r
328         {\r
329             close();\r
330             super.finalize();\r
331         }\r
332 \r
333         @Override\r
334         public boolean isInputPort()\r
335         {\r
336             return true;\r
337         }\r
338 \r
339         @Override\r
340         public void start()\r
341         {\r
342             if (owner != null && androidPort != null && ! isConnected) {\r
343                 androidPort.connect(this);\r
344                 isConnected = true;\r
345             }\r
346         }\r
347 \r
348         @Override\r
349         public void stop()\r
350         {\r
351             if (owner != null && androidPort != null && isConnected) {\r
352                 androidPort.disconnect(this);\r
353                 isConnected = false;\r
354             }\r
355         }\r
356 \r
357         @Override\r
358         public void close()\r
359         {\r
360             if (androidPort != null) {\r
361                 try {\r
362                     androidPort.close();\r
363                 } catch (IOException exception) {\r
364                     Log.d("JUCE", "IO Exception while closing port");\r
365                 }\r
366             }\r
367 \r
368             if (owner != null)\r
369                 owner.removePort (portPath);\r
370 \r
371             owner = null;\r
372             androidPort = null;\r
373         }\r
374 \r
375         @Override\r
376         public void onSend (byte[] msg, int offset, int count, long timestamp)\r
377         {\r
378             if (count > 0)\r
379                 handleReceive (juceHost, msg, offset, count, timestamp);\r
380         }\r
381 \r
382         @Override\r
383         public void onFlush()\r
384         {}\r
385 \r
386         @Override\r
387         public void sendMidi (byte[] msg, int offset, int count)\r
388         {\r
389         }\r
390 \r
391         MidiDeviceManager owner;\r
392         MidiOutputPort androidPort;\r
393         MidiPortPath portPath;\r
394         long juceHost;\r
395         boolean isConnected;\r
396     }\r
397 \r
398     public static class JuceMidiOutputPort implements JuceMidiPort\r
399     {\r
400         public JuceMidiOutputPort (MidiDeviceManager mm, MidiInputPort actualPort, MidiPortPath portPathToUse)\r
401         {\r
402             owner = mm;\r
403             androidPort = actualPort;\r
404             portPath = portPathToUse;\r
405         }\r
406 \r
407         @Override\r
408         protected void finalize() throws Throwable\r
409         {\r
410             close();\r
411             super.finalize();\r
412         }\r
413 \r
414         @Override\r
415         public boolean isInputPort()\r
416         {\r
417             return false;\r
418         }\r
419 \r
420         @Override\r
421         public void start()\r
422         {\r
423         }\r
424 \r
425         @Override\r
426         public void stop()\r
427         {\r
428         }\r
429 \r
430         @Override\r
431         public void sendMidi (byte[] msg, int offset, int count)\r
432         {\r
433             if (androidPort != null)\r
434             {\r
435                 try {\r
436                     androidPort.send(msg, offset, count);\r
437                 } catch (IOException exception)\r
438                 {\r
439                     Log.d ("JUCE", "send midi had IO exception");\r
440                 }\r
441             }\r
442         }\r
443 \r
444         @Override\r
445         public void close()\r
446         {\r
447             if (androidPort != null) {\r
448                 try {\r
449                     androidPort.close();\r
450                 } catch (IOException exception) {\r
451                     Log.d("JUCE", "IO Exception while closing port");\r
452                 }\r
453             }\r
454 \r
455             if (owner != null)\r
456                 owner.removePort (portPath);\r
457 \r
458             owner = null;\r
459             androidPort = null;\r
460         }\r
461 \r
462         MidiDeviceManager owner;\r
463         MidiInputPort androidPort;\r
464         MidiPortPath portPath;\r
465     }\r
466 \r
467     private static class MidiPortPath extends Object\r
468     {\r
469         public MidiPortPath (int deviceIdToUse, boolean direction, int androidIndex)\r
470         {\r
471             deviceId = deviceIdToUse;\r
472             isInput = direction;\r
473             portIndex = androidIndex;\r
474 \r
475         }\r
476 \r
477         public int deviceId;\r
478         public int portIndex;\r
479         public boolean isInput;\r
480 \r
481         @Override\r
482         public int hashCode()\r
483         {\r
484             Integer i = new Integer ((deviceId * 128) + (portIndex < 128 ? portIndex : 127));\r
485             return i.hashCode() * (isInput ? -1 : 1);\r
486         }\r
487 \r
488         @Override\r
489         public boolean equals (Object obj)\r
490         {\r
491             if (obj == null)\r
492                 return false;\r
493 \r
494             if (getClass() != obj.getClass())\r
495                 return false;\r
496 \r
497             MidiPortPath other = (MidiPortPath) obj;\r
498             return (portIndex == other.portIndex && isInput == other.isInput && deviceId == other.deviceId);\r
499         }\r
500     }\r
501 \r
502     //==============================================================================\r
503     public class MidiDeviceManager extends MidiManager.DeviceCallback implements MidiManager.OnDeviceOpenedListener\r
504     {\r
505         //==============================================================================\r
506         private class DummyBluetoothGattCallback extends BluetoothGattCallback\r
507         {\r
508             public DummyBluetoothGattCallback (MidiDeviceManager mm)\r
509             {\r
510                 super();\r
511                 owner = mm;\r
512             }\r
513 \r
514             public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)\r
515             {\r
516                 if (newState == BluetoothProfile.STATE_CONNECTED)\r
517                 {\r
518                     gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);\r
519                     owner.pairBluetoothDeviceStepTwo (gatt.getDevice());\r
520                 }\r
521             }\r
522             public void onServicesDiscovered(BluetoothGatt gatt, int status) {}\r
523             public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}\r
524             public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}\r
525             public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}\r
526             public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {}\r
527             public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {}\r
528             public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {}\r
529             public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {}\r
530             public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {}\r
531 \r
532             private MidiDeviceManager owner;\r
533         }\r
534 \r
535         //==============================================================================\r
536         private class MidiDeviceOpenTask extends java.util.TimerTask\r
537         {\r
538             public MidiDeviceOpenTask (MidiDeviceManager deviceManager, MidiDevice device, BluetoothGatt gattToUse)\r
539             {\r
540                 owner = deviceManager;\r
541                 midiDevice = device;\r
542                 btGatt = gattToUse;\r
543             }\r
544 \r
545             @Override\r
546             public boolean cancel()\r
547             {\r
548                 synchronized (MidiDeviceOpenTask.class)\r
549                 {\r
550                     owner = null;\r
551                     boolean retval = super.cancel();\r
552 \r
553                     if (btGatt != null)\r
554                     {\r
555                         btGatt.disconnect();\r
556                         btGatt.close();\r
557 \r
558                         btGatt = null;\r
559                     }\r
560 \r
561                     if (midiDevice != null)\r
562                     {\r
563                         try\r
564                         {\r
565                             midiDevice.close();\r
566                         }\r
567                         catch (IOException e)\r
568                         {}\r
569 \r
570                         midiDevice = null;\r
571                     }\r
572 \r
573                     return retval;\r
574                 }\r
575             }\r
576 \r
577             public String getBluetoothAddress()\r
578             {\r
579                 synchronized (MidiDeviceOpenTask.class)\r
580                 {\r
581                     if (midiDevice != null)\r
582                     {\r
583                         MidiDeviceInfo info = midiDevice.getInfo();\r
584                         if (info.getType() == MidiDeviceInfo.TYPE_BLUETOOTH)\r
585                         {\r
586                             BluetoothDevice btDevice = (BluetoothDevice) info.getProperties().get (info.PROPERTY_BLUETOOTH_DEVICE);\r
587                             if (btDevice != null)\r
588                                 return btDevice.getAddress();\r
589                         }\r
590                     }\r
591                 }\r
592 \r
593                 return "";\r
594             }\r
595 \r
596             public BluetoothGatt getGatt() { return btGatt; }\r
597 \r
598             public int getID()\r
599             {\r
600                 return midiDevice.getInfo().getId();\r
601             }\r
602 \r
603             @Override\r
604             public void run()\r
605             {\r
606                 synchronized (MidiDeviceOpenTask.class)\r
607                 {\r
608                     if (owner != null && midiDevice != null)\r
609                         owner.onDeviceOpenedDelayed (midiDevice);\r
610                 }\r
611             }\r
612 \r
613             private MidiDeviceManager owner;\r
614             private MidiDevice midiDevice;\r
615             private BluetoothGatt btGatt;\r
616         }\r
617 \r
618         //==============================================================================\r
619         public MidiDeviceManager()\r
620         {\r
621             manager = (MidiManager) getSystemService (MIDI_SERVICE);\r
622 \r
623             if (manager == null)\r
624             {\r
625                 Log.d ("JUCE", "MidiDeviceManager error: could not get MidiManager system service");\r
626                 return;\r
627             }\r
628 \r
629             openPorts = new HashMap<MidiPortPath, WeakReference<JuceMidiPort>> ();\r
630             midiDevices = new ArrayList<Pair<MidiDevice,BluetoothGatt>>();\r
631             openTasks = new HashMap<Integer, MidiDeviceOpenTask>();\r
632             btDevicesPairing = new HashMap<String, BluetoothGatt>();\r
633 \r
634             MidiDeviceInfo[] foundDevices = manager.getDevices();\r
635             for (MidiDeviceInfo info : foundDevices)\r
636                 onDeviceAdded (info);\r
637 \r
638             manager.registerDeviceCallback (this, null);\r
639         }\r
640 \r
641         protected void finalize() throws Throwable\r
642         {\r
643             manager.unregisterDeviceCallback (this);\r
644 \r
645             synchronized (MidiDeviceManager.class)\r
646             {\r
647                 btDevicesPairing.clear();\r
648 \r
649                 for (Integer deviceID : openTasks.keySet())\r
650                     openTasks.get (deviceID).cancel();\r
651 \r
652                 openTasks = null;\r
653             }\r
654 \r
655             for (MidiPortPath key : openPorts.keySet())\r
656                 openPorts.get (key).get().close();\r
657 \r
658             openPorts = null;\r
659 \r
660             for (Pair<MidiDevice, BluetoothGatt> device : midiDevices)\r
661             {\r
662                 if (device.second != null)\r
663                 {\r
664                     device.second.disconnect();\r
665                     device.second.close();\r
666                 }\r
667 \r
668                 device.first.close();\r
669             }\r
670 \r
671             midiDevices.clear();\r
672 \r
673             super.finalize();\r
674         }\r
675 \r
676         public String[] getJuceAndroidMidiInputDevices()\r
677         {\r
678             return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_OUTPUT);\r
679         }\r
680 \r
681         public String[] getJuceAndroidMidiOutputDevices()\r
682         {\r
683             return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_INPUT);\r
684         }\r
685 \r
686         private String[] getJuceAndroidMidiDevices (int portType)\r
687         {\r
688             // only update the list when JUCE asks for a new list\r
689             synchronized (MidiDeviceManager.class)\r
690             {\r
691                 deviceInfos = getDeviceInfos();\r
692             }\r
693 \r
694             ArrayList<String> portNames = new ArrayList<String>();\r
695 \r
696             int index = 0;\r
697             for (MidiPortPath portInfo = getPortPathForJuceIndex (portType, index); portInfo != null; portInfo = getPortPathForJuceIndex (portType, ++index))\r
698                 portNames.add (getPortName (portInfo));\r
699 \r
700             String[] names = new String[portNames.size()];\r
701             return portNames.toArray (names);\r
702         }\r
703 \r
704         private JuceMidiPort openMidiPortWithJuceIndex (int index, long host, boolean isInput)\r
705         {\r
706             synchronized (MidiDeviceManager.class)\r
707             {\r
708                 int portTypeToFind = (isInput ? MidiDeviceInfo.PortInfo.TYPE_OUTPUT : MidiDeviceInfo.PortInfo.TYPE_INPUT);\r
709                 MidiPortPath portInfo = getPortPathForJuceIndex (portTypeToFind, index);\r
710 \r
711                 if (portInfo != null)\r
712                 {\r
713                     // ports must be opened exclusively!\r
714                     if (openPorts.containsKey (portInfo))\r
715                         return null;\r
716 \r
717                     Pair<MidiDevice,BluetoothGatt> devicePair = getMidiDevicePairForId (portInfo.deviceId);\r
718 \r
719                     if (devicePair != null)\r
720                     {\r
721                         MidiDevice device = devicePair.first;\r
722                         if (device != null)\r
723                         {\r
724                             JuceMidiPort juceMidiPort = null;\r
725 \r
726                             if (isInput)\r
727                             {\r
728                                 MidiOutputPort outputPort = device.openOutputPort(portInfo.portIndex);\r
729 \r
730                                 if (outputPort != null)\r
731                                     juceMidiPort = new JuceMidiInputPort(this, outputPort, portInfo, host);\r
732                             }\r
733                             else\r
734                             {\r
735                                 MidiInputPort inputPort = device.openInputPort(portInfo.portIndex);\r
736 \r
737                                 if (inputPort != null)\r
738                                     juceMidiPort = new JuceMidiOutputPort(this, inputPort, portInfo);\r
739                             }\r
740 \r
741                             if (juceMidiPort != null)\r
742                             {\r
743                                 openPorts.put(portInfo, new WeakReference<JuceMidiPort>(juceMidiPort));\r
744 \r
745                                 return juceMidiPort;\r
746                             }\r
747                         }\r
748                     }\r
749                 }\r
750             }\r
751 \r
752             return null;\r
753         }\r
754 \r
755         public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host)\r
756         {\r
757             return openMidiPortWithJuceIndex (index, host, true);\r
758         }\r
759 \r
760         public JuceMidiPort openMidiOutputPortWithJuceIndex (int index)\r
761         {\r
762             return openMidiPortWithJuceIndex (index, 0, false);\r
763         }\r
764 \r
765         /* 0: unpaired, 1: paired, 2: pairing */\r
766         public int getBluetoothDeviceStatus (String address)\r
767         {\r
768             synchronized (MidiDeviceManager.class)\r
769             {\r
770                 if (! address.isEmpty())\r
771                 {\r
772                     if (findMidiDeviceForBluetoothAddress (address) != null)\r
773                         return 1;\r
774 \r
775                     if (btDevicesPairing.containsKey (address))\r
776                         return 2;\r
777 \r
778                     if (findOpenTaskForBluetoothAddress (address) != null)\r
779                         return 2;\r
780                 }\r
781             }\r
782 \r
783             return 0;\r
784         }\r
785 \r
786         public boolean pairBluetoothDevice (BluetoothDevice btDevice)\r
787         {\r
788             String btAddress = btDevice.getAddress();\r
789             if (btAddress.isEmpty())\r
790                 return false;\r
791 \r
792             synchronized (MidiDeviceManager.class)\r
793             {\r
794                 if (getBluetoothDeviceStatus (btAddress) != 0)\r
795                     return false;\r
796 \r
797 \r
798                 btDevicesPairing.put (btDevice.getAddress(), null);\r
799                 BluetoothGatt gatt = btDevice.connectGatt (getApplicationContext(), true, new DummyBluetoothGattCallback (this));\r
800 \r
801                 if (gatt != null)\r
802                 {\r
803                     btDevicesPairing.put (btDevice.getAddress(), gatt);\r
804                 }\r
805                 else\r
806                 {\r
807                     pairBluetoothDeviceStepTwo (btDevice);\r
808                 }\r
809             }\r
810 \r
811             return true;\r
812         }\r
813 \r
814         public void pairBluetoothDeviceStepTwo (BluetoothDevice btDevice)\r
815         {\r
816             manager.openBluetoothDevice(btDevice, this, null);\r
817         }\r
818 \r
819         public void unpairBluetoothDevice (String address)\r
820         {\r
821             if (address.isEmpty())\r
822                 return;\r
823 \r
824             synchronized (MidiDeviceManager.class)\r
825             {\r
826                 if (btDevicesPairing.containsKey (address))\r
827                 {\r
828                     BluetoothGatt gatt = btDevicesPairing.get (address);\r
829                     if (gatt != null)\r
830                     {\r
831                         gatt.disconnect();\r
832                         gatt.close();\r
833                     }\r
834 \r
835                     btDevicesPairing.remove (address);\r
836                 }\r
837 \r
838                 MidiDeviceOpenTask openTask = findOpenTaskForBluetoothAddress (address);\r
839                 if (openTask != null)\r
840                 {\r
841                     int deviceID = openTask.getID();\r
842                     openTask.cancel();\r
843                     openTasks.remove (deviceID);\r
844                 }\r
845 \r
846                 Pair<MidiDevice, BluetoothGatt> midiDevicePair = findMidiDeviceForBluetoothAddress (address);\r
847                 if (midiDevicePair != null)\r
848                 {\r
849                     MidiDevice midiDevice = midiDevicePair.first;\r
850                     onDeviceRemoved (midiDevice.getInfo());\r
851 \r
852                     try {\r
853                         midiDevice.close();\r
854                     }\r
855                     catch (IOException exception)\r
856                     {\r
857                         Log.d ("JUCE", "IOException while closing midi device");\r
858                     }\r
859                 }\r
860             }\r
861         }\r
862 \r
863         private Pair<MidiDevice, BluetoothGatt> findMidiDeviceForBluetoothAddress (String address)\r
864         {\r
865             for (Pair<MidiDevice,BluetoothGatt> midiDevice : midiDevices)\r
866             {\r
867                 MidiDeviceInfo info = midiDevice.first.getInfo();\r
868                 if (info.getType() == MidiDeviceInfo.TYPE_BLUETOOTH)\r
869                 {\r
870                     BluetoothDevice btDevice = (BluetoothDevice) info.getProperties().get (info.PROPERTY_BLUETOOTH_DEVICE);\r
871                     if (btDevice != null && btDevice.getAddress().equals (address))\r
872                         return midiDevice;\r
873                 }\r
874             }\r
875 \r
876             return null;\r
877         }\r
878 \r
879         private MidiDeviceOpenTask findOpenTaskForBluetoothAddress (String address)\r
880         {\r
881             for (Integer deviceID : openTasks.keySet())\r
882             {\r
883                 MidiDeviceOpenTask openTask = openTasks.get (deviceID);\r
884                 if (openTask.getBluetoothAddress().equals (address))\r
885                     return openTask;\r
886             }\r
887 \r
888             return null;\r
889         }\r
890 \r
891         public void removePort (MidiPortPath path)\r
892         {\r
893             openPorts.remove (path);\r
894         }\r
895 \r
896         public String getInputPortNameForJuceIndex (int index)\r
897         {\r
898             MidiPortPath portInfo = getPortPathForJuceIndex (MidiDeviceInfo.PortInfo.TYPE_OUTPUT, index);\r
899             if (portInfo != null)\r
900                 return getPortName (portInfo);\r
901 \r
902             return "";\r
903         }\r
904 \r
905         public String getOutputPortNameForJuceIndex (int index)\r
906         {\r
907             MidiPortPath portInfo = getPortPathForJuceIndex (MidiDeviceInfo.PortInfo.TYPE_INPUT, index);\r
908             if (portInfo != null)\r
909                 return getPortName (portInfo);\r
910 \r
911             return "";\r
912         }\r
913 \r
914         public void onDeviceAdded (MidiDeviceInfo info)\r
915         {\r
916             // only add standard midi devices\r
917             if (info.getType() == info.TYPE_BLUETOOTH)\r
918                 return;\r
919 \r
920             manager.openDevice (info, this, null);\r
921         }\r
922 \r
923         public void onDeviceRemoved (MidiDeviceInfo info)\r
924         {\r
925             synchronized (MidiDeviceManager.class)\r
926             {\r
927                 Pair<MidiDevice, BluetoothGatt> devicePair = getMidiDevicePairForId (info.getId());\r
928 \r
929                 if (devicePair != null)\r
930                 {\r
931                     MidiDevice midiDevice = devicePair.first;\r
932                     BluetoothGatt gatt = devicePair.second;\r
933 \r
934                     // close all ports that use this device\r
935                     boolean removedPort = true;\r
936 \r
937                     while (removedPort == true)\r
938                     {\r
939                         removedPort = false;\r
940                         for (MidiPortPath key : openPorts.keySet())\r
941                         {\r
942                             if (key.deviceId == info.getId())\r
943                             {\r
944                                 openPorts.get(key).get().close();\r
945                                 removedPort = true;\r
946                                 break;\r
947                             }\r
948                         }\r
949                     }\r
950 \r
951                     if (gatt != null)\r
952                     {\r
953                         gatt.disconnect();\r
954                         gatt.close();\r
955                     }\r
956 \r
957                     midiDevices.remove (devicePair);\r
958                 }\r
959             }\r
960         }\r
961 \r
962         public void onDeviceStatusChanged (MidiDeviceStatus status)\r
963         {\r
964         }\r
965 \r
966         @Override\r
967         public void onDeviceOpened (MidiDevice theDevice)\r
968         {\r
969             synchronized (MidiDeviceManager.class)\r
970             {\r
971                 MidiDeviceInfo info = theDevice.getInfo();\r
972                 int deviceID = info.getId();\r
973                 BluetoothGatt gatt = null;\r
974                 boolean isBluetooth = false;\r
975 \r
976                 if (! openTasks.containsKey (deviceID))\r
977                 {\r
978                     if (info.getType() == MidiDeviceInfo.TYPE_BLUETOOTH)\r
979                     {\r
980                         isBluetooth = true;\r
981                         BluetoothDevice btDevice = (BluetoothDevice) info.getProperties().get (info.PROPERTY_BLUETOOTH_DEVICE);\r
982                         if (btDevice != null)\r
983                         {\r
984                             String btAddress = btDevice.getAddress();\r
985                             if (btDevicesPairing.containsKey (btAddress))\r
986                             {\r
987                                 gatt = btDevicesPairing.get (btAddress);\r
988                                 btDevicesPairing.remove (btAddress);\r
989                             }\r
990                             else\r
991                             {\r
992                                 // unpair was called in the mean time\r
993                                 try\r
994                                 {\r
995                                     Pair<MidiDevice, BluetoothGatt> midiDevicePair = findMidiDeviceForBluetoothAddress (btDevice.getAddress());\r
996                                     if (midiDevicePair != null)\r
997                                     {\r
998                                         gatt = midiDevicePair.second;\r
999 \r
1000                                         if (gatt != null)\r
1001                                         {\r
1002                                             gatt.disconnect();\r
1003                                             gatt.close();\r
1004                                         }\r
1005                                     }\r
1006 \r
1007                                     theDevice.close();\r
1008                                 }\r
1009                                 catch (IOException e)\r
1010                                 {}\r
1011 \r
1012                                 return;\r
1013                             }\r
1014                         }\r
1015                     }\r
1016 \r
1017                     MidiDeviceOpenTask openTask = new MidiDeviceOpenTask (this, theDevice, gatt);\r
1018                     openTasks.put (deviceID, openTask);\r
1019 \r
1020                     new java.util.Timer().schedule (openTask, (isBluetooth ? 2000 : 100));\r
1021                 }\r
1022             }\r
1023         }\r
1024 \r
1025         public void onDeviceOpenedDelayed (MidiDevice theDevice)\r
1026         {\r
1027             synchronized (MidiDeviceManager.class)\r
1028             {\r
1029                 int deviceID = theDevice.getInfo().getId();\r
1030 \r
1031                 if (openTasks.containsKey (deviceID))\r
1032                 {\r
1033                     if (! midiDevices.contains(theDevice))\r
1034                     {\r
1035                         BluetoothGatt gatt = openTasks.get (deviceID).getGatt();\r
1036                         openTasks.remove (deviceID);\r
1037                         midiDevices.add (new Pair<MidiDevice,BluetoothGatt> (theDevice, gatt));\r
1038                     }\r
1039                 }\r
1040                 else\r
1041                 {\r
1042                     // unpair was called in the mean time\r
1043                     MidiDeviceInfo info = theDevice.getInfo();\r
1044                     BluetoothDevice btDevice = (BluetoothDevice) info.getProperties().get (info.PROPERTY_BLUETOOTH_DEVICE);\r
1045                     if (btDevice != null)\r
1046                     {\r
1047                         String btAddress = btDevice.getAddress();\r
1048                         Pair<MidiDevice, BluetoothGatt> midiDevicePair = findMidiDeviceForBluetoothAddress (btDevice.getAddress());\r
1049                         if (midiDevicePair != null)\r
1050                         {\r
1051                             BluetoothGatt gatt = midiDevicePair.second;\r
1052 \r
1053                             if (gatt != null)\r
1054                             {\r
1055                                 gatt.disconnect();\r
1056                                 gatt.close();\r
1057                             }\r
1058                         }\r
1059                     }\r
1060 \r
1061                     try\r
1062                     {\r
1063                         theDevice.close();\r
1064                     }\r
1065                     catch (IOException e)\r
1066                     {}\r
1067                 }\r
1068             }\r
1069         }\r
1070 \r
1071         public String getPortName(MidiPortPath path)\r
1072         {\r
1073             int portTypeToFind = (path.isInput ? MidiDeviceInfo.PortInfo.TYPE_INPUT : MidiDeviceInfo.PortInfo.TYPE_OUTPUT);\r
1074 \r
1075             synchronized (MidiDeviceManager.class)\r
1076             {\r
1077                 for (MidiDeviceInfo info : deviceInfos)\r
1078                 {\r
1079                     int localIndex = 0;\r
1080                     if (info.getId() == path.deviceId)\r
1081                     {\r
1082                         for (MidiDeviceInfo.PortInfo portInfo : info.getPorts())\r
1083                         {\r
1084                             int portType = portInfo.getType();\r
1085                             if (portType == portTypeToFind)\r
1086                             {\r
1087                                 int portIndex = portInfo.getPortNumber();\r
1088                                 if (portIndex == path.portIndex)\r
1089                                 {\r
1090                                     String portName = portInfo.getName();\r
1091                                     if (portName.isEmpty())\r
1092                                         portName = (String) info.getProperties().get(info.PROPERTY_NAME);\r
1093 \r
1094                                     return portName;\r
1095                                 }\r
1096                             }\r
1097                         }\r
1098                     }\r
1099                 }\r
1100             }\r
1101 \r
1102             return "";\r
1103         }\r
1104 \r
1105         public MidiPortPath getPortPathForJuceIndex (int portType, int juceIndex)\r
1106         {\r
1107             int portIdx = 0;\r
1108             for (MidiDeviceInfo info : deviceInfos)\r
1109             {\r
1110                 for (MidiDeviceInfo.PortInfo portInfo : info.getPorts())\r
1111                 {\r
1112                     if (portInfo.getType() == portType)\r
1113                     {\r
1114                         if (portIdx == juceIndex)\r
1115                             return new MidiPortPath (info.getId(),\r
1116                                     (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT),\r
1117                                     portInfo.getPortNumber());\r
1118 \r
1119                         portIdx++;\r
1120                     }\r
1121                 }\r
1122             }\r
1123 \r
1124             return null;\r
1125         }\r
1126 \r
1127         private MidiDeviceInfo[] getDeviceInfos()\r
1128         {\r
1129             synchronized (MidiDeviceManager.class)\r
1130             {\r
1131                 MidiDeviceInfo[] infos = new MidiDeviceInfo[midiDevices.size()];\r
1132 \r
1133                 int idx = 0;\r
1134                 for (Pair<MidiDevice,BluetoothGatt> midiDevice : midiDevices)\r
1135                     infos[idx++] = midiDevice.first.getInfo();\r
1136 \r
1137                 return infos;\r
1138             }\r
1139         }\r
1140 \r
1141         private Pair<MidiDevice, BluetoothGatt> getMidiDevicePairForId (int deviceId)\r
1142         {\r
1143             synchronized (MidiDeviceManager.class)\r
1144             {\r
1145                 for (Pair<MidiDevice,BluetoothGatt> midiDevice : midiDevices)\r
1146                     if (midiDevice.first.getInfo().getId() == deviceId)\r
1147                         return midiDevice;\r
1148             }\r
1149 \r
1150             return null;\r
1151         }\r
1152 \r
1153         private MidiManager manager;\r
1154         private HashMap<String, BluetoothGatt> btDevicesPairing;\r
1155         private HashMap<Integer, MidiDeviceOpenTask> openTasks;\r
1156         private ArrayList<Pair<MidiDevice, BluetoothGatt>> midiDevices;\r
1157         private MidiDeviceInfo[] deviceInfos;\r
1158         private HashMap<MidiPortPath, WeakReference<JuceMidiPort>> openPorts;\r
1159     }\r
1160 \r
1161     public MidiDeviceManager getAndroidMidiDeviceManager()\r
1162     {\r
1163         if (getSystemService (MIDI_SERVICE) == null)\r
1164             return null;\r
1165 \r
1166         synchronized (AudioPerformanceTest.class)\r
1167         {\r
1168             if (midiDeviceManager == null)\r
1169                 midiDeviceManager = new MidiDeviceManager();\r
1170         }\r
1171 \r
1172         return midiDeviceManager;\r
1173     }\r
1174 \r
1175     public BluetoothManager getAndroidBluetoothManager()\r
1176     {\r
1177         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();\r
1178 \r
1179         if (adapter == null)\r
1180             return null;\r
1181 \r
1182         if (adapter.getBluetoothLeScanner() == null)\r
1183             return null;\r
1184 \r
1185         synchronized (AudioPerformanceTest.class)\r
1186         {\r
1187             if (bluetoothManager == null)\r
1188                 bluetoothManager = new BluetoothManager();\r
1189         }\r
1190 \r
1191         return bluetoothManager;\r
1192     }\r
1193 \r
1194     //==============================================================================\r
1195     @Override\r
1196     public void onCreate (Bundle savedInstanceState)\r
1197     {\r
1198         super.onCreate (savedInstanceState);\r
1199 \r
1200         isScreenSaverEnabled = true;\r
1201         hideActionBar();\r
1202         viewHolder = new ViewHolder (this);\r
1203         setContentView (viewHolder);\r
1204 \r
1205         setVolumeControlStream (AudioManager.STREAM_MUSIC);\r
1206 \r
1207         permissionCallbackPtrMap = new HashMap<Integer, Long>();\r
1208     }\r
1209 \r
1210     @Override\r
1211     protected void onDestroy()\r
1212     {\r
1213         quitApp();\r
1214         super.onDestroy();\r
1215 \r
1216         clearDataCache();\r
1217     }\r
1218 \r
1219     @Override\r
1220     protected void onPause()\r
1221     {\r
1222         suspendApp();\r
1223 \r
1224         try\r
1225         {\r
1226             Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down\r
1227                                  // openGL glitches when pausing/resuming apps..\r
1228         } catch (InterruptedException e) {}\r
1229 \r
1230         super.onPause();\r
1231     }\r
1232 \r
1233     @Override\r
1234     protected void onResume()\r
1235     {\r
1236         super.onResume();\r
1237         resumeApp();\r
1238 \r
1239         // Ensure that navigation/status bar visibility is correctly restored.\r
1240         for (int i = 0; i < viewHolder.getChildCount(); ++i)\r
1241             ((ComponentPeerView) viewHolder.getChildAt (i)).appResumed();\r
1242     }\r
1243 \r
1244     @Override\r
1245     public void onConfigurationChanged (Configuration cfg)\r
1246     {\r
1247         super.onConfigurationChanged (cfg);\r
1248         setContentView (viewHolder);\r
1249     }\r
1250 \r
1251     private void callAppLauncher()\r
1252     {\r
1253         launchApp (getApplicationInfo().publicSourceDir,\r
1254                    getApplicationInfo().dataDir);\r
1255     }\r
1256 \r
1257     // Need to override this as the default implementation always finishes the activity.\r
1258     @Override\r
1259     public void onBackPressed()\r
1260     {\r
1261         ComponentPeerView focusedView = getViewWithFocusOrDefaultView();\r
1262 \r
1263         if (focusedView == null)\r
1264             return;\r
1265 \r
1266         focusedView.backButtonPressed();\r
1267     }\r
1268 \r
1269     private ComponentPeerView getViewWithFocusOrDefaultView()\r
1270     {\r
1271         for (int i = 0; i < viewHolder.getChildCount(); ++i)\r
1272         {\r
1273             if (viewHolder.getChildAt (i).hasFocus())\r
1274                 return (ComponentPeerView) viewHolder.getChildAt (i);\r
1275         }\r
1276 \r
1277         if (viewHolder.getChildCount() > 0)\r
1278             return (ComponentPeerView) viewHolder.getChildAt (0);\r
1279 \r
1280         return null;\r
1281     }\r
1282 \r
1283     //==============================================================================\r
1284     private void hideActionBar()\r
1285     {\r
1286         // get "getActionBar" method\r
1287         java.lang.reflect.Method getActionBarMethod = null;\r
1288         try\r
1289         {\r
1290             getActionBarMethod = this.getClass().getMethod ("getActionBar");\r
1291         }\r
1292         catch (SecurityException e)     { return; }\r
1293         catch (NoSuchMethodException e) { return; }\r
1294         if (getActionBarMethod == null) return;\r
1295 \r
1296         // invoke "getActionBar" method\r
1297         Object actionBar = null;\r
1298         try\r
1299         {\r
1300             actionBar = getActionBarMethod.invoke (this);\r
1301         }\r
1302         catch (java.lang.IllegalArgumentException e) { return; }\r
1303         catch (java.lang.IllegalAccessException e) { return; }\r
1304         catch (java.lang.reflect.InvocationTargetException e) { return; }\r
1305         if (actionBar == null) return;\r
1306 \r
1307         // get "hide" method\r
1308         java.lang.reflect.Method actionBarHideMethod = null;\r
1309         try\r
1310         {\r
1311             actionBarHideMethod = actionBar.getClass().getMethod ("hide");\r
1312         }\r
1313         catch (SecurityException e)     { return; }\r
1314         catch (NoSuchMethodException e) { return; }\r
1315         if (actionBarHideMethod == null) return;\r
1316 \r
1317         // invoke "hide" method\r
1318         try\r
1319         {\r
1320             actionBarHideMethod.invoke (actionBar);\r
1321         }\r
1322         catch (java.lang.IllegalArgumentException e) {}\r
1323         catch (java.lang.IllegalAccessException e) {}\r
1324         catch (java.lang.reflect.InvocationTargetException e) {}\r
1325     }\r
1326 \r
1327     void requestPermissionsCompat (String[] permissions, int requestCode)\r
1328     {\r
1329         Method requestPermissionsMethod = null;\r
1330         try\r
1331         {\r
1332             requestPermissionsMethod = this.getClass().getMethod ("requestPermissions",\r
1333                                                                   String[].class, int.class);\r
1334         }\r
1335         catch (SecurityException e)     { return; }\r
1336         catch (NoSuchMethodException e) { return; }\r
1337         if (requestPermissionsMethod == null) return;\r
1338 \r
1339         try\r
1340         {\r
1341             requestPermissionsMethod.invoke (this, permissions, requestCode);\r
1342         }\r
1343         catch (java.lang.IllegalArgumentException e) {}\r
1344         catch (java.lang.IllegalAccessException e) {}\r
1345         catch (java.lang.reflect.InvocationTargetException e) {}\r
1346     }\r
1347 \r
1348     //==============================================================================\r
1349     private native void launchApp (String appFile, String appDataDir);\r
1350     private native void quitApp();\r
1351     private native void suspendApp();\r
1352     private native void resumeApp();\r
1353     private native void setScreenSize (int screenWidth, int screenHeight, int dpi);\r
1354     private native void appActivityResult (int requestCode, int resultCode, Intent data);\r
1355     private native void appNewIntent (Intent intent);\r
1356 \r
1357     //==============================================================================\r
1358     private ViewHolder viewHolder;\r
1359     private MidiDeviceManager midiDeviceManager = null;\r
1360     private BluetoothManager bluetoothManager = null;\r
1361     private boolean isScreenSaverEnabled;\r
1362     private java.util.Timer keepAliveTimer;\r
1363 \r
1364     public final ComponentPeerView createNewView (boolean opaque, long host)\r
1365     {\r
1366         ComponentPeerView v = new ComponentPeerView (this, opaque, host);\r
1367         viewHolder.addView (v);\r
1368         return v;\r
1369     }\r
1370 \r
1371     public final void deleteView (ComponentPeerView view)\r
1372     {\r
1373         view.host = 0;\r
1374 \r
1375         ViewGroup group = (ViewGroup) (view.getParent());\r
1376 \r
1377         if (group != null)\r
1378             group.removeView (view);\r
1379     }\r
1380 \r
1381     public final void deleteNativeSurfaceView (NativeSurfaceView view)\r
1382     {\r
1383         ViewGroup group = (ViewGroup) (view.getParent());\r
1384 \r
1385         if (group != null)\r
1386             group.removeView (view);\r
1387     }\r
1388 \r
1389     final class ViewHolder  extends ViewGroup\r
1390     {\r
1391         public ViewHolder (Context context)\r
1392         {\r
1393             super (context);\r
1394             setDescendantFocusability (ViewGroup.FOCUS_AFTER_DESCENDANTS);\r
1395             setFocusable (false);\r
1396         }\r
1397 \r
1398         protected final void onLayout (boolean changed, int left, int top, int right, int bottom)\r
1399         {\r
1400             setScreenSize (getWidth(), getHeight(), getDPI());\r
1401 \r
1402             if (isFirstResize)\r
1403             {\r
1404                 isFirstResize = false;\r
1405                 callAppLauncher();\r
1406             }\r
1407         }\r
1408 \r
1409         private final int getDPI()\r
1410         {\r
1411             DisplayMetrics metrics = new DisplayMetrics();\r
1412             getWindowManager().getDefaultDisplay().getMetrics (metrics);\r
1413             return metrics.densityDpi;\r
1414         }\r
1415 \r
1416         private boolean isFirstResize = true;\r
1417     }\r
1418 \r
1419     public final void excludeClipRegion (android.graphics.Canvas canvas, float left, float top, float right, float bottom)\r
1420     {\r
1421         canvas.clipRect (left, top, right, bottom, android.graphics.Region.Op.DIFFERENCE);\r
1422     }\r
1423 \r
1424     //==============================================================================\r
1425     public final void setScreenSaver (boolean enabled)\r
1426     {\r
1427         if (isScreenSaverEnabled != enabled)\r
1428         {\r
1429             isScreenSaverEnabled = enabled;\r
1430 \r
1431             if (keepAliveTimer != null)\r
1432             {\r
1433                 keepAliveTimer.cancel();\r
1434                 keepAliveTimer = null;\r
1435             }\r
1436 \r
1437             if (enabled)\r
1438             {\r
1439                 getWindow().clearFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\r
1440             }\r
1441             else\r
1442             {\r
1443                 getWindow().addFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\r
1444 \r
1445                 // If no user input is received after about 3 seconds, the OS will lower the\r
1446                 // task's priority, so this timer forces it to be kept active.\r
1447                 keepAliveTimer = new java.util.Timer();\r
1448 \r
1449                 keepAliveTimer.scheduleAtFixedRate (new TimerTask()\r
1450                 {\r
1451                     @Override\r
1452                     public void run()\r
1453                     {\r
1454                         android.app.Instrumentation instrumentation = new android.app.Instrumentation();\r
1455 \r
1456                         try\r
1457                         {\r
1458                             instrumentation.sendKeyDownUpSync (KeyEvent.KEYCODE_UNKNOWN);\r
1459                         }\r
1460                         catch (Exception e)\r
1461                         {\r
1462                         }\r
1463                     }\r
1464                 }, 2000, 2000);\r
1465             }\r
1466         }\r
1467     }\r
1468 \r
1469     public final boolean getScreenSaver()\r
1470     {\r
1471         return isScreenSaverEnabled;\r
1472     }\r
1473 \r
1474     //==============================================================================\r
1475     public final String getClipboardContent()\r
1476     {\r
1477         ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE);\r
1478         return clipboard.getText().toString();\r
1479     }\r
1480 \r
1481     public final void setClipboardContent (String newText)\r
1482     {\r
1483         ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE);\r
1484         clipboard.setText (newText);\r
1485     }\r
1486 \r
1487     //==============================================================================\r
1488     public final void showMessageBox (String title, String message, final long callback)\r
1489     {\r
1490         AlertDialog.Builder builder = new AlertDialog.Builder (this);\r
1491         builder.setTitle (title)\r
1492                .setMessage (message)\r
1493                .setCancelable (true)\r
1494                .setOnCancelListener (new DialogInterface.OnCancelListener()\r
1495                     {\r
1496                         public void onCancel (DialogInterface dialog)\r
1497                         {\r
1498                             AudioPerformanceTest.this.alertDismissed (callback, 0);\r
1499                         }\r
1500                     })\r
1501                .setPositiveButton ("OK", new DialogInterface.OnClickListener()\r
1502                     {\r
1503                         public void onClick (DialogInterface dialog, int id)\r
1504                         {\r
1505                             dialog.dismiss();\r
1506                             AudioPerformanceTest.this.alertDismissed (callback, 0);\r
1507                         }\r
1508                     });\r
1509 \r
1510         builder.create().show();\r
1511     }\r
1512 \r
1513     public final void showOkCancelBox (String title, String message, final long callback,\r
1514                                        String okButtonText, String cancelButtonText)\r
1515     {\r
1516         AlertDialog.Builder builder = new AlertDialog.Builder (this);\r
1517         builder.setTitle (title)\r
1518                .setMessage (message)\r
1519                .setCancelable (true)\r
1520                .setOnCancelListener (new DialogInterface.OnCancelListener()\r
1521                     {\r
1522                         public void onCancel (DialogInterface dialog)\r
1523                         {\r
1524                             AudioPerformanceTest.this.alertDismissed (callback, 0);\r
1525                         }\r
1526                     })\r
1527                .setPositiveButton (okButtonText.isEmpty() ? "OK" : okButtonText, new DialogInterface.OnClickListener()\r
1528                     {\r
1529                         public void onClick (DialogInterface dialog, int id)\r
1530                         {\r
1531                             dialog.dismiss();\r
1532                             AudioPerformanceTest.this.alertDismissed (callback, 1);\r
1533                         }\r
1534                     })\r
1535                .setNegativeButton (cancelButtonText.isEmpty() ? "Cancel" : cancelButtonText, new DialogInterface.OnClickListener()\r
1536                     {\r
1537                         public void onClick (DialogInterface dialog, int id)\r
1538                         {\r
1539                             dialog.dismiss();\r
1540                             AudioPerformanceTest.this.alertDismissed (callback, 0);\r
1541                         }\r
1542                     });\r
1543 \r
1544         builder.create().show();\r
1545     }\r
1546 \r
1547     public final void showYesNoCancelBox (String title, String message, final long callback)\r
1548     {\r
1549         AlertDialog.Builder builder = new AlertDialog.Builder (this);\r
1550         builder.setTitle (title)\r
1551                .setMessage (message)\r
1552                .setCancelable (true)\r
1553                .setOnCancelListener (new DialogInterface.OnCancelListener()\r
1554                     {\r
1555                         public void onCancel (DialogInterface dialog)\r
1556                         {\r
1557                             AudioPerformanceTest.this.alertDismissed (callback, 0);\r
1558                         }\r
1559                     })\r
1560                .setPositiveButton ("Yes", new DialogInterface.OnClickListener()\r
1561                     {\r
1562                         public void onClick (DialogInterface dialog, int id)\r
1563                         {\r
1564                             dialog.dismiss();\r
1565                             AudioPerformanceTest.this.alertDismissed (callback, 1);\r
1566                         }\r
1567                     })\r
1568                .setNegativeButton ("No", new DialogInterface.OnClickListener()\r
1569                     {\r
1570                         public void onClick (DialogInterface dialog, int id)\r
1571                         {\r
1572                             dialog.dismiss();\r
1573                             AudioPerformanceTest.this.alertDismissed (callback, 2);\r
1574                         }\r
1575                     })\r
1576                .setNeutralButton ("Cancel", new DialogInterface.OnClickListener()\r
1577                     {\r
1578                         public void onClick (DialogInterface dialog, int id)\r
1579                         {\r
1580                             dialog.dismiss();\r
1581                             AudioPerformanceTest.this.alertDismissed (callback, 0);\r
1582                         }\r
1583                     });\r
1584 \r
1585         builder.create().show();\r
1586     }\r
1587 \r
1588     public native void alertDismissed (long callback, int id);\r
1589 \r
1590     //==============================================================================\r
1591     public final class ComponentPeerView extends ViewGroup\r
1592                                          implements View.OnFocusChangeListener\r
1593     {\r
1594         public ComponentPeerView (Context context, boolean opaque_, long host)\r
1595         {\r
1596             super (context);\r
1597             this.host = host;\r
1598             setWillNotDraw (false);\r
1599             opaque = opaque_;\r
1600 \r
1601             setFocusable (true);\r
1602             setFocusableInTouchMode (true);\r
1603             setOnFocusChangeListener (this);\r
1604 \r
1605             // swap red and blue colours to match internal opengl texture format\r
1606             ColorMatrix colorMatrix = new ColorMatrix();\r
1607 \r
1608             float[] colorTransform = { 0,    0,    1.0f, 0,    0,\r
1609                                        0,    1.0f, 0,    0,    0,\r
1610                                        1.0f, 0,    0,    0,    0,\r
1611                                        0,    0,    0,    1.0f, 0 };\r
1612 \r
1613             colorMatrix.set (colorTransform);\r
1614             paint.setColorFilter (new ColorMatrixColorFilter (colorMatrix));\r
1615 \r
1616             java.lang.reflect.Method method = null;\r
1617 \r
1618             try\r
1619             {\r
1620                 method = getClass().getMethod ("setLayerType", int.class, Paint.class);\r
1621             }\r
1622             catch (SecurityException e)     {}\r
1623             catch (NoSuchMethodException e) {}\r
1624 \r
1625             if (method != null)\r
1626             {\r
1627                 try\r
1628                 {\r
1629                     int layerTypeNone = 0;\r
1630                     method.invoke (this, layerTypeNone, null);\r
1631                 }\r
1632                 catch (java.lang.IllegalArgumentException e) {}\r
1633                 catch (java.lang.IllegalAccessException e) {}\r
1634                 catch (java.lang.reflect.InvocationTargetException e) {}\r
1635             }\r
1636         }\r
1637 \r
1638         //==============================================================================\r
1639         private native void handlePaint (long host, Canvas canvas, Paint paint);\r
1640 \r
1641         @Override\r
1642         public void onDraw (Canvas canvas)\r
1643         {\r
1644             if (host == 0)\r
1645                 return;\r
1646 \r
1647             handlePaint (host, canvas, paint);\r
1648         }\r
1649 \r
1650         @Override\r
1651         public boolean isOpaque()\r
1652         {\r
1653             return opaque;\r
1654         }\r
1655 \r
1656         private boolean opaque;\r
1657         private long host;\r
1658         private Paint paint = new Paint();\r
1659 \r
1660         //==============================================================================\r
1661         private native void handleMouseDown (long host, int index, float x, float y, long time);\r
1662         private native void handleMouseDrag (long host, int index, float x, float y, long time);\r
1663         private native void handleMouseUp   (long host, int index, float x, float y, long time);\r
1664 \r
1665         @Override\r
1666         public boolean onTouchEvent (MotionEvent event)\r
1667         {\r
1668             if (host == 0)\r
1669                 return false;\r
1670 \r
1671             int action = event.getAction();\r
1672             long time = event.getEventTime();\r
1673 \r
1674             switch (action & MotionEvent.ACTION_MASK)\r
1675             {\r
1676                 case MotionEvent.ACTION_DOWN:\r
1677                     handleMouseDown (host, event.getPointerId(0), event.getX(), event.getY(), time);\r
1678                     return true;\r
1679 \r
1680                 case MotionEvent.ACTION_CANCEL:\r
1681                 case MotionEvent.ACTION_UP:\r
1682                     handleMouseUp (host, event.getPointerId(0), event.getX(), event.getY(), time);\r
1683                     return true;\r
1684 \r
1685                 case MotionEvent.ACTION_MOVE:\r
1686                 {\r
1687                     int n = event.getPointerCount();\r
1688                     for (int i = 0; i < n; ++i)\r
1689                         handleMouseDrag (host, event.getPointerId(i), event.getX(i), event.getY(i), time);\r
1690 \r
1691                     return true;\r
1692                 }\r
1693 \r
1694                 case MotionEvent.ACTION_POINTER_UP:\r
1695                 {\r
1696                     int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;\r
1697                     handleMouseUp (host, event.getPointerId(i), event.getX(i), event.getY(i), time);\r
1698                     return true;\r
1699                 }\r
1700 \r
1701                 case MotionEvent.ACTION_POINTER_DOWN:\r
1702                 {\r
1703                     int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;\r
1704                     handleMouseDown (host, event.getPointerId(i), event.getX(i), event.getY(i), time);\r
1705                     return true;\r
1706                 }\r
1707 \r
1708                 default:\r
1709                     break;\r
1710             }\r
1711 \r
1712             return false;\r
1713         }\r
1714 \r
1715         //==============================================================================\r
1716         private native void handleKeyDown (long host, int keycode, int textchar);\r
1717         private native void handleKeyUp (long host, int keycode, int textchar);\r
1718         private native void handleBackButton (long host);\r
1719         private native void handleKeyboardHidden (long host);\r
1720 \r
1721         public void showKeyboard (String type)\r
1722         {\r
1723             InputMethodManager imm = (InputMethodManager) getSystemService (Context.INPUT_METHOD_SERVICE);\r
1724 \r
1725             if (imm != null)\r
1726             {\r
1727                 if (type.length() > 0)\r
1728                 {\r
1729                     imm.showSoftInput (this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT);\r
1730                     imm.setInputMethod (getWindowToken(), type);\r
1731                     keyboardDismissListener.startListening();\r
1732                 }\r
1733                 else\r
1734                 {\r
1735                     imm.hideSoftInputFromWindow (getWindowToken(), 0);\r
1736                     keyboardDismissListener.stopListening();\r
1737                 }\r
1738             }\r
1739         }\r
1740 \r
1741         public void backButtonPressed()\r
1742         {\r
1743             if (host == 0)\r
1744                 return;\r
1745 \r
1746             handleBackButton (host);\r
1747         }\r
1748 \r
1749         @Override\r
1750         public boolean onKeyDown (int keyCode, KeyEvent event)\r
1751         {\r
1752             if (host == 0)\r
1753                 return false;\r
1754 \r
1755             switch (keyCode)\r
1756             {\r
1757                 case KeyEvent.KEYCODE_VOLUME_UP:\r
1758                 case KeyEvent.KEYCODE_VOLUME_DOWN:\r
1759                     return super.onKeyDown (keyCode, event);\r
1760                 case KeyEvent.KEYCODE_BACK:\r
1761                 {\r
1762                     ((Activity) getContext()).onBackPressed();\r
1763                     return true;\r
1764                 }\r
1765 \r
1766                 default:\r
1767                     break;\r
1768             }\r
1769 \r
1770             handleKeyDown (host, keyCode, event.getUnicodeChar());\r
1771             return true;\r
1772         }\r
1773 \r
1774         @Override\r
1775         public boolean onKeyUp (int keyCode, KeyEvent event)\r
1776         {\r
1777             if (host == 0)\r
1778                 return false;\r
1779 \r
1780             handleKeyUp (host, keyCode, event.getUnicodeChar());\r
1781             return true;\r
1782         }\r
1783 \r
1784         @Override\r
1785         public boolean onKeyMultiple (int keyCode, int count, KeyEvent event)\r
1786         {\r
1787             if (host == 0)\r
1788                 return false;\r
1789 \r
1790             if (keyCode != KeyEvent.KEYCODE_UNKNOWN || event.getAction() != KeyEvent.ACTION_MULTIPLE)\r
1791                 return super.onKeyMultiple (keyCode, count, event);\r
1792 \r
1793             if (event.getCharacters() != null)\r
1794             {\r
1795                 int utf8Char = event.getCharacters().codePointAt (0);\r
1796                 handleKeyDown (host, utf8Char, utf8Char);\r
1797                 return true;\r
1798             }\r
1799 \r
1800             return false;\r
1801         }\r
1802 \r
1803         //==============================================================================\r
1804         private final class KeyboardDismissListener\r
1805         {\r
1806             public KeyboardDismissListener (ComponentPeerView viewToUse)\r
1807             {\r
1808                 view = viewToUse;\r
1809             }\r
1810 \r
1811             private void startListening()\r
1812             {\r
1813                 view.getViewTreeObserver().addOnGlobalLayoutListener(viewTreeObserver);\r
1814             }\r
1815 \r
1816             private void stopListening()\r
1817             {\r
1818                 view.getViewTreeObserver().removeGlobalOnLayoutListener(viewTreeObserver);\r
1819             }\r
1820 \r
1821             private class TreeObserver implements ViewTreeObserver.OnGlobalLayoutListener\r
1822             {\r
1823                 @Override\r
1824                 public void onGlobalLayout()\r
1825                 {\r
1826                     Rect r = new Rect();\r
1827 \r
1828                     view.getWindowVisibleDisplayFrame(r);\r
1829 \r
1830                     int diff = view.getHeight() - (r.bottom - r.top);\r
1831 \r
1832                     // Arbitrary threshold, surely keyboard would take more than 20 pix.\r
1833                     if (diff < 20)\r
1834                         handleKeyboardHidden (view.host);\r
1835                 };\r
1836             };\r
1837 \r
1838             private ComponentPeerView view;\r
1839             private TreeObserver viewTreeObserver = new TreeObserver();\r
1840         }\r
1841 \r
1842         private KeyboardDismissListener keyboardDismissListener = new KeyboardDismissListener(this);\r
1843 \r
1844         // this is here to make keyboard entry work on a Galaxy Tab2 10.1\r
1845         @Override\r
1846         public InputConnection onCreateInputConnection (EditorInfo outAttrs)\r
1847         {\r
1848             outAttrs.actionLabel = "";\r
1849             outAttrs.hintText = "";\r
1850             outAttrs.initialCapsMode = 0;\r
1851             outAttrs.initialSelEnd = outAttrs.initialSelStart = -1;\r
1852             outAttrs.label = "";\r
1853             outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI;\r
1854             outAttrs.inputType = InputType.TYPE_NULL;\r
1855 \r
1856             return new BaseInputConnection (this, false);\r
1857         }\r
1858 \r
1859         //==============================================================================\r
1860         @Override\r
1861         protected void onSizeChanged (int w, int h, int oldw, int oldh)\r
1862         {\r
1863             if (host == 0)\r
1864                 return;\r
1865 \r
1866             super.onSizeChanged (w, h, oldw, oldh);\r
1867             viewSizeChanged (host);\r
1868         }\r
1869 \r
1870         @Override\r
1871         protected void onLayout (boolean changed, int left, int top, int right, int bottom)\r
1872         {\r
1873             for (int i = getChildCount(); --i >= 0;)\r
1874                 requestTransparentRegion (getChildAt (i));\r
1875         }\r
1876 \r
1877         private native void viewSizeChanged (long host);\r
1878 \r
1879         @Override\r
1880         public void onFocusChange (View v, boolean hasFocus)\r
1881         {\r
1882             if (host == 0)\r
1883                 return;\r
1884 \r
1885             if (v == this)\r
1886                 focusChanged (host, hasFocus);\r
1887         }\r
1888 \r
1889         private native void focusChanged (long host, boolean hasFocus);\r
1890 \r
1891         public void setViewName (String newName)    {}\r
1892 \r
1893         public void setSystemUiVisibilityCompat (int visibility)\r
1894         {\r
1895             Method systemUIVisibilityMethod = null;\r
1896             try\r
1897             {\r
1898                 systemUIVisibilityMethod = this.getClass().getMethod ("setSystemUiVisibility", int.class);\r
1899             }\r
1900             catch (SecurityException e)     { return; }\r
1901             catch (NoSuchMethodException e) { return; }\r
1902             if (systemUIVisibilityMethod == null) return;\r
1903 \r
1904             try\r
1905             {\r
1906                 systemUIVisibilityMethod.invoke (this, visibility);\r
1907             }\r
1908             catch (java.lang.IllegalArgumentException e) {}\r
1909             catch (java.lang.IllegalAccessException e) {}\r
1910             catch (java.lang.reflect.InvocationTargetException e) {}\r
1911         }\r
1912 \r
1913         public boolean isVisible()                  { return getVisibility() == VISIBLE; }\r
1914         public void setVisible (boolean b)          { setVisibility (b ? VISIBLE : INVISIBLE); }\r
1915 \r
1916         public boolean containsPoint (int x, int y)\r
1917         {\r
1918             return true; //xxx needs to check overlapping views\r
1919         }\r
1920 \r
1921         //==============================================================================\r
1922         private native void handleAppResumed (long host);\r
1923 \r
1924         public void appResumed()\r
1925         {\r
1926             if (host == 0)\r
1927                 return;\r
1928 \r
1929             handleAppResumed (host);\r
1930         }\r
1931     }\r
1932 \r
1933     //==============================================================================\r
1934     public static class NativeSurfaceView    extends SurfaceView\r
1935                                           implements SurfaceHolder.Callback\r
1936     {\r
1937         private long nativeContext = 0;\r
1938 \r
1939         NativeSurfaceView (Context context, long nativeContextPtr)\r
1940         {\r
1941             super (context);\r
1942             nativeContext = nativeContextPtr;\r
1943         }\r
1944 \r
1945         public Surface getNativeSurface()\r
1946         {\r
1947             Surface retval = null;\r
1948 \r
1949             SurfaceHolder holder = getHolder();\r
1950             if (holder != null)\r
1951                 retval = holder.getSurface();\r
1952 \r
1953             return retval;\r
1954         }\r
1955 \r
1956         //==============================================================================\r
1957         @Override\r
1958         public void surfaceChanged (SurfaceHolder holder, int format, int width, int height)\r
1959         {\r
1960             surfaceChangedNative (nativeContext, holder, format, width, height);\r
1961         }\r
1962 \r
1963         @Override\r
1964         public void surfaceCreated (SurfaceHolder holder)\r
1965         {\r
1966             surfaceCreatedNative (nativeContext, holder);\r
1967         }\r
1968 \r
1969         @Override\r
1970         public void surfaceDestroyed (SurfaceHolder holder)\r
1971         {\r
1972             surfaceDestroyedNative (nativeContext, holder);\r
1973         }\r
1974 \r
1975         @Override\r
1976         protected void dispatchDraw (Canvas canvas)\r
1977         {\r
1978             super.dispatchDraw (canvas);\r
1979             dispatchDrawNative (nativeContext, canvas);\r
1980         }\r
1981 \r
1982         //==============================================================================\r
1983         @Override\r
1984         protected void onAttachedToWindow ()\r
1985         {\r
1986             super.onAttachedToWindow();\r
1987             getHolder().addCallback (this);\r
1988         }\r
1989 \r
1990         @Override\r
1991         protected void onDetachedFromWindow ()\r
1992         {\r
1993             super.onDetachedFromWindow();\r
1994             getHolder().removeCallback (this);\r
1995         }\r
1996 \r
1997         //==============================================================================\r
1998         private native void dispatchDrawNative (long nativeContextPtr, Canvas canvas);\r
1999         private native void surfaceCreatedNative (long nativeContextptr, SurfaceHolder holder);\r
2000         private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder);\r
2001         private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder,\r
2002                                                   int format, int width, int height);\r
2003     }\r
2004 \r
2005     public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr)\r
2006     {\r
2007         return new NativeSurfaceView (this, nativeSurfacePtr);\r
2008     }\r
2009 \r
2010     //==============================================================================\r
2011     public final int[] renderGlyph (char glyph1, char glyph2, Paint paint, android.graphics.Matrix matrix, Rect bounds)\r
2012     {\r
2013         Path p = new Path();\r
2014 \r
2015         char[] str = { glyph1, glyph2 };\r
2016         paint.getTextPath (str, 0, (glyph2 != 0 ? 2 : 1), 0.0f, 0.0f, p);\r
2017 \r
2018         RectF boundsF = new RectF();\r
2019         p.computeBounds (boundsF, true);\r
2020         matrix.mapRect (boundsF);\r
2021 \r
2022         boundsF.roundOut (bounds);\r
2023         bounds.left--;\r
2024         bounds.right++;\r
2025 \r
2026         final int w = bounds.width();\r
2027         final int h = Math.max (1, bounds.height());\r
2028 \r
2029         Bitmap bm = Bitmap.createBitmap (w, h, Bitmap.Config.ARGB_8888);\r
2030 \r
2031         Canvas c = new Canvas (bm);\r
2032         matrix.postTranslate (-bounds.left, -bounds.top);\r
2033         c.setMatrix (matrix);\r
2034         c.drawPath (p, paint);\r
2035 \r
2036         final int sizeNeeded = w * h;\r
2037         if (cachedRenderArray.length < sizeNeeded)\r
2038             cachedRenderArray = new int [sizeNeeded];\r
2039 \r
2040         bm.getPixels (cachedRenderArray, 0, w, 0, 0, w, h);\r
2041         bm.recycle();\r
2042         return cachedRenderArray;\r
2043     }\r
2044 \r
2045     private int[] cachedRenderArray = new int [256];\r
2046 \r
2047     //==============================================================================\r
2048     public static class NativeInvocationHandler implements InvocationHandler\r
2049     {\r
2050         public NativeInvocationHandler (Activity activityToUse, long nativeContextRef)\r
2051         {\r
2052             activity = activityToUse;\r
2053             nativeContext = nativeContextRef;\r
2054         }\r
2055 \r
2056         public void nativeContextDeleted()\r
2057         {\r
2058             nativeContext = 0;\r
2059         }\r
2060 \r
2061         @Override\r
2062         public void finalize()\r
2063         {\r
2064             activity.runOnUiThread (new Runnable()\r
2065                                     {\r
2066                                         @Override\r
2067                                         public void run()\r
2068                                         {\r
2069                                             if (nativeContext != 0)\r
2070                                                 dispatchFinalize (nativeContext);\r
2071                                         }\r
2072                                     });\r
2073         }\r
2074 \r
2075         @Override\r
2076         public Object invoke (Object proxy, Method method, Object[] args) throws Throwable\r
2077         {\r
2078             return dispatchInvoke (nativeContext, proxy, method, args);\r
2079         }\r
2080 \r
2081         //==============================================================================\r
2082         Activity activity;\r
2083         private long nativeContext = 0;\r
2084 \r
2085         private native void dispatchFinalize (long nativeContextRef);\r
2086         private native Object dispatchInvoke (long nativeContextRef, Object proxy, Method method, Object[] args);\r
2087     }\r
2088 \r
2089     public InvocationHandler createInvocationHandler (long nativeContextRef)\r
2090     {\r
2091         return new NativeInvocationHandler (this, nativeContextRef);\r
2092     }\r
2093 \r
2094     public void invocationHandlerContextDeleted (InvocationHandler handler)\r
2095     {\r
2096         ((NativeInvocationHandler) handler).nativeContextDeleted();\r
2097     }\r
2098 \r
2099     //==============================================================================\r
2100     public static class HTTPStream\r
2101     {\r
2102         public HTTPStream (String address, boolean isPostToUse, byte[] postDataToUse,\r
2103                            String headersToUse, int timeOutMsToUse,\r
2104                            int[] statusCodeToUse, StringBuffer responseHeadersToUse,\r
2105                            int numRedirectsToFollowToUse, String httpRequestCmdToUse) throws IOException\r
2106         {\r
2107             isPost = isPostToUse;\r
2108             postData = postDataToUse;\r
2109             headers = headersToUse;\r
2110             timeOutMs = timeOutMsToUse;\r
2111             statusCode = statusCodeToUse;\r
2112             responseHeaders = responseHeadersToUse;\r
2113             totalLength = -1;\r
2114             numRedirectsToFollow = numRedirectsToFollowToUse;\r
2115             httpRequestCmd = httpRequestCmdToUse;\r
2116 \r
2117             connection = createConnection (address, isPost, postData, headers, timeOutMs, httpRequestCmd);\r
2118         }\r
2119 \r
2120         private final HttpURLConnection createConnection (String address, boolean isPost, byte[] postData,\r
2121                                                           String headers, int timeOutMs, String httpRequestCmdToUse) throws IOException\r
2122         {\r
2123             HttpURLConnection newConnection = (HttpURLConnection) (new URL(address).openConnection());\r
2124 \r
2125             try\r
2126             {\r
2127                 newConnection.setInstanceFollowRedirects (false);\r
2128                 newConnection.setConnectTimeout (timeOutMs);\r
2129                 newConnection.setReadTimeout (timeOutMs);\r
2130 \r
2131                 // 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.\r
2132                 // So convert headers string to an array, with an element for each line\r
2133                 String headerLines[] = headers.split("\\n");\r
2134 \r
2135                 // Set request headers\r
2136                 for (int i = 0; i < headerLines.length; ++i)\r
2137                 {\r
2138                     int pos = headerLines[i].indexOf (":");\r
2139 \r
2140                     if (pos > 0 && pos < headerLines[i].length())\r
2141                     {\r
2142                         String field = headerLines[i].substring (0, pos);\r
2143                         String value = headerLines[i].substring (pos + 1);\r
2144 \r
2145                         if (value.length() > 0)\r
2146                             newConnection.setRequestProperty (field, value);\r
2147                     }\r
2148                 }\r
2149 \r
2150                 newConnection.setRequestMethod (httpRequestCmd);\r
2151 \r
2152                 if (isPost)\r
2153                 {\r
2154                     newConnection.setDoOutput (true);\r
2155 \r
2156                     if (postData != null)\r
2157                     {\r
2158                         OutputStream out = newConnection.getOutputStream();\r
2159                         out.write(postData);\r
2160                         out.flush();\r
2161                     }\r
2162                 }\r
2163 \r
2164                 return newConnection;\r
2165             }\r
2166             catch (Throwable e)\r
2167             {\r
2168                 newConnection.disconnect();\r
2169                 throw new IOException ("Connection error");\r
2170             }\r
2171         }\r
2172 \r
2173         private final InputStream getCancellableStream (final boolean isInput) throws ExecutionException\r
2174         {\r
2175             synchronized (createFutureLock)\r
2176             {\r
2177                 if (hasBeenCancelled.get())\r
2178                     return null;\r
2179 \r
2180                 streamFuture = executor.submit (new Callable<BufferedInputStream>()\r
2181                 {\r
2182                     @Override\r
2183                     public BufferedInputStream call() throws IOException\r
2184                     {\r
2185                         return new BufferedInputStream (isInput ? connection.getInputStream()\r
2186                                                                 : connection.getErrorStream());\r
2187                     }\r
2188                 });\r
2189             }\r
2190 \r
2191             try\r
2192             {\r
2193                 return streamFuture.get();\r
2194             }\r
2195             catch (InterruptedException e)\r
2196             {\r
2197                 return null;\r
2198             }\r
2199             catch (CancellationException e)\r
2200             {\r
2201                 return null;\r
2202             }\r
2203         }\r
2204 \r
2205         public final boolean connect()\r
2206         {\r
2207             boolean result = false;\r
2208             int numFollowedRedirects = 0;\r
2209 \r
2210             while (true)\r
2211             {\r
2212                 result = doConnect();\r
2213 \r
2214                 if (! result)\r
2215                     return false;\r
2216 \r
2217                 if (++numFollowedRedirects > numRedirectsToFollow)\r
2218                     break;\r
2219 \r
2220                 int status = statusCode[0];\r
2221 \r
2222                 if (status == 301 || status == 302 || status == 303 || status == 307)\r
2223                 {\r
2224                     // Assumes only one occurrence of "Location"\r
2225                     int pos1 = responseHeaders.indexOf ("Location:") + 10;\r
2226                     int pos2 = responseHeaders.indexOf ("\n", pos1);\r
2227 \r
2228                     if (pos2 > pos1)\r
2229                     {\r
2230                         String currentLocation = connection.getURL().toString();\r
2231                         String newLocation = responseHeaders.substring (pos1, pos2);\r
2232 \r
2233                         try\r
2234                         {\r
2235                             // Handle newLocation whether it's absolute or relative\r
2236                             URL baseUrl = new URL (currentLocation);\r
2237                             URL newUrl  = new URL (baseUrl, newLocation);\r
2238                             String transformedNewLocation = newUrl.toString();\r
2239 \r
2240                             if (transformedNewLocation != currentLocation)\r
2241                             {\r
2242                                 // Clear responseHeaders before next iteration\r
2243                                 responseHeaders.delete (0, responseHeaders.length());\r
2244 \r
2245                                 synchronized (createStreamLock)\r
2246                                 {\r
2247                                     if (hasBeenCancelled.get())\r
2248                                         return false;\r
2249 \r
2250                                     connection.disconnect();\r
2251 \r
2252                                     try\r
2253                                     {\r
2254                                         connection = createConnection (transformedNewLocation, isPost,\r
2255                                                                        postData, headers, timeOutMs,\r
2256                                                                        httpRequestCmd);\r
2257                                     }\r
2258                                     catch (Throwable e)\r
2259                                     {\r
2260                                         return false;\r
2261                                     }\r
2262                                 }\r
2263                             }\r
2264                             else\r
2265                             {\r
2266                                 break;\r
2267                             }\r
2268                         }\r
2269                         catch (Throwable e)\r
2270                         {\r
2271                             return false;\r
2272                         }\r
2273                     }\r
2274                     else\r
2275                     {\r
2276                         break;\r
2277                     }\r
2278                 }\r
2279                 else\r
2280                 {\r
2281                     break;\r
2282                 }\r
2283             }\r
2284 \r
2285             return result;\r
2286         }\r
2287 \r
2288         private final boolean doConnect()\r
2289         {\r
2290             synchronized (createStreamLock)\r
2291             {\r
2292                 if (hasBeenCancelled.get())\r
2293                     return false;\r
2294 \r
2295                 try\r
2296                 {\r
2297                     try\r
2298                     {\r
2299                         inputStream = getCancellableStream (true);\r
2300                     }\r
2301                     catch (ExecutionException e)\r
2302                     {\r
2303                         if (connection.getResponseCode() < 400)\r
2304                         {\r
2305                             statusCode[0] = connection.getResponseCode();\r
2306                             connection.disconnect();\r
2307                             return false;\r
2308                         }\r
2309                     }\r
2310                     finally\r
2311                     {\r
2312                         statusCode[0] = connection.getResponseCode();\r
2313                     }\r
2314 \r
2315                     try\r
2316                     {\r
2317                         if (statusCode[0] >= 400)\r
2318                             inputStream = getCancellableStream (false);\r
2319                         else\r
2320                             inputStream = getCancellableStream (true);\r
2321                     }\r
2322                     catch (ExecutionException e)\r
2323                     {}\r
2324 \r
2325                     for (java.util.Map.Entry<String, java.util.List<String>> entry : connection.getHeaderFields().entrySet())\r
2326                     {\r
2327                         if (entry.getKey() != null && entry.getValue() != null)\r
2328                         {\r
2329                             responseHeaders.append(entry.getKey() + ": "\r
2330                                                    + android.text.TextUtils.join(",", entry.getValue()) + "\n");\r
2331 \r
2332                             if (entry.getKey().compareTo ("Content-Length") == 0)\r
2333                                 totalLength = Integer.decode (entry.getValue().get (0));\r
2334                         }\r
2335                     }\r
2336 \r
2337                     return true;\r
2338                 }\r
2339                 catch (IOException e)\r
2340                 {\r
2341                     return false;\r
2342                 }\r
2343             }\r
2344         }\r
2345 \r
2346         static class DisconnectionRunnable implements Runnable\r
2347         {\r
2348             public DisconnectionRunnable (HttpURLConnection theConnection,\r
2349                                           InputStream theInputStream,\r
2350                                           ReentrantLock theCreateStreamLock,\r
2351                                           Object theCreateFutureLock,\r
2352                                           Future<BufferedInputStream> theStreamFuture)\r
2353             {\r
2354                 connectionToDisconnect = theConnection;\r
2355                 inputStream = theInputStream;\r
2356                 createStreamLock = theCreateStreamLock;\r
2357                 createFutureLock = theCreateFutureLock;\r
2358                 streamFuture = theStreamFuture;\r
2359             }\r
2360 \r
2361             public void run()\r
2362             {\r
2363                 try\r
2364                 {\r
2365                     if (! createStreamLock.tryLock())\r
2366                     {\r
2367                         synchronized (createFutureLock)\r
2368                         {\r
2369                             if (streamFuture != null)\r
2370                                 streamFuture.cancel (true);\r
2371                         }\r
2372 \r
2373                         createStreamLock.lock();\r
2374                     }\r
2375 \r
2376                     if (connectionToDisconnect != null)\r
2377                         connectionToDisconnect.disconnect();\r
2378 \r
2379                     if (inputStream != null)\r
2380                         inputStream.close();\r
2381                 }\r
2382                 catch (IOException e)\r
2383                 {}\r
2384                 finally\r
2385                 {\r
2386                     createStreamLock.unlock();\r
2387                 }\r
2388             }\r
2389 \r
2390             private HttpURLConnection connectionToDisconnect;\r
2391             private InputStream inputStream;\r
2392             private ReentrantLock createStreamLock;\r
2393             private Object createFutureLock;\r
2394             Future<BufferedInputStream> streamFuture;\r
2395         }\r
2396 \r
2397         public final void release()\r
2398         {\r
2399             DisconnectionRunnable disconnectionRunnable = new DisconnectionRunnable (connection,\r
2400                                                                                      inputStream,\r
2401                                                                                      createStreamLock,\r
2402                                                                                      createFutureLock,\r
2403                                                                                      streamFuture);\r
2404 \r
2405             synchronized (createStreamLock)\r
2406             {\r
2407                 hasBeenCancelled.set (true);\r
2408 \r
2409                 connection = null;\r
2410             }\r
2411 \r
2412             Thread disconnectionThread = new Thread(disconnectionRunnable);\r
2413             disconnectionThread.start();\r
2414         }\r
2415 \r
2416         public final int read (byte[] buffer, int numBytes)\r
2417         {\r
2418             int num = 0;\r
2419 \r
2420             try\r
2421             {\r
2422                 synchronized (createStreamLock)\r
2423                 {\r
2424                     if (inputStream != null)\r
2425                         num = inputStream.read (buffer, 0, numBytes);\r
2426                 }\r
2427             }\r
2428             catch (IOException e)\r
2429             {}\r
2430 \r
2431             if (num > 0)\r
2432                 position += num;\r
2433 \r
2434             return num;\r
2435         }\r
2436 \r
2437         public final long getPosition()                 { return position; }\r
2438         public final long getTotalLength()              { return totalLength; }\r
2439         public final boolean isExhausted()              { return false; }\r
2440         public final boolean setPosition (long newPos)  { return false; }\r
2441 \r
2442         private boolean isPost;\r
2443         private byte[] postData;\r
2444         private String headers;\r
2445         private int timeOutMs;\r
2446         String httpRequestCmd;\r
2447         private HttpURLConnection connection;\r
2448         private int[] statusCode;\r
2449         private StringBuffer responseHeaders;\r
2450         private int totalLength;\r
2451         private int numRedirectsToFollow;\r
2452         private InputStream inputStream;\r
2453         private long position;\r
2454         private final ReentrantLock createStreamLock = new ReentrantLock();\r
2455         private final Object createFutureLock = new Object();\r
2456         private AtomicBoolean hasBeenCancelled = new AtomicBoolean();\r
2457 \r
2458         private final ExecutorService executor = Executors.newCachedThreadPool (Executors.defaultThreadFactory());\r
2459         Future<BufferedInputStream> streamFuture;\r
2460     }\r
2461 \r
2462     public static final HTTPStream createHTTPStream (String address, boolean isPost, byte[] postData,\r
2463                                                      String headers, int timeOutMs, int[] statusCode,\r
2464                                                      StringBuffer responseHeaders, int numRedirectsToFollow,\r
2465                                                      String httpRequestCmd)\r
2466     {\r
2467         // timeout parameter of zero for HttpUrlConnection is a blocking connect (negative value for juce::URL)\r
2468         if (timeOutMs < 0)\r
2469             timeOutMs = 0;\r
2470         else if (timeOutMs == 0)\r
2471             timeOutMs = 30000;\r
2472 \r
2473         for (;;)\r
2474         {\r
2475             try\r
2476             {\r
2477                 HTTPStream httpStream = new HTTPStream (address, isPost, postData, headers,\r
2478                                                         timeOutMs, statusCode, responseHeaders,\r
2479                                                         numRedirectsToFollow, httpRequestCmd);\r
2480 \r
2481                 return httpStream;\r
2482             }\r
2483             catch (Throwable e) {}\r
2484 \r
2485             return null;\r
2486         }\r
2487     }\r
2488 \r
2489     public final void launchURL (String url)\r
2490     {\r
2491         startActivity (new Intent (Intent.ACTION_VIEW, Uri.parse (url)));\r
2492     }\r
2493 \r
2494     private native boolean webViewPageLoadStarted (long host, WebView view, String url);\r
2495     private native void webViewPageLoadFinished (long host, WebView view, String url);\r
2496     private native void webViewReceivedError (long host, WebView view, WebResourceRequest request, WebResourceError error);    private native void webViewReceivedHttpError (long host, WebView view, WebResourceRequest request, WebResourceResponse errorResponse);    private native void webViewReceivedSslError (long host, WebView view, SslErrorHandler handler, SslError error);\r
2497     private native void webViewCloseWindowRequest (long host, WebView view);\r
2498     private native void webViewCreateWindowRequest (long host, WebView view);\r
2499 \r
2500     //==============================================================================\r
2501     public class JuceWebViewClient   extends WebViewClient\r
2502     {\r
2503         public JuceWebViewClient (long hostToUse)\r
2504         {\r
2505             host = hostToUse;\r
2506         }\r
2507 \r
2508         public void hostDeleted()\r
2509         {\r
2510             synchronized (hostLock)\r
2511             {\r
2512                 host = 0;\r
2513             }\r
2514         }\r
2515 \r
2516         @Override\r
2517         public void onPageFinished (WebView view, String url)\r
2518         {\r
2519             if (host == 0)\r
2520                 return;\r
2521 \r
2522             webViewPageLoadFinished (host, view, url);\r
2523         }\r
2524 \r
2525         @Override\r
2526         public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error)\r
2527         {\r
2528             if (host == 0)\r
2529                 return;\r
2530 \r
2531             webViewReceivedSslError (host, view, handler, error);\r
2532         }\r
2533 \r
2534         @Override\r
2535         public void onReceivedError (WebView view, WebResourceRequest request, WebResourceError error)\r
2536         {\r
2537             if (host == 0)\r
2538                 return;\r
2539 \r
2540             webViewReceivedError (host, view, request, error);\r
2541         }\r
2542 \r
2543         @Override\r
2544         public void onReceivedHttpError (WebView view, WebResourceRequest request, WebResourceResponse errorResponse)\r
2545         {\r
2546             if (host == 0)\r
2547                 return;\r
2548 \r
2549             webViewReceivedHttpError (host, view, request, errorResponse);\r
2550         }\r
2551 \r
2552         @Override\r
2553         public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)\r
2554         {\r
2555             synchronized (hostLock)\r
2556             {\r
2557                 if (host != 0)\r
2558                 {\r
2559                     boolean shouldLoad = webViewPageLoadStarted (host, view, request.getUrl().toString());\r
2560 \r
2561                     if (shouldLoad)\r
2562                         return null;\r
2563                 }\r
2564             }\r
2565 \r
2566             return new WebResourceResponse ("text/html", null, null);\r
2567         }\r
2568 \r
2569         private long host;\r
2570         private final Object hostLock = new Object();\r
2571     }\r
2572 \r
2573     public class JuceWebChromeClient    extends WebChromeClient\r
2574     {\r
2575         public JuceWebChromeClient (long hostToUse)\r
2576         {\r
2577             host = hostToUse;\r
2578         }\r
2579 \r
2580         @Override\r
2581         public void onCloseWindow (WebView window)\r
2582         {\r
2583             webViewCloseWindowRequest (host, window);\r
2584         }\r
2585 \r
2586         @Override\r
2587         public boolean onCreateWindow (WebView view, boolean isDialog,\r
2588                                        boolean isUserGesture, Message resultMsg)\r
2589         {\r
2590             webViewCreateWindowRequest (host, view);\r
2591             return false;\r
2592         }\r
2593 \r
2594         private long host;\r
2595         private final Object hostLock = new Object();\r
2596     }\r
2597 \r
2598     //==============================================================================\r
2599     public static final String getLocaleValue (boolean isRegion)\r
2600     {\r
2601         java.util.Locale locale = java.util.Locale.getDefault();\r
2602 \r
2603         return isRegion ? locale.getCountry()\r
2604                         : locale.getLanguage();\r
2605     }\r
2606 \r
2607     private static final String getFileLocation (String type)\r
2608     {\r
2609         return Environment.getExternalStoragePublicDirectory (type).getAbsolutePath();\r
2610     }\r
2611 \r
2612     public static final String getDocumentsFolder()\r
2613     {\r
2614         if (getAndroidSDKVersion() >= 19)\r
2615             return getFileLocation ("Documents");\r
2616 \r
2617         return Environment.getDataDirectory().getAbsolutePath();\r
2618     }\r
2619 \r
2620     public static final String getPicturesFolder()   { return getFileLocation (Environment.DIRECTORY_PICTURES); }\r
2621     public static final String getMusicFolder()      { return getFileLocation (Environment.DIRECTORY_MUSIC); }\r
2622     public static final String getMoviesFolder()     { return getFileLocation (Environment.DIRECTORY_MOVIES); }\r
2623     public static final String getDownloadsFolder()  { return getFileLocation (Environment.DIRECTORY_DOWNLOADS); }\r
2624 \r
2625     //==============================================================================\r
2626     @Override\r
2627     protected void onActivityResult (int requestCode, int resultCode, Intent data)\r
2628     {\r
2629         appActivityResult (requestCode, resultCode, data);\r
2630     }\r
2631 \r
2632     @Override\r
2633     protected void onNewIntent (Intent intent)\r
2634     {\r
2635         super.onNewIntent(intent);\r
2636         setIntent(intent);\r
2637 \r
2638         appNewIntent (intent);\r
2639     }\r
2640 \r
2641     //==============================================================================\r
2642     public final Typeface getTypeFaceFromAsset (String assetName)\r
2643     {\r
2644         try\r
2645         {\r
2646             return Typeface.createFromAsset (this.getResources().getAssets(), assetName);\r
2647         }\r
2648         catch (Throwable e) {}\r
2649 \r
2650         return null;\r
2651     }\r
2652 \r
2653     final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();\r
2654 \r
2655     public static String bytesToHex (byte[] bytes)\r
2656     {\r
2657         char[] hexChars = new char[bytes.length * 2];\r
2658 \r
2659         for (int j = 0; j < bytes.length; ++j)\r
2660         {\r
2661             int v = bytes[j] & 0xff;\r
2662             hexChars[j * 2]     = hexArray[v >>> 4];\r
2663             hexChars[j * 2 + 1] = hexArray[v & 0x0f];\r
2664         }\r
2665 \r
2666         return new String (hexChars);\r
2667     }\r
2668 \r
2669     final private java.util.Map dataCache = new java.util.HashMap();\r
2670 \r
2671     synchronized private final File getDataCacheFile (byte[] data)\r
2672     {\r
2673         try\r
2674         {\r
2675             java.security.MessageDigest digest = java.security.MessageDigest.getInstance ("MD5");\r
2676             digest.update (data);\r
2677 \r
2678             String key = bytesToHex (digest.digest());\r
2679 \r
2680             if (dataCache.containsKey (key))\r
2681                 return (File) dataCache.get (key);\r
2682 \r
2683             File f = new File (this.getCacheDir(), "bindata_" + key);\r
2684             f.delete();\r
2685             FileOutputStream os = new FileOutputStream (f);\r
2686             os.write (data, 0, data.length);\r
2687             dataCache.put (key, f);\r
2688             return f;\r
2689         }\r
2690         catch (Throwable e) {}\r
2691 \r
2692         return null;\r
2693     }\r
2694 \r
2695     private final void clearDataCache()\r
2696     {\r
2697         java.util.Iterator it = dataCache.values().iterator();\r
2698 \r
2699         while (it.hasNext())\r
2700         {\r
2701             File f = (File) it.next();\r
2702             f.delete();\r
2703         }\r
2704     }\r
2705 \r
2706     public final Typeface getTypeFaceFromByteArray (byte[] data)\r
2707     {\r
2708         try\r
2709         {\r
2710             File f = getDataCacheFile (data);\r
2711 \r
2712             if (f != null)\r
2713                 return Typeface.createFromFile (f);\r
2714         }\r
2715         catch (Exception e)\r
2716         {\r
2717             Log.e ("JUCE", e.toString());\r
2718         }\r
2719 \r
2720         return null;\r
2721     }\r
2722 \r
2723     public static final int getAndroidSDKVersion()\r
2724     {\r
2725         return android.os.Build.VERSION.SDK_INT;\r
2726     }\r
2727 \r
2728     public final String audioManagerGetProperty (String property)\r
2729     {\r
2730         Object obj = getSystemService (AUDIO_SERVICE);\r
2731         if (obj == null)\r
2732             return null;\r
2733 \r
2734         java.lang.reflect.Method method;\r
2735 \r
2736         try\r
2737         {\r
2738             method = obj.getClass().getMethod ("getProperty", String.class);\r
2739         }\r
2740         catch (SecurityException e)     { return null; }\r
2741         catch (NoSuchMethodException e) { return null; }\r
2742 \r
2743         if (method == null)\r
2744             return null;\r
2745 \r
2746         try\r
2747         {\r
2748             return (String) method.invoke (obj, property);\r
2749         }\r
2750         catch (java.lang.IllegalArgumentException e) {}\r
2751         catch (java.lang.IllegalAccessException e) {}\r
2752         catch (java.lang.reflect.InvocationTargetException e) {}\r
2753 \r
2754         return null;\r
2755     }\r
2756 \r
2757     public final boolean hasSystemFeature (String property)\r
2758     {\r
2759         return getPackageManager().hasSystemFeature (property);\r
2760     }\r
2761 }\r