Use of sound files in TTS on Marshmallow (Android 6) fails with permission issues

1k views Asked by At

Using addSpeech() in android TTS, you can link a certain text to a sound file. Then, the TTS engine plays the file instead of synthesizing the sound of the text (also in question at Android TTS(Text to Speech)'s addSpeech() and speak() can't play a sound file in the external storage from marshmallow(api 23) above, with Google TTS ). This is not working in Android 6.0 with TTS version 3.9.14 (and 3.10.10). So far, I did not see ant post with an answer as to why this is not working in Android 6.0. So I thought I'll provide more data on the issue that could help someone figure out the issue. (I had added this to the question in the link above, but moderators deleted it saying this is not an answer. They did not suggest a way to add more data like this except saying ask another question. Hence this question. In reality this is additional data on the same question that is not answered yet). so here goes.

In the manifest I have provided both read and write permissions to the app using TTS (which in turn uses Media Player) to play the voice files provided.

android:targetSdkVersion="22"
...
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Note that this works fine up to Android 5.0, but fails on android 6.0 (except when files in res/raw are used as voice files). Also when it fails it seems to give slightly different error in logcat depending on whether the voice file is on internal or external storage.

  1. When the voice file in res/raw folder, TTS works fine playing the required voice file using the resource id (addSpeech (word, pkgName, resId))

  2. When the voice file is located on external storage (/storage/sdcard0/pkgName/soundFiles/.. playing using TTS gives the EACCES failure log (for both amr and mp3 files).

    09-08 16:57:17.514 1549-7830/? D/MediaPlayer: create failed: 
    java.io.FileNotFoundException: /storage/emulated/0/pkgName/soundFiles/voice1.amr: open failed: EACCES (Permission denied)
    at libcore.io.IoBridge.open(IoBridge.java:487)
    at java.io.FileInputStream.(FileInputStream.java:76)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1115)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1066)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1003)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:983)
    at android.media.MediaPlayer.create(MediaPlayer.java:890
    at android.speech.tts.AudioPlaybackQueueItem.run(AudioPlaybackQueueItem.java:58)
    at android.speech.tts.AudioPlaybackHandler$MessageLoop.run(AudioPlaybackHandler.java:134)
    at java.lang.Thread.run(Thread.java:818)
    Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
    at libcore.io.Posix.open(Native Method)
    at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
    at libcore.io.IoBridge.open(IoBridge.java:473)
    at java.io.FileInputStream.(FileInputStream.java:76)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1115)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1066)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1003)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:983)
    at android.media.MediaPlayer.create(MediaPlayer.java:890)
    at android.speech.tts.AudioPlaybackQueueItem.run(AudioPlaybackQueueItem.java:58)
    at android.speech.tts.AudioPlaybackHandler$MessageLoop.run(AudioPlaybackHandler.java:134)
    at java.lang.Thread.run(Thread.java:818
    
  3. When the voice file is located on internal storage (/data/.../pkgName/soundFiles/.. playing the word using TTS gives the following error log (for both amr and mp3 files).

    09-08 17:24:23.103 1549-32732/? D/MediaPlayer: create failed: 
    java.io.IOException: setDataSource failed.
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1120)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1066)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1003)
    at android.media.MediaPlayer.setDataSource(MediaPlayer.java:983)
    at android.media.MediaPlayer.create(MediaPlayer.java:890)
    at android.speech.tts.AudioPlaybackQueueItem.run(AudioPlaybackQueueItem.java:58)        
    at android.speech.tts.AudioPlaybackHandler$MessageLoop.run(AudioPlaybackHandler.java:134)
    at java.lang.Thread.run(Thread.java:818)
    

As an experiment, created a MediaPlayer object in the same class where TTS is being used and played the same sound files that are failing with TTS. They played fine without any issue. So it looks like only the instantiation of MediaPlayer within TTS is having file permission issues.

Any help is appreciated.

Edit: Please note, runtime permissions have been granted, so this is not the issue. The problem is limited to the Google Text to Speech engine. Other engines work correctly.

3

There are 3 answers

0
Darkhorse1870 On

As an experiment, created a MediaPlayer object in the same class where TTS is being used and played the same sound files that are failing with TTS. They played fine without any issue. So it looks like only the instantiation of MediaPlayer within TTS is having file permission issues.

Any help is appreciated.

The problem is, "MediaPlayer within TTS" is running in the other process, which does not have an access to the application files on actual (23+) Android versions.

In addSpeech(CharSequence text, File file) function, TextToSpeech internally converts File to Uri for interprocess communication using Uri.fromFile(file) function, which produces the "file://" Uri, but for apps targeting Android 7.0, the Android framework enforces the StrictMode API policy that prohibits exposing file:// URIs outside your app, so this is completely useless !!!

It produces the funny collision I have faced with: you can cache an utterance to file with synthesizeToFile(CharSequence text, Bundle params, File file, String utteranceId) TextToSpeech function, but you can not use this file later in addSpeech(CharSequence text, File file).

My workaround for this mess is as follows:

  1. Use FileProvider (from AndroidX, android.support or create your own from scratch) to give an acces to the directory with your sound files. For example, to give an access to your "cache/tts" directory, put the following to your res/xml/file_paths.xml:
        <paths>
            <cache-path name="ttsCache" path="tts" />
        </paths>
  1. Use the following code to add your sound file to TTS:
        File cacheFile = new File(ttsCacheDir, speechFileName);
        if (cacheFile.isFile()) {
            Uri uri = FileProvider.getUriForFile(context, FileProvider.AUTHORITY, cacheFile);
            context.grantUriPermission(textToSpeech.getDefaultEngine(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                textToSpeech.addSpeech(speechText, uri);
            } else {
                textToSpeech.addSpeech(speechText, uri.toString());
            }
        }
0
raining211 On

I'm the guy who posted the link below you mentioned. Android TTS(Text to Speech)'s addSpeech() and speak() can't play a sound file in the external storage from marshmallow(api 23) above, with Google TTS

Thank you for all the experiments. I agree with you that the mediaplayer in TTS can't read the audio files in the internal/external storage.

At that time, I set my target device at 22 or something to avoid the issue using MODE_WORLD_READABLE. Now that it's impossible to set the target so low, the best option I can think of is to make a function that works as 'addSpeech()' yourself. It's not that hard to play an audio file instead of a certain text with MediaPlayer. The tricky part, however, is about how to deal with 'utteranceId' in your workaround if you need it.

One of the options is you place 'playSilentUtterance(final long durationInMs, final int queueMode, final String utteranceId)' in your MediaPlayer onCompleteListener so that it can trigger the utteranceId you want. You can put a very short time like 100millisec in durationInMs.

I hope this helps anyone who is stuck with the issue, and hopefully Google resolves this issue.

6
Varun Chandran On

I think it fails because you have to add runtime permissions(like you need to ask permission while user using that feature ) in android phones above lollipop 5.1 OS,

https://developer.android.com/training/permissions/requesting.html

Check this link for the best understanding of this problem. And if need to know how it can be done simply just check this code below.

https://www.androidhive.info/2016/11/android-working-marshmallow-m-runtime-permissions/

here is the code:

if (ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)
    != PackageManager.PERMISSION_GRANTED) {

    // Should we show an explanation?
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.WRITE_EXTERNAL_STORAGE)) {

        // Show an explanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.

    } else {

        // No explanation needed, we can request the permission.

        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);

        // MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE is an
        // app-defined int constant. The callback method gets the
        // result of the request.
    }
}