diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h index a65eb281fb..5f5458fffb 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h @@ -252,19 +252,22 @@ public: File javaSourceFolder (coreModule->getFolder().getChildFile ("native") .getChildFile ("java")); - String juceMidiCode, juceMidiImports; + String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode; juceMidiImports << newLine; if (getMinimumSDKVersionString().getIntValue() >= 23) { File javaAndroidMidi (javaSourceFolder.getChildFile ("AndroidMidi.java")); + File javaRuntimePermissions (javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java")); juceMidiImports << "import android.media.midi.*;" << newLine << "import android.bluetooth.*;" << newLine << "import android.bluetooth.le.*;" << newLine; juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className); + + juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className); } else { @@ -287,6 +290,8 @@ public: newFile << juceMidiImports; else if (line.contains ("$$JuceAndroidMidiCode$$")) newFile << juceMidiCode; + else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$")) + newFile << juceRuntimePermissionsCode; else newFile << line.replace ("JuceAppActivity", className) .replace ("package com.juce;", "package " + package + ";") << newLine; diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h index 72beee2162..2122345ef1 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h @@ -626,6 +626,17 @@ private: return result; } + String createDependencies (const String& indent) const + { + String result; + + result << "dependencies {" << newLine + << indent << "compile \"com.android.support:support-v4:+\"" << newLine // needed for ContextCompat and ActivityCompat + << "}" << newLine; + + return result; + } + void writeBuildDotGradleApp (const File& folder) const { MemoryOutputStream memoryOutputStream; @@ -656,7 +667,8 @@ private: << CodeHelpers::indent (createModelDotAndroidDotSigningConfigs (indent), indent.length(), true) << newLine << CodeHelpers::indent (createModelDotAndroidDotProductFlavors (indent), indent.length(), true) - << "}"; + << "}" << newLine << newLine + << createDependencies (indent); overwriteFileIfDifferentOrThrow (folder.getChildFile ("app/build.gradle"), memoryOutputStream); } diff --git a/modules/juce_audio_devices/native/juce_android_Audio.cpp b/modules/juce_audio_devices/native/juce_android_Audio.cpp index 9e5160626e..5714d62358 100644 --- a/modules/juce_audio_devices/native/juce_android_Audio.cpp +++ b/modules/juce_audio_devices/native/juce_android_Audio.cpp @@ -195,25 +195,51 @@ public: STREAM_MUSIC, sampleRate, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT, (jint) (minBufferSizeOut * numDeviceOutputChannels * sizeof (int16)), MODE_STREAM)); - if (env->CallIntMethod (outputDevice, AudioTrack.getState) != STATE_UNINITIALIZED) + int outputDeviceState = env->CallIntMethod (outputDevice, AudioTrack.getState); + if (outputDeviceState > 0) + { isRunning = true; + } else - outputDevice.clear(); // failed to open the device + { + // failed to open the device + outputDevice.clear(); + lastError = "Error opening audio output device: android.media.AudioTrack failed with state = " + String (outputDeviceState); + } } if (numClientInputChannels > 0 && numDeviceInputChannelsAvailable > 0) { - numDeviceInputChannels = jmin (numClientInputChannels, numDeviceInputChannelsAvailable); - inputDevice = GlobalRef (env->NewObject (AudioRecord, AudioRecord.constructor, - 0 /* (default audio source) */, sampleRate, - numDeviceInputChannelsAvailable > 1 ? CHANNEL_IN_STEREO : CHANNEL_IN_MONO, - ENCODING_PCM_16BIT, - (jint) (minBufferSizeIn * numDeviceInputChannels * sizeof (int16)))); + if (! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) + { + // If you hit this assert, you probably forgot to get RuntimePermissions::recordAudio + // before trying to open an audio input device. This is not going to work! + jassertfalse; - if (env->CallIntMethod (inputDevice, AudioRecord.getState) != STATE_UNINITIALIZED) - isRunning = true; + inputDevice.clear(); + lastError = "Error opening audio input device: the app was not granted android.permission.RECORD_AUDIO"; + } else - inputDevice.clear(); // failed to open the device + { + numDeviceInputChannels = jmin (numClientInputChannels, numDeviceInputChannelsAvailable); + inputDevice = GlobalRef (env->NewObject (AudioRecord, AudioRecord.constructor, + 0 /* (default audio source) */, sampleRate, + numDeviceInputChannelsAvailable > 1 ? CHANNEL_IN_STEREO : CHANNEL_IN_MONO, + ENCODING_PCM_16BIT, + (jint) (minBufferSizeIn * numDeviceInputChannels * sizeof (int16)))); + + int inputDeviceState = env->CallIntMethod (inputDevice, AudioRecord.getState); + if (inputDeviceState > 0) + { + isRunning = true; + } + else + { + // failed to open the device + inputDevice.clear(); + lastError = "Error opening audio input device: android.media.AudioRecord failed with state = " + String (inputDeviceState); + } + } } if (isRunning) diff --git a/modules/juce_audio_devices/native/juce_android_OpenSL.cpp b/modules/juce_audio_devices/native/juce_android_OpenSL.cpp index d08c25ffe7..7102afeeee 100644 --- a/modules/juce_audio_devices/native/juce_android_OpenSL.cpp +++ b/modules/juce_audio_devices/native/juce_android_OpenSL.cpp @@ -145,12 +145,32 @@ public: << ", sampleRate = " << sampleRate); if (numInputChannels > 0) - recorder = engine.createRecorder (numInputChannels, sampleRate, - audioBuffersToEnqueue, actualBufferSize); + { + if (! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) + { + // If you hit this assert, you probably forgot to get RuntimePermissions::recordAudio + // before trying to open an audio input device. This is not going to work! + jassertfalse; + lastError = "Error opening OpenSL input device: the app was not granted android.permission.RECORD_AUDIO"; + } + else + { + recorder = engine.createRecorder (numInputChannels, sampleRate, + audioBuffersToEnqueue, actualBufferSize); + + if (recorder == nullptr) + lastError = "Error opening OpenSL input device: creating Recorder failed."; + } + } if (numOutputChannels > 0) - player = engine.createPlayer (numOutputChannels, sampleRate, - audioBuffersToEnqueue, actualBufferSize); + { + player = engine.createPlayer (numOutputChannels, sampleRate, + audioBuffersToEnqueue, actualBufferSize); + + if (player == nullptr) + lastError = "Error opening OpenSL input device: creating Player failed."; + } // pre-fill buffers for (int i = 0; i < audioBuffersToEnqueue; ++i) diff --git a/modules/juce_audio_utils/native/juce_android_BluetoothMidiDevicePairingDialogue.cpp b/modules/juce_audio_utils/native/juce_android_BluetoothMidiDevicePairingDialogue.cpp index 7f98d645e2..05a8afe04a 100644 --- a/modules/juce_audio_utils/native/juce_android_BluetoothMidiDevicePairingDialogue.cpp +++ b/modules/juce_audio_utils/native/juce_android_BluetoothMidiDevicePairingDialogue.cpp @@ -427,6 +427,15 @@ private: //============================================================================== bool BluetoothMidiDevicePairingDialogue::open() { + if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi)) + { + // If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi. + // This is not going to work, boo! The pairing dialogue won't be able to scan for or + // find any devices, it will just display an empty list, so don't bother opening it. + jassertfalse; + return false; + } + BluetoothMidiSelectorOverlay* overlay = new BluetoothMidiSelectorOverlay; return true; } diff --git a/modules/juce_core/juce_core.cpp b/modules/juce_core/juce_core.cpp index 1ac8b13ebf..b052bf54eb 100644 --- a/modules/juce_core/juce_core.cpp +++ b/modules/juce_core/juce_core.cpp @@ -144,6 +144,7 @@ namespace juce #include "maths/juce_Expression.cpp" #include "maths/juce_Random.cpp" #include "memory/juce_MemoryBlock.cpp" +#include "misc/juce_RuntimePermissions.cpp" #include "misc/juce_Result.cpp" #include "misc/juce_Uuid.cpp" #include "network/juce_MACAddress.cpp" @@ -228,6 +229,7 @@ namespace juce #include "native/juce_android_Network.cpp" #include "native/juce_android_SystemStats.cpp" #include "native/juce_android_Threads.cpp" +#include "native/juce_android_RuntimePermissions.cpp" #endif diff --git a/modules/juce_core/juce_core.h b/modules/juce_core/juce_core.h index 6f5c67c97b..7f7523ed8e 100644 --- a/modules/juce_core/juce_core.h +++ b/modules/juce_core/juce_core.h @@ -242,6 +242,7 @@ extern JUCE_API void JUCE_CALLTYPE logAssertion (const char* file, int line) noe #include "maths/juce_BigInteger.h" #include "maths/juce_Expression.h" #include "maths/juce_Random.h" +#include "misc/juce_RuntimePermissions.h" #include "misc/juce_Uuid.h" #include "misc/juce_WindowsRegistry.h" #include "system/juce_SystemStats.h" diff --git a/modules/juce_core/misc/juce_RuntimePermissions.cpp b/modules/juce_core/misc/juce_RuntimePermissions.cpp new file mode 100644 index 0000000000..c9e38ea0ed --- /dev/null +++ b/modules/juce_core/misc/juce_RuntimePermissions.cpp @@ -0,0 +1,48 @@ +/* + ============================================================================== + + This file is part of the juce_core module of the JUCE library. + Copyright (c) 2016 - ROLI Ltd. + + Permission to use, copy, modify, and/or distribute this software for any purpose with + or without fee is hereby granted, provided that the above copyright notice and this + permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ------------------------------------------------------------------------------ + + NOTE! This permissive ISC license applies ONLY to files within the juce_core module! + All other JUCE modules are covered by a dual GPL/commercial license, so if you are + using any other modules, be sure to check that you also comply with their license. + + For more details, visit www.juce.com + + ============================================================================== +*/ + +#if ! JUCE_ANDROID // We currently don't request runtime permissions on any other platform + // than Android, so this file contains a dummy implementation for those. + // This may change in the future. + +void RuntimePermissions::request (PermissionID /*permission*/, Callback callback) +{ + callback (true); +} + +bool RuntimePermissions::isRequired (PermissionID /*permission*/) +{ + return false; +} + +bool RuntimePermissions::isGranted (PermissionID /*permission*/) +{ + return true; +} + +#endif diff --git a/modules/juce_core/misc/juce_RuntimePermissions.h b/modules/juce_core/misc/juce_RuntimePermissions.h new file mode 100644 index 0000000000..814cecc690 --- /dev/null +++ b/modules/juce_core/misc/juce_RuntimePermissions.h @@ -0,0 +1,131 @@ +/* + ============================================================================== + + This file is part of the juce_core module of the JUCE library. + Copyright (c) 2016 - ROLI Ltd. + + Permission to use, copy, modify, and/or distribute this software for any purpose with + or without fee is hereby granted, provided that the above copyright notice and this + permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ------------------------------------------------------------------------------ + + NOTE! This permissive ISC license applies ONLY to files within the juce_core module! + All other JUCE modules are covered by a dual GPL/commercial license, so if you are + using any other modules, be sure to check that you also comply with their license. + + For more details, visit www.juce.com + + ============================================================================== +*/ + +#ifndef JUCE_RUNTIMEPERMISSIONS_H_INCLUDED +#define JUCE_RUNTIMEPERMISSIONS_H_INCLUDED + +//============================================================================== +/** + Class to handle app runtime permissions for certain functionality on some platforms. + + The use of this class is currently only required if the app should run on + Android API level 23 and higher. + + On lower API levels, the permissions are specified in the app manifest. On iOS, + runtime permission requests are handled automatically by the Apple APIs and not + manually in the app code. On Windows, OS X, and Linux, runtime permissions are not + used at all. In all these cases, request() will simply call through to the + callback with no overhead and pass true (making it safe to use on all platforms). + + For example, to enable audio recording on Android in your cross-platform app, + you could modify your code as follows: + + Old code: + + audioDeviceManager.initialise (2, 2, nullptr, true, String(), nullptr); + + New code: + + RuntimePermissions::request ( + RuntimePermissions::audioRecording, + [this] (bool wasGranted) + { + if (! wasGranted) + { + // e.g. display an error or initialise with 0 input channels + return; + } + + audioDeviceManager.initialise (2, 2, nullptr, true, String(), nullptr); + } + ); +*/ +class JUCE_API RuntimePermissions +{ +public: + //========================================================================== + enum PermissionID + { + /** Permission to access the microphone (required on Android). + You need to request this, for example, to initialise an AudioDeviceManager with + a non-zero number of input channels, and to open the default audio input device. + */ + recordAudio = 1, + + /** Permission to scan for and pair to Bluetooth MIDI devices (required on Android). + You need to request this before calling BluetoothMidiDevicePairingDialogue::open(), + otherwise no devices will be found. + */ + bluetoothMidi = 2, + }; + + //========================================================================== + /** Function type of runtime permission request callbacks. */ + #if JUCE_COMPILER_SUPPORTS_LAMBDAS + typedef std::function Callback; + #else + typedef void (*Callback) (bool); + #endif + + //========================================================================== + /** Call this method to request a runtime permission. + + @param permission The PermissionID of the permission you want to request. + + @param callback The callback to be called after the request has been granted + or denied; the argument passed will be true if the permission + has been granted and false otherwise. + + If no runtime request is required or possible to obtain the permission, the + callback will be called immediately. The argument passed in will be true + if the permission is granted or no permission is required on this platform, + and false otherwise. + + If a runtime request is required to obtain the permission, the callback + will be called asynchronously after the OS has granted or denied the requested + permission (typically by displaying a dialog box to the user and waiting until + the user has responded). + */ + static void request (PermissionID permission, Callback callback); + + /** Returns whether a runtime request is required to obtain the permission + on the current platform. + */ + static bool isRequired (PermissionID permission); + + /** Returns true if the app has been already granted this permission, either + via a previous runtime request or otherwise, or no permission is necessary. + + Note that this can be false even if isRequired returns false. In this case, + the permission can not be obtained at all at runtime. + */ + static bool isGranted (PermissionID permission); +}; + + +#endif // JUCE_RUNTIMEPERMISSIONS_H_INCLUDED diff --git a/modules/juce_core/native/java/AndroidRuntimePermissions.java b/modules/juce_core/native/java/AndroidRuntimePermissions.java new file mode 100644 index 0000000000..b2a8a461ce --- /dev/null +++ b/modules/juce_core/native/java/AndroidRuntimePermissions.java @@ -0,0 +1,14 @@ + private native void androidRuntimePermissionsCallback (boolean permissionWasGranted, long ptrToCallback); + + @Override + public void onRequestPermissionsResult (int permissionID, String permissions[], int[] grantResults) + { + boolean permissionsGranted = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); + + if (! permissionsGranted) + Log.d ("JUCE", "onRequestPermissionsResult: runtime permission was DENIED: " + getAndroidPermissionName (permissionID)); + + Long ptrToCallback = permissionCallbackPtrMap.get (permissionID); + permissionCallbackPtrMap.remove (permissionID); + androidRuntimePermissionsCallback (permissionsGranted, ptrToCallback); + } diff --git a/modules/juce_core/native/java/JuceAppActivity.java b/modules/juce_core/native/java/JuceAppActivity.java index d69ccdd0e1..63f80e09f0 100644 --- a/modules/juce_core/native/java/JuceAppActivity.java +++ b/modules/juce_core/native/java/JuceAppActivity.java @@ -30,13 +30,12 @@ import android.content.DialogInterface; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Looper; import android.os.Handler; -import android.os.Build; -import android.os.Process; import android.os.ParcelUuid; import android.os.Environment; import android.view.*; @@ -50,19 +49,16 @@ import android.text.InputType; import android.util.DisplayMetrics; import android.util.Log; import java.lang.Runnable; -import java.util.List; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.TimerTask; +import java.util.*; import java.io.*; import java.net.URL; import java.net.HttpURLConnection; import android.media.AudioManager; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; - +import android.support.v4.content.ContextCompat; +import android.support.v4.app.ActivityCompat; +import android.Manifest; $$JuceAndroidMidiImports$$ // If you get an error here, you need to re-save your project with the introjucer! @@ -75,6 +71,73 @@ public class JuceAppActivity extends Activity System.loadLibrary ("juce_jni"); } + //============================================================================== + public boolean isPermissionDeclaredInManifest (int permissionID) + { + String permissionToCheck = getAndroidPermissionName(permissionID); + + try + { + PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); + + if (info.requestedPermissions != null) + for (String permission : info.requestedPermissions) + if (permission.equals (permissionToCheck)) + return true; + } + catch (PackageManager.NameNotFoundException e) + { + Log.d ("JUCE", "isPermissionDeclaredInManifest: PackageManager.NameNotFoundException = " + e.toString()); + } + + Log.d ("JUCE", "isPermissionDeclaredInManifest: could not find requested permission " + permissionToCheck); + return false; + } + + //============================================================================== + // these have to match the values of enum PermissionID in C++ class RuntimePermissions: + private static final int JUCE_PERMISSIONS_RECORD_AUDIO = 1; + private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; + + private static String getAndroidPermissionName (int permissionID) + { + switch (permissionID) + { + case JUCE_PERMISSIONS_RECORD_AUDIO: return Manifest.permission.RECORD_AUDIO; + case JUCE_PERMISSIONS_BLUETOOTH_MIDI: return Manifest.permission.ACCESS_COARSE_LOCATION; + } + + // unknown permission ID! + assert false; + return new String(); + } + + public boolean isPermissionGranted (int permissionID) + { + return ContextCompat.checkSelfPermission (this, getAndroidPermissionName (permissionID)) == PackageManager.PERMISSION_GRANTED; + } + + private Map permissionCallbackPtrMap; + + public void requestRuntimePermission (int permissionID, long ptrToCallback) + { + String permissionName = getAndroidPermissionName (permissionID); + + if (ContextCompat.checkSelfPermission (this, permissionName) != PackageManager.PERMISSION_GRANTED) + { + // remember callbackPtr, request permissions, and let onRequestPermissionResult call callback asynchronously + permissionCallbackPtrMap.put (permissionID, ptrToCallback); + ActivityCompat.requestPermissions (this, new String[]{permissionName}, permissionID); + } + else + { + // permissions were already granted before, we can call callback directly + androidRuntimePermissionsCallback (true, ptrToCallback); + } + } + + $$JuceAndroidRuntimePermissionsCode$$ // If you get an error here, you need to re-save your project with the introjucer! + //============================================================================== public static class MidiPortID extends Object { @@ -138,6 +201,8 @@ public class JuceAppActivity extends Activity setContentView (viewHolder); setVolumeControlStream (AudioManager.STREAM_MUSIC); + + permissionCallbackPtrMap = new HashMap(); } @Override diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 98e3389410..ca3f056105 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -294,6 +294,9 @@ extern AndroidSystem android; METHOD (setCurrentThreadPriority, "setCurrentThreadPriority", "(I)I") \ METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \ METHOD (createNewThread, "createNewThread", "(JLjava/lang/String;J)Ljava/lang/Thread;") \ + METHOD (requestRuntimePermission, "requestRuntimePermission", "(IJ)V" ) \ + METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \ + METHOD (isPermissionDeclaredInManifest, "isPermissionDeclaredInManifest", "(I)Z" ) \ DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); #undef JNI_CLASS_MEMBERS diff --git a/modules/juce_core/native/juce_android_RuntimePermissions.cpp b/modules/juce_core/native/juce_android_RuntimePermissions.cpp new file mode 100644 index 0000000000..d368415745 --- /dev/null +++ b/modules/juce_core/native/juce_android_RuntimePermissions.cpp @@ -0,0 +1,90 @@ +/* + ============================================================================== + + This file is part of the juce_core module of the JUCE library. + Copyright (c) 2016 - ROLI Ltd. + + Permission to use, copy, modify, and/or distribute this software for any purpose with + or without fee is hereby granted, provided that the above copyright notice and this + permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ------------------------------------------------------------------------------ + + NOTE! This permissive ISC license applies ONLY to files within the juce_core module! + All other JUCE modules are covered by a dual GPL/commercial license, so if you are + using any other modules, be sure to check that you also comply with their license. + + For more details, visit www.juce.com + + ============================================================================== +*/ + +namespace +{ + void handleAndroidCallback (bool permissionWasGranted, RuntimePermissions::Callback* callbackPtr) + { + if (callbackPtr == nullptr) + { + // got a nullptr passed in from java! this should never happen... + jassertfalse; + return; + } + + std::unique_ptr uptr (callbackPtr); + (*uptr) (permissionWasGranted); + } +} + +JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, + androidRuntimePermissionsCallback, + void, (JNIEnv* env, jobject /*javaObjectHandle*/, jboolean permissionsGranted, jlong callbackPtr)) +{ + setEnv (env); + handleAndroidCallback (permissionsGranted != 0, + reinterpret_cast (callbackPtr)); +} + +void RuntimePermissions::request (PermissionID permission, Callback callback) +{ + if (! android.activity.callBooleanMethod (JuceAppActivity.isPermissionDeclaredInManifest, (jint) permission)) + { + // Error! If you want to be able to request this runtime permission, you + // also need to declare it in your app's manifest. You can do so via + // the Introjucer. Otherwise this can't work. + jassertfalse; + + callback (false); + return; + } + + if (JUCE_ANDROID_API_VERSION < 23) + { + // There is no runtime permission system on API level below 23. As long as the + // permission is in the manifest (seems to be the case), we can simply ask Android + // if the app has the permission, and then directly call through to the callback. + callback (isGranted (permission)); + return; + } + + // we need to move the callback object to the heap so Java can keep track of the pointer + // and asynchronously pass it back to us (to be called and then deleted) + Callback* callbackPtr = new Callback (std::move (callback)); + android.activity.callVoidMethod (JuceAppActivity.requestRuntimePermission, permission, (jlong) callbackPtr); +} + +bool RuntimePermissions::isRequired (PermissionID /*permission*/) +{ + return JUCE_ANDROID_API_VERSION >= 23; +} + +bool RuntimePermissions::isGranted (PermissionID permission) +{ + return android.activity.callBooleanMethod (JuceAppActivity.isPermissionGranted, permission); +}