1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Added feature RuntimePermissions, which allows to request permissions at runtime to access the microphone and bluetooth (required for Android apps using SDK Level 23 and above).

This commit is contained in:
Timur Doumler 2016-02-25 10:12:30 +00:00
parent c8428f2168
commit 9ea874428c
13 changed files with 452 additions and 26 deletions

View file

@ -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;

View file

@ -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);
}

View file

@ -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)

View file

@ -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)

View file

@ -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;
}

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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<void (bool)> 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

View file

@ -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);
}

View file

@ -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<Integer, Long> permissionCallbackPtrMap;
public void requestRuntimePermission (int permissionID, long ptrToCallback)
{
String permissionName = getAndroidPermissionName (permissionID);
if (ContextCompat.checkSelfPermission (this, permissionName) != PackageManager.PERMISSION_GRANTED)
{
// remember callbackPtr, request permissions, and let onRequestPermissionResult call callback asynchronously
permissionCallbackPtrMap.put (permissionID, ptrToCallback);
ActivityCompat.requestPermissions (this, new String[]{permissionName}, permissionID);
}
else
{
// permissions were already granted before, we can call callback directly
androidRuntimePermissionsCallback (true, ptrToCallback);
}
}
$$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<Integer, Long>();
}
@Override

View file

@ -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

View file

@ -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<RuntimePermissions::Callback> 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<RuntimePermissions::Callback*> (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);
}