This decidedly irritating situation comes about because when the .apk
is built, some assets are compressed before storing them, whereas other are treated as already compressed (e.g. images, video) and are left alone. The latter group can be opened using openAssetFd
, the former group can't - if you try, you get the "This file can not be opened as a file descriptor; it is probably compressed" error.
One option is to trick the build system into not compressing the assets (see the link in @nicstrong's answer), but this is fiddly. Better to try and work around the problem in a more predictable fashion.
The solution I cam up with uses the fact that while you can't open an AssetFileDescriptor
for the asset, you can still open an InputStream
. You can use this to copy the asset into the application's file cache, and then return a descriptor for that:
@Override
public AssetFileDescriptor openAssetFile(final Uri uri, final String mode) throws FileNotFoundException
{
final String assetPath = uri.getLastPathSegment(); // or whatever
try
{
final boolean canBeReadDirectlyFromAssets = ... // if your asset going to be compressed?
if (canBeReadDirectlyFromAssets)
{
return getContext().getAssets().openFd(assetPath);
}
else
{
final File cacheFile = new File(getContext().getCacheDir(), assetPath);
cacheFile.getParentFile().mkdirs();
copyToCacheFile(assetPath, cacheFile);
return new AssetFileDescriptor(ParcelFileDescriptor.open(cacheFile, MODE_READ_ONLY), 0, -1);
}
}
catch (FileNotFoundException ex)
{
throw ex;
}
catch (IOException ex)
{
throw new FileNotFoundException(ex.getMessage());
}
}
private void copyToCacheFile(final String assetPath, final File cacheFile) throws IOException
{
final InputStream inputStream = getContext().getAssets().open(assetPath, ACCESS_BUFFER);
try
{
final FileOutputStream fileOutputStream = new FileOutputStream(cacheFile, false);
try
{
//using Guava IO lib to copy the streams, but could also do it manually
ByteStreams.copy(inputStream, fileOutputStream);
}
finally
{
fileOutputStream.close();
}
}
finally
{
inputStream.close();
}
}
This does mean that your app will leave cache files lying about, but that's fine. It also doesn't attempt to re-use existing cache files, which you may or may not care about.