Search code examples
androidgoogle-drive-android-api

Google Drive & Android - read failed: EBADF (Bad file number)


So I have a piece of code which works correctly on all devices and emulators I have access to, except one. It's a cheap old Android device, Huawei Y330-U01, running 4.2.2. I'm compiling with com.google.android.gms:play-services-drive:9.8.0. It's absolutely standard, as far as I can tell.

I get the file, which is over a megabyte of plain text, and I can read it character by character, for a few thousand characters (the amount varies, and not between numbers which are powers of two or anything), before getting the error

IOException while testing the stream's first character
java.io.IOException: read failed: EBADF (Bad file number)
    at libcore.io.IoBridge.read(IoBridge.java:486)
    at java.io.FileInputStream.read(FileInputStream.java:179)
    at libcore.io.Streams.readSingleByte(Streams.java:41)
    at java.io.FileInputStream.read(FileInputStream.java:175)
    at com.suchideas.android.alamode.sync.SyncActivity$b.run(Unknown Source)
 Caused by: libcore.io.ErrnoException: read failed: EBADF (Bad file number)
    at libcore.io.Posix.readBytes(Native Method)
    at libcore.io.Posix.read(Posix.java:123)
    at libcore.io.BlockGuardOs.read(BlockGuardOs.java:149)
    at libcore.io.IoBridge.read(IoBridge.java:476)
    at java.io.FileInputStream.read(FileInputStream.java:179) 
    at libcore.io.Streams.readSingleByte(Streams.java:41) 
    at java.io.FileInputStream.read(FileInputStream.java:175) 
    at com.suchideas.android.alamode.sync.SyncActivity$b.run(Unknown Source) 

I'm pretty confident this is something like running out of RAM or disk space (there's certainly more than enough enough space for this file, by hundreds of megabytes, but the device does like to complain about storage) and clearing away something which was actually in use. Again, to reiterate, this code works perfectly on emulators of the same Android version, and all other devices tested.

So. Is there a fix, do you think?

Here's the code, you should be able to fill in the gaps...

if (!mGoogleApiClient.isConnected()) {
    mGoogleApiClient.connect();
    while (mGoogleApiClient.isConnecting()) {
        try {
            sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    if (!mGoogleApiClient.isConnected())
        return;
}

appFolder = Drive.DriveApi.getAppFolder(mGoogleApiClient);
Query query = new Query.Builder()
        .addFilter(Filters.eq(SearchableField.TITLE, UPLOADED_DATABASE_NAME))
        .build();
DriveApi.MetadataBufferResult metadataBufferResult = appFolder.queryChildren(mGoogleApiClient, query).await();
if (!metadataBufferResult.getStatus().isSuccess()) {
    metadataBufferResult.release();
    return;
}

MetadataBuffer databaseFileResults = metadataBufferResult.getMetadataBuffer();
if (databaseFileResults.getCount() == 0) {
    return;
}
Metadata md = databaseFileResults.get(0);
Log.d(TAG, "Database file retrieved [" + md.getFileSize() + "B]. Created " + md.getCreatedDate() + ", modified " + md.getModifiedDate() + ".");
DriveId databaseFileID = md.getDriveId();
databaseFileResults.release();
metadataBufferResult.release();

DriveFile databaseFile = databaseFileID.asDriveFile();
DriveApi.DriveContentsResult driveContentsResult = databaseFile.open(mGoogleApiClient, DriveFile.MODE_READ_ONLY, new DriveFile.DownloadProgressListener() {
    @Override
    public void onProgress(long downloaded, long expected) {

    }
}).await();
if (!driveContentsResult.getStatus().isSuccess()) {
    return;
}
DriveContents driveContents = driveContentsResult.getDriveContents();

InputStream in = driveContents.getInputStream();

try {
    int c = 0;
    for(int i = 0; true; i++) {
        c = in.read();
        if(c == -1) break;
        Log.d(TAG, "Character "+i+": "+(char)c);
    }
} catch (IOException e) {
    Log.e(TAG, "IOException while testing the stream character", e);
    return;
}

Solution

  • Okay, so one can almost certainly do better than this (I don't think you need to read character by character, some buffering is probably okay), but after a few hours of battling, I found a way to avoid triggering the issue on this device.

    In practice, I would recommend trying a normal driveContents.getInputStream() first. Then one can catch the sort of errors discussed above, and only turn to this approach if it becomes necessary.

    But it works.

    The approach: open the DriveContents directly from its FileDescriptor rather than through an InputStream. Gradually build this up in a buffer (I'm just using a StringBuilder here, since this was proof-of-concept). Catch IOExceptions, and if you've successfully read at least some data, start all over again, and keep going, until you reach the end of the string.

    private static String safeDriveFileToString(DriveContents driveContents) throws IOException {
        StringBuilder sb = new StringBuilder();
    
        InputStream in;
    
        int n = 0, nPrevious = 0;
        while(true) {
            in = new FileInputStream(driveContents.getParcelFileDescriptor().getFileDescriptor());
            try {
                int toSkip = n;
                while(toSkip > 0) {
                    toSkip -= in.skip(toSkip);
                }
    
                int c;
                while ((c = in.read()) != -1) {
                    sb.append((char) c);
                    n++;
                }
    
                if(c == -1) break;
            } catch (IOException e) {
                if(nPrevious == n) {
                    throw e;
                } else {
                    Log.e(TAG, "Ignoring error part-way through a file:", e);
                }
                nPrevious = n;
            }
        }
    
        return sb.toString();
    }
    

    Wanna know the weirdest thing? After reading this file once with such an approach, it now always works without needing to recourse to this. Absolutely bizarre.