diff --git a/src/android/AudioHandler.java b/src/android/AudioHandler.java index 9e734c440..e3db74775 100644 --- a/src/android/AudioHandler.java +++ b/src/android/AudioHandler.java @@ -544,7 +544,13 @@ private void promptForRecord() } else if(PermissionHelper.hasPermission(this, permissions[RECORD_AUDIO])) { - getWritePermission(WRITE_EXTERNAL_STORAGE); + //do not need permission, because it will save to scoped storage directories + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this.startRecordingAudio(recordId, FileHelper.stripFileProtocol(fileUriStr)); + } + else { + getWritePermission(WRITE_EXTERNAL_STORAGE); + } } else { diff --git a/src/android/AudioPlayer.java b/src/android/AudioPlayer.java index b40e281ed..7bd5ce665 100644 --- a/src/android/AudioPlayer.java +++ b/src/android/AudioPlayer.java @@ -38,6 +38,9 @@ Licensed to the Apache Software Foundation (ASF) under one import java.io.OutputStream; import java.io.IOException; import java.util.LinkedList; +import org.apache.cordova.PermissionHelper; +import android.Manifest; +import android.os.Build; /** * This class implements the audio playback and recording capabilities used by Cordova. @@ -109,7 +112,23 @@ public AudioPlayer(AudioHandler handler, String id, String file) { private String generateTempFile() { String tempFileName = null; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - tempFileName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording-" + System.currentTimeMillis() + ".3gp"; + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //deprecated after sdk 29, + + //if we have storage write permission we keep doing it this way + if(hasWritePermission()) { + tempFileName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording-" + System.currentTimeMillis() + ".3gp"; + } else { + //otherwise we get a directory private to the app (scoped storage) + //new way + tempFileName = this.handler.cordova.getActivity().getApplicationContext().getExternalFilesDir(null).getAbsolutePath() + "/tmprecording-" + System.currentTimeMillis() + ".3gp"; + } + + } else { + //prior to sdk 29, we keep asking permissions as before so we write to the same place as before + tempFileName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording-" + System.currentTimeMillis() + ".3gp"; + } + } else { tempFileName = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/tmprecording-" + System.currentTimeMillis() + ".3gp"; } @@ -186,10 +205,24 @@ public void moveFile(String file) { if (!file.startsWith("/")) { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file; + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //deprecated after sdk 29, + //if we have storage write permission we keep doing it this way + if(hasWritePermission()) { + file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file; + } else { + //new path + file = this.handler.cordova.getActivity().getApplicationContext().getExternalFilesDir(null).getAbsolutePath() + File.separator + file; + } + } else { + //same as before + file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file; + } + } else { file = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/" + file; } + } int size = this.tempFiles.size(); @@ -649,6 +682,10 @@ private boolean readyPlayer(String file) { return true; } } else { + if (this.player == null) { + this.player = new MediaPlayer(); + this.player.setOnErrorListener(this); + } //reset the player this.player.reset(); try { @@ -698,7 +735,20 @@ private void loadAudioFile(String file) throws IllegalArgumentException, Securit fileInputStream.close(); } else { - this.player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/" + file); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //deprecated after sdk 29, + //if we have storage write permission we keep doing it this way + if(hasWritePermission()) { + this.player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/" + file); + } else { + //new way + this.player.setDataSource(this.handler.cordova.getActivity().getApplicationContext().getExternalFilesDir(null).getPath() + "/" + file); + } + + } else { + this.player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/" + file); + } + } } this.setState(STATE.MEDIA_STARTING); @@ -757,4 +807,8 @@ public float getCurrentAmplitude() { } return 0; } + + private boolean hasWritePermission() { + return PermissionHelper.hasPermission(this.handler, Manifest.permission.WRITE_EXTERNAL_STORAGE); + } } diff --git a/src/ios/CDVSound.m b/src/ios/CDVSound.m index d63b0ab2d..034cc3d4e 100644 --- a/src/ios/CDVSound.m +++ b/src/ios/CDVSound.m @@ -380,8 +380,14 @@ - (void)startPlayingAudio:(CDVInvokedUrlCommand*)command } if (!bError) { NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + + BOOL isLocalFile = [audioFile.resourceURL isFileURL] || [resourcePath hasPrefix:CDVFILE_PREFIX] || [resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]; + double duration = 0; - if (avPlayer.currentItem && avPlayer.currentItem.asset) { + //avoid play a file:// or a cdvfile:// with the incorrect player (if before we played a remote sound) + //it can happen when we alternate between local & remote URLS. + //as a consequence we don“t hear the sounds. + if (avPlayer.currentItem && avPlayer.currentItem.asset && !isLocalFile) { CMTime time = avPlayer.currentItem.asset.duration; duration = CMTimeGetSeconds(time); if (isnan(duration)) { @@ -459,6 +465,9 @@ - (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId // create the player NSURL* resourceURL = audioFile.resourceURL; + //check if the file is local or over network + BOOL isLocalFile = [resourceURL isFileURL] || [resourceURL.absoluteString hasPrefix:CDVFILE_PREFIX] || [resourceURL.absoluteString hasPrefix:DOCUMENTS_SCHEME_PREFIX]; + if ([resourceURL isFileURL]) { audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:resourceURL error:&playerError]; } else { @@ -496,8 +505,14 @@ - (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId } else { audioFile.player.mediaId = mediaId; audioFile.player.delegate = self; - if (avPlayer == nil) - bError = ![audioFile.player prepareToPlay]; + //if (avPlayer == nil) ?? + //one should not be related with the other, what matters is the player that we are going to use & the resource location + //without this check if we previously played a remote sound we have a avPlayer instance with an currentItem, therefore the + //prepareToPlay above would not be called, and the current sound could end up on the wrong player + if(isLocalFile) { + bError = ![audioFile.player prepareToPlay]; + } + } return bError; }