From e3e8b8a91d5c782957b2d978d6c672ff57b66f95 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 8 Nov 2022 15:38:31 +0000 Subject: [PATCH] Projucer: Support file permissions in Android 33 --- .../jucer_ProjectExport_Android.h | 52 +++++++- .../Source/Utility/Helpers/jucer_PresetIDs.h | 3 + .../juce_core/misc/juce_RuntimePermissions.h | 17 ++- .../juce_android_RuntimePermissions.cpp | 118 ++++++++++-------- 4 files changed, 129 insertions(+), 61 deletions(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index d585bfe6fe..5103b4869d 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -71,6 +71,19 @@ public: createOtherExporterProperties (props); } + void updateDeprecatedSettings() override + { + const auto needsExternalRead = getSettingString (Ids::androidExternalReadNeeded); + settings.removeProperty (Ids::androidExternalReadNeeded, nullptr); + + if (needsExternalRead.isEmpty()) + return; + + androidReadMediaAudioPermission .setValue (needsExternalRead, nullptr); + androidReadMediaImagesPermission.setValue (needsExternalRead, nullptr); + androidReadMediaVideoPermission .setValue (needsExternalRead, nullptr); + } + static String getDisplayName() { return "Android"; } static String getValueTreeTypeName() { return "ANDROIDSTUDIO"; } static String getTargetFolderName() { return "Android"; } @@ -94,7 +107,8 @@ public: androidCustomActivityClass, androidCustomApplicationClass, androidManifestCustomXmlElements, androidGradleSettingsContent, androidVersionCode, androidMinimumSDK, androidTargetSDK, androidTheme, androidExtraAssetsFolder, androidOboeRepositoryPath, androidInternetNeeded, androidMicNeeded, androidCameraNeeded, - androidBluetoothNeeded, androidExternalReadPermission, androidExternalWritePermission, + androidBluetoothNeeded, androidReadMediaAudioPermission, androidReadMediaImagesPermission, + androidReadMediaVideoPermission, androidExternalWritePermission, androidInAppBillingPermission, androidVibratePermission, androidOtherPermissions, androidPushNotifications, androidEnableRemoteNotifications, androidRemoteNotificationsConfigFile, androidEnableContentSharing, androidKeyStore, androidKeyStorePass, androidKeyAlias, androidKeyAliasPass, gradleVersion, gradleToolchain, androidPluginVersion; @@ -124,7 +138,9 @@ public: androidMicNeeded (settings, Ids::microphonePermissionNeeded, getUndoManager(), false), androidCameraNeeded (settings, Ids::cameraPermissionNeeded, getUndoManager(), false), androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, getUndoManager(), true), - androidExternalReadPermission (settings, Ids::androidExternalReadNeeded, getUndoManager(), true), + androidReadMediaAudioPermission (settings, Ids::androidReadMediaAudioPermission, getUndoManager(), true), + androidReadMediaImagesPermission (settings, Ids::androidReadMediaImagesPermission, getUndoManager(), true), + androidReadMediaVideoPermission (settings, Ids::androidReadMediaVideoPermission, getUndoManager(), true), androidExternalWritePermission (settings, Ids::androidExternalWriteNeeded, getUndoManager(), true), androidInAppBillingPermission (settings, Ids::androidInAppBilling, getUndoManager(), false), androidVibratePermission (settings, Ids::androidVibratePermissionNeeded, getUndoManager(), false), @@ -1103,8 +1119,14 @@ private: props.add (new ChoicePropertyComponent (androidBluetoothNeeded, "Bluetooth Permissions Required"), "If enabled, this will set the android.permission.BLUETOOTH and android.permission.BLUETOOTH_ADMIN flag in the manifest. This is required for Bluetooth MIDI on Android."); - props.add (new ChoicePropertyComponent (androidExternalReadPermission, "Read From External Storage"), - "If enabled, this will set the android.permission.READ_EXTERNAL_STORAGE flag in the manifest."); + props.add (new ChoicePropertyComponent (androidReadMediaAudioPermission, "Read Audio From External Storage"), + "If enabled, this will set the android.permission.READ_MEDIA_AUDIO and android.permission.READ_EXTERNAL_STORAGE flags in the manifest."); + + props.add (new ChoicePropertyComponent (androidReadMediaImagesPermission, "Read Images From External Storage"), + "If enabled, this will set the android.permission.READ_MEDIA_IMAGES and android.permission.READ_EXTERNAL_STORAGE flags in the manifest."); + + props.add (new ChoicePropertyComponent (androidReadMediaVideoPermission, "Read Video From External Storage"), + "If enabled, this will set the android.permission.READ_MEDIA_VIDEO and android.permission.READ_EXTERNAL_STORAGE flags in the manifest."); props.add (new ChoicePropertyComponent (androidExternalWritePermission, "Write to External Storage"), "If enabled, this will set the android.permission.WRITE_EXTERNAL_STORAGE flag in the manifest."); @@ -1680,6 +1702,15 @@ private: // This permission only has an effect on SDK version 28 and lower if (permission == "android.permission.WRITE_EXTERNAL_STORAGE") usesPermission->setAttribute ("android:maxSdkVersion", "28"); + + // https://developer.android.com/training/data-storage/shared/documents-files + // If the SDK version is <= 28, READ_EXTERNAL_STORAGE is required to access any + // media file, including files created by the current app. + // If the SDK version is <= 32, READ_EXTERNAL_STORAGE is required to access other + // apps' media files. + // This permission has no effect on later Android versions. + if (permission == "android.permission.READ_EXTERNAL_STORAGE") + usesPermission->setAttribute ("android:maxSdkVersion", "32"); } } @@ -1845,7 +1876,18 @@ private: s.add ("android.permission.ACCESS_COARSE_LOCATION"); } - if (androidExternalReadPermission.get()) + if (androidReadMediaAudioPermission.get()) + s.add ("android.permission.READ_MEDIA_AUDIO"); + + if (androidReadMediaImagesPermission.get()) + s.add ("android.permission.READ_MEDIA_IMAGES"); + + if (androidReadMediaVideoPermission.get()) + s.add ("android.permission.READ_MEDIA_VIDEO"); + + if ( androidReadMediaAudioPermission.get() + || androidReadMediaImagesPermission.get() + || androidReadMediaVideoPermission.get()) s.add ("android.permission.READ_EXTERNAL_STORAGE"); if (androidExternalWritePermission.get()) diff --git a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h index 350d7c4155..3f64d12a59 100644 --- a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h +++ b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h @@ -235,6 +235,9 @@ namespace Ids DECLARE_ID (androidCustomStringXmlElements); DECLARE_ID (androidBluetoothNeeded); DECLARE_ID (androidExternalReadNeeded); + DECLARE_ID (androidReadMediaAudioPermission); + DECLARE_ID (androidReadMediaImagesPermission); + DECLARE_ID (androidReadMediaVideoPermission); DECLARE_ID (androidExternalWriteNeeded); DECLARE_ID (androidInAppBilling); DECLARE_ID (androidVibratePermissionNeeded); diff --git a/modules/juce_core/misc/juce_RuntimePermissions.h b/modules/juce_core/misc/juce_RuntimePermissions.h index 3f1cd113db..ca8d139fb9 100644 --- a/modules/juce_core/misc/juce_RuntimePermissions.h +++ b/modules/juce_core/misc/juce_RuntimePermissions.h @@ -86,7 +86,22 @@ public: writeExternalStorage = 4, /** Permission to use camera */ - camera = 5 + camera = 5, + + /** Permission to read audio files that your app didn't create. + Has the same effect as readExternalStorage on iOS and Android versions before 33. + */ + readMediaAudio = 6, + + /** Permission to read image files that your app didn't create. + Has the same effect as readExternalStorage on iOS and Android versions before 33. + */ + readMediaImages = 7, + + /** Permission to read video files that your app didn't create. + Has the same effect as readExternalStorage on iOS and Android versions before 33. + */ + readMediaVideo = 8 }; //============================================================================== diff --git a/modules/juce_core/native/juce_android_RuntimePermissions.cpp b/modules/juce_core/native/juce_android_RuntimePermissions.cpp index 1bbc073b75..68ab453ec8 100644 --- a/modules/juce_core/native/juce_android_RuntimePermissions.cpp +++ b/modules/juce_core/native/juce_android_RuntimePermissions.cpp @@ -24,15 +24,39 @@ namespace juce { //============================================================================== -static String jucePermissionToAndroidPermission (RuntimePermissions::PermissionID permission) +static StringArray jucePermissionToAndroidPermissions (RuntimePermissions::PermissionID permission) { + const auto externalStorageOrMedia = [] (const auto* newPermission) + { + return getAndroidSDKVersion() < 33 ? "android.permission.READ_EXTERNAL_STORAGE" : newPermission; + }; + switch (permission) { - case RuntimePermissions::recordAudio: return "android.permission.RECORD_AUDIO"; - case RuntimePermissions::bluetoothMidi: return "android.permission.ACCESS_FINE_LOCATION"; - case RuntimePermissions::readExternalStorage: return "android.permission.READ_EXTERNAL_STORAGE"; - case RuntimePermissions::writeExternalStorage: return "android.permission.WRITE_EXTERNAL_STORAGE"; - case RuntimePermissions::camera: return "android.permission.CAMERA"; + case RuntimePermissions::recordAudio: return { "android.permission.RECORD_AUDIO" }; + case RuntimePermissions::bluetoothMidi: return { "android.permission.ACCESS_FINE_LOCATION" }; + case RuntimePermissions::writeExternalStorage: return { "android.permission.WRITE_EXTERNAL_STORAGE" }; + case RuntimePermissions::camera: return { "android.permission.CAMERA" }; + + case RuntimePermissions::readExternalStorage: + { + // See: https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE + if (getAndroidSDKVersion() < 33) + return { "android.permission.READ_EXTERNAL_STORAGE" }; + + return { "android.permission.READ_MEDIA_AUDIO", + "android.permission.READ_MEDIA_IMAGES", + "android.permission.READ_MEDIA_VIDEO" }; + } + + case RuntimePermissions::readMediaAudio: + return { externalStorageOrMedia ("android.permission.READ_MEDIA_AUDIO") }; + + case RuntimePermissions::readMediaImages: + return { externalStorageOrMedia ("android.permission.READ_MEDIA_IMAGES") }; + + case RuntimePermissions::readMediaVideo: + return { externalStorageOrMedia ("android.permission.READ_MEDIA_VIDEO") }; } // invalid permission @@ -42,53 +66,28 @@ static String jucePermissionToAndroidPermission (RuntimePermissions::PermissionI static RuntimePermissions::PermissionID androidPermissionToJucePermission (const String& permission) { - if (permission == "android.permission.RECORD_AUDIO") return RuntimePermissions::recordAudio; - else if (permission == "android.permission.ACCESS_FINE_LOCATION") return RuntimePermissions::bluetoothMidi; - else if (permission == "android.permission.READ_EXTERNAL_STORAGE") return RuntimePermissions::readExternalStorage; - else if (permission == "android.permission.WRITE_EXTERNAL_STORAGE") return RuntimePermissions::writeExternalStorage; - else if (permission == "android.permission.CAMERA") return RuntimePermissions::camera; + static const std::map map + { + { "android.permission.RECORD_AUDIO", RuntimePermissions::recordAudio }, + { "android.permission.ACCESS_FINE_LOCATION", RuntimePermissions::bluetoothMidi }, + { "android.permission.READ_EXTERNAL_STORAGE", RuntimePermissions::readExternalStorage }, + { "android.permission.WRITE_EXTERNAL_STORAGE", RuntimePermissions::writeExternalStorage }, + { "android.permission.CAMERA", RuntimePermissions::camera }, + { "android.permission.READ_MEDIA_AUDIO", RuntimePermissions::readMediaAudio }, + { "android.permission.READ_MEDIA_IMAGES", RuntimePermissions::readMediaImages }, + { "android.permission.READ_MEDIA_VIDEO", RuntimePermissions::readMediaVideo }, + }; - return static_cast (-1); + const auto iter = map.find (permission); + return iter != map.cend() ? iter->second + : static_cast (-1); } //============================================================================== struct PermissionsRequest { - PermissionsRequest() {} - - // using "= default" on the following method triggers an internal compiler error - // in Android NDK 17 - PermissionsRequest (const PermissionsRequest& o) - : callback (o.callback), permission (o.permission) - {} - - PermissionsRequest (PermissionsRequest&& o) - : callback (std::move (o.callback)), permission (o.permission) - { - o.permission = static_cast (-1); - } - - PermissionsRequest (RuntimePermissions::Callback && callbackToUse, - RuntimePermissions::PermissionID permissionToRequest) - : callback (std::move (callbackToUse)), permission (permissionToRequest) - {} - - PermissionsRequest& operator= (const PermissionsRequest & o) - { - callback = o.callback; - permission = o.permission; - return *this; - } - - PermissionsRequest& operator= (PermissionsRequest && o) - { - callback = std::move (o.callback); - permission = o.permission; - return *this; - } - RuntimePermissions::Callback callback; - RuntimePermissions::PermissionID permission; + RuntimePermissions::PermissionID permission = static_cast (-1); }; //============================================================================== @@ -165,8 +164,7 @@ struct PermissionsOverlay : FragmentOverlay { auto &request = requests.front(); - StringArray permissionsArray{ - jucePermissionToAndroidPermission (request.permission)}; + auto permissionsArray = jucePermissionToAndroidPermissions (request.permission); auto jPermissionsArray = juceStringArrayToJava (permissionsArray); @@ -199,9 +197,16 @@ struct PermissionsOverlay : FragmentOverlay //============================================================================== void RuntimePermissions::request (PermissionID permission, Callback callback) { - auto requestedPermission = jucePermissionToAndroidPermission (permission); + const auto requestedPermissions = jucePermissionToAndroidPermissions (permission); - if (! isPermissionDeclaredInManifest (requestedPermission)) + const auto allPermissionsInManifest = std::all_of (requestedPermissions.begin(), + requestedPermissions.end(), + [] (const auto& p) + { + return isPermissionDeclaredInManifest (p); + }); + + if (! allPermissionsInManifest) { // 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 @@ -220,7 +225,7 @@ void RuntimePermissions::request (PermissionID permission, Callback callback) return; } - PermissionsRequest request (std::move (callback), permission); + PermissionsRequest request { std::move (callback), permission }; static CriticalSection overlayGuard; ScopedLock lock (overlayGuard); @@ -250,12 +255,15 @@ bool RuntimePermissions::isGranted (PermissionID permission) { auto* env = getEnv(); - auto requestedPermission = jucePermissionToAndroidPermission (permission); - int result = env->CallIntMethod (getAppContext().get(), AndroidContext.checkCallingOrSelfPermission, - javaString (requestedPermission).get()); + const auto requestedPermissions = jucePermissionToAndroidPermissions (permission); + return std::all_of (requestedPermissions.begin(), requestedPermissions.end(), [env] (const auto& p) + { + return 0 == env->CallIntMethod (getAppContext().get(), + AndroidContext.checkCallingOrSelfPermission, + javaString (p).get()); + }); - return result == 0 /* PERMISSION_GRANTED */; } } // namespace juce