58

Currently, I'm using Google Drive Android API, to store my Android app data, to Google Drive App Folder.

This is what I'm doing when saving my application data

  1. Generate a checksum for the current local zip file.
  2. Search in Google Drive App Folder, to see whether there is an existing App Folder zip file.
  3. If there is, overwrite the content of existing App Folder zip file, with current local zip files. Also, we will rename existing App Folder zip filename, with the latest checksum.
  4. If there isn't existing App Folder zip file, generate a new App Folder zip file, with local zip file's content. We will use the latest checksum as App Folder zip filename.

Here's the code which performs the above-mentioned operations.

Generate new App Folder zip file, or update existing App Folder zip file

public static boolean saveToGoogleDrive(GoogleApiClient googleApiClient, File file, HandleStatusable h, PublishProgressable p) {
    // Should we new or replace?

    GoogleCloudFile googleCloudFile = searchFromGoogleDrive(googleApiClient, h, p);

    try {
        p.publishProgress(JStockApplication.instance().getString(R.string.uploading));

        final long checksum = org.yccheok.jstock.gui.Utils.getChecksum(file);
        final long date = new Date().getTime();
        final int version = org.yccheok.jstock.gui.Utils.getCloudFileVersionID();
        final String title = getGoogleDriveTitle(checksum, date, version);

        DriveContents driveContents;
        DriveFile driveFile = null;

        if (googleCloudFile == null) {
            DriveApi.DriveContentsResult driveContentsResult = Drive.DriveApi.newDriveContents(googleApiClient).await();

            if (driveContentsResult == null) {
                return false;
            }

            Status status = driveContentsResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }

            driveContents = driveContentsResult.getDriveContents();

        } else {
            driveFile = googleCloudFile.metadata.getDriveId().asDriveFile();
            DriveApi.DriveContentsResult driveContentsResult = driveFile.open(googleApiClient, DriveFile.MODE_WRITE_ONLY, null).await();

            if (driveContentsResult == null) {
                return false;
            }

            Status status = driveContentsResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }

            driveContents = driveContentsResult.getDriveContents();
        }

        OutputStream outputStream = driveContents.getOutputStream();
        InputStream inputStream = null;

        byte[] buf = new byte[8192];

        try {
            inputStream = new FileInputStream(file);
            int c;

            while ((c = inputStream.read(buf, 0, buf.length)) > 0) {
                outputStream.write(buf, 0, c);
            }

        } catch (IOException e) {
            Log.e(TAG, "", e);
            return false;
        } finally {
            org.yccheok.jstock.file.Utils.close(outputStream);
            org.yccheok.jstock.file.Utils.close(inputStream);
        }

        if (googleCloudFile == null) {
            // Create the metadata for the new file including title and MIME
            // type.
            MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
                    .setTitle(title)
                    .setMimeType("application/zip").build();

            DriveFolder driveFolder = Drive.DriveApi.getAppFolder(googleApiClient);
            DriveFolder.DriveFileResult driveFileResult = driveFolder.createFile(googleApiClient, metadataChangeSet, driveContents).await();

            if (driveFileResult == null) {
                return false;
            }

            Status status = driveFileResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }
        } else {
            MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
                    .setTitle(title).build();

            DriveResource.MetadataResult metadataResult = driveFile.updateMetadata(googleApiClient, metadataChangeSet).await();
            Status status = metadataResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }
        }

        Status status;
        try {
            status = driveContents.commit(googleApiClient, null).await();
        } catch (java.lang.IllegalStateException e) {
            // java.lang.IllegalStateException: DriveContents already closed.
            Log.e(TAG, "", e);
            return false;
        }

        if (!status.isSuccess()) {
            h.handleStatus(status);
            return false;
        }

        status = Drive.DriveApi.requestSync(googleApiClient).await();
        if (!status.isSuccess()) {
            // Sync request rate limit exceeded.
            //
            //h.handleStatus(status);
            //return false;
        }

        return true;
    } finally {
        if (googleCloudFile != null) {
            googleCloudFile.metadataBuffer.release();
        }
    }
}

Search for existing App Folder zip file

private static String getGoogleDriveTitle(long checksum, long date, int version) {
    return "jstock-" + org.yccheok.jstock.gui.Utils.getJStockUUID() + "-checksum=" + checksum + "-date=" + date + "-version=" + version + ".zip";
}

// https://stackoverflow.com/questions/1360113/is-java-regex-thread-safe
private static final Pattern googleDocTitlePattern = Pattern.compile("jstock-" + org.yccheok.jstock.gui.Utils.getJStockUUID() + "-checksum=([0-9]+)-date=([0-9]+)-version=([0-9]+)\\.zip", Pattern.CASE_INSENSITIVE);

private static GoogleCloudFile searchFromGoogleDrive(GoogleApiClient googleApiClient, HandleStatusable h, PublishProgressable p) {
    DriveFolder driveFolder = Drive.DriveApi.getAppFolder(googleApiClient);

    // https://stackoverflow.com/questions/34705929/filters-ownedbyme-doesnt-work-in-drive-api-for-android-but-works-correctly-i
    final String titleName = ("jstock-" + org.yccheok.jstock.gui.Utils.getJStockUUID() + "-checksum=");
    Query query = new Query.Builder()
            .addFilter(Filters.and(
                Filters.contains(SearchableField.TITLE, titleName),
                Filters.eq(SearchableField.TRASHED, false)
            ))
            .build();

    DriveApi.MetadataBufferResult metadataBufferResult = driveFolder.queryChildren(googleApiClient, query).await();

    if (metadataBufferResult == null) {
        return null;
    }

    Status status = metadataBufferResult.getStatus();

    if (!status.isSuccess()) {
        h.handleStatus(status);
        return null;
    }

    MetadataBuffer metadataBuffer = null;
    boolean needToReleaseMetadataBuffer = true;

    try {
        metadataBuffer = metadataBufferResult.getMetadataBuffer();
        if (metadataBuffer != null ) {
            long checksum = 0;
            long date = 0;
            int version = 0;
            Metadata metadata = null;

            for (Metadata md : metadataBuffer) {
                if (p.isCancelled()) {
                    return null;
                }

                if (md == null || !md.isDataValid()) {
                    continue;
                }

                final String title = md.getTitle();

                // Retrieve checksum, date and version information from filename.
                final Matcher matcher = googleDocTitlePattern.matcher(title);
                String _checksum = null;
                String _date = null;
                String _version = null;
                if (matcher.find()){
                    if (matcher.groupCount() == 3) {
                        _checksum = matcher.group(1);
                        _date = matcher.group(2);
                        _version = matcher.group(3);
                    }
                }
                if (_checksum == null || _date == null || _version == null) {
                    continue;
                }

                try {
                    checksum = Long.parseLong(_checksum);
                    date = Long.parseLong(_date);
                    version = Integer.parseInt(_version);
                } catch (NumberFormatException ex) {
                    Log.e(TAG, "", ex);
                    continue;
                }

                metadata = md;

                break;

            }   // for

            if (metadata != null) {
                // Caller will be responsible to release the resource. If release too early,
                // metadata will not readable.
                needToReleaseMetadataBuffer = false;
                return GoogleCloudFile.newInstance(metadataBuffer, metadata, checksum, date, version);
            }
        }   // if
    } finally {
        if (needToReleaseMetadataBuffer) {
            if (metadataBuffer != null) {
                metadataBuffer.release();
            }
        }
    }

    return null;
}

The problem occurs, during loading application data. Imagine the following operations

  1. Upload zip data to Google Drive App Folder for the first time. The checksum is 12345. The filename being used is ...checksum=12345...zip
  2. Search for zip data from Google Drive App Folder. Able to find the file with filename ...checksum=12345...zip. Download the content. Verify the checksum of content is 12345 too.
  3. Overwrite new zip data to existing Google Drive App Folder file. New zip data checksum is 67890. The existing app folder zip file is renamed to ...checksum=67890...zip
  4. Search for zip data from Google Drive App Folder. Able to find the file with filename ...checksum=67890...zip. However, after downloading the content, the checksum of the content is still old 12345!

Download App Folder zip file

public static CloudFile loadFromGoogleDrive(GoogleApiClient googleApiClient, HandleStatusable h, PublishProgressable p) {
    final java.io.File directory = JStockApplication.instance().getExternalCacheDir();
    if (directory == null) {
        org.yccheok.jstock.gui.Utils.showLongToast(R.string.unable_to_access_external_storage);
        return null;
    }

    Status status = Drive.DriveApi.requestSync(googleApiClient).await();
    if (!status.isSuccess()) {
        // Sync request rate limit exceeded.
        //
        //h.handleStatus(status);
        //return null;
    }

    GoogleCloudFile googleCloudFile = searchFromGoogleDrive(googleApiClient, h, p);

    if (googleCloudFile == null) {
        return null;
    }

    try {
        DriveFile driveFile = googleCloudFile.metadata.getDriveId().asDriveFile();
        DriveApi.DriveContentsResult driveContentsResult = driveFile.open(googleApiClient, DriveFile.MODE_READ_ONLY, null).await();

        if (driveContentsResult == null) {
            return null;
        }

        status = driveContentsResult.getStatus();
        if (!status.isSuccess()) {
            h.handleStatus(status);
            return null;
        }

        final long checksum = googleCloudFile.checksum;
        final long date = googleCloudFile.date;
        final int version = googleCloudFile.version;

        p.publishProgress(JStockApplication.instance().getString(R.string.downloading));

        final DriveContents driveContents = driveContentsResult.getDriveContents();

        InputStream inputStream = null;
        java.io.File outputFile = null;
        OutputStream outputStream = null;

        try {
            inputStream = driveContents.getInputStream();
            outputFile = java.io.File.createTempFile(org.yccheok.jstock.gui.Utils.getJStockUUID(), ".zip", directory);
            outputFile.deleteOnExit();
            outputStream = new FileOutputStream(outputFile);

            int read = 0;
            byte[] bytes = new byte[1024];

            while ((read = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, read);
            }
        } catch (IOException ex) {
            Log.e(TAG, "", ex);
        } finally {
            org.yccheok.jstock.file.Utils.close(outputStream);
            org.yccheok.jstock.file.Utils.close(inputStream);
            driveContents.discard(googleApiClient);
        }

        if (outputFile == null) {
            return null;
        }

        return CloudFile.newInstance(outputFile, checksum, date, version);
    } finally {
        googleCloudFile.metadataBuffer.release();
    }
}

First, I thought

Status status = Drive.DriveApi.requestSync(googleApiClient).await()

doesn't do the job well. It fails in most of the situation, with error message Sync request rate limit exceeded. In fact, the hard limit imposed in requestSync, make that API not particularly useful - Android Google Play / Drive Api


However, even when requestSync success, loadFromGoogleDrive still can only get the latest filename, but outdated checksum content.

I'm 100% sure loadFromGoogleDrive is returning me a cached data content, with the following observations.

  1. I install a DownloadProgressListener in driveFile.open, bytesDownloaded is 0 and bytesExpected is -1.
  2. If I use Google Drive Rest API, with the following desktop code, I can find the latest filename with correct checksum content.
  3. If I uninstall my Android app and re-install again, loadFromGoogleDrive will able to get the latest filename with correct checksum content.

Is there any robust way, to avoid from always loading cached app data from Google Drive?


I manage to produce a demo. Here are the steps to reproduce this problem.

Step 1: Download source code

https://github.com/yccheok/google-drive-bug

Step 2 : Setup in API console

enter image description here

Step 3: Press button SAVE "123.TXT" WITH CONTENT "123"

enter image description here

A file with filename "123.TXT", content "123" will create in the app folder.

Step 4: Press button SAVE "456.TXT" WITH CONTENT "456"

enter image description here

The previous file will be renamed to "456.TXT", with content updated to "456"

Step 5: Press button LOAD LAST SAVED FILE

enter image description here

File with filename "456.TXT" was found, but the previous cached content "123" is read. I was expecting content "456".

Take note that, if we

  1. Uninstall demo app.
  2. Re-install demo app.
  3. Press button LOAD LAST SAVED FILE, file with filename "456.TXT" and content "456" is found.

I had submitted issues report officially - https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4727


Other info

This is how it looks like under my device - http://youtu.be/kuIHoi4A1c0

I realise, not all users will hit with this problem. For instance, I had tested with another Nexus 6, Google Play Services 9.4.52 (440-127739847). The problem doesn't appear.

I had compiled an APK for testing purpose - https://github.com/yccheok/google-drive-bug/releases/download/1.0/demo.apk

Community
  • 1
  • 1
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • 4
    When I had similar problems a while ago, I ended up using the "regular" Java API as a workaround. Did you consider filing a bug about the requestSync issues -- what you describe above doesn't sound like intended behavior...? – Stefan Haustein Aug 13 '16 at 19:24
  • 2
    @StefanHaustein Before Google Play Services, I were using "regular" Java Rest API too. Due to the newly required permission http://stackoverflow.com/a/34701518/72437 in regular Java Rest API, I decide switch over to Google Drive Android API. Now, I realize such decision does more harm than good. – Cheok Yan Cheng Aug 14 '16 at 14:11
  • @StefanHaustein I don't think I want to file a bug report at this moment. As, even when they fix `requestSync` behavior, the strictly imposed limit (Sync request rate limit exceeded) on `requestSync` - http://stackoverflow.com/q/31203255/72437 , make that API not particular useful. – Cheok Yan Cheng Aug 14 '16 at 14:13
  • @StefanHaustein I realize you work in Google. Do you know anyone who is working on Google Drive Android API? Not sure you are able to draw his attention on this question. Would like to hear some input from them :) – Cheok Yan Cheng Aug 14 '16 at 14:27
  • I think we are encouraged to use the official channels (= file a bug) -- which helps ensuring that they actually work. See http://stackoverflow.com/questions/27697741/what-could-be-the-cause-for-remote-data-corruption-when-using-the-google-drive-a – Stefan Haustein Aug 14 '16 at 14:46
  • @StefanHaustein ok. Thanks for the link. I'm in the process of producing a minimal workable example to demonstrate the bug. https://github.com/yccheok/google-drive-bug I will let you know once it is done. Thank you very much. – Cheok Yan Cheng Aug 15 '16 at 02:10
  • It seems like your metadata update request is succeeding but the content request is failing, can you try the driveContents.commit request with the metadataChangeset (instead of null)? – Shailendra Aug 15 '16 at 05:42
  • @Shailendra I had modified the code by using driveContents.commit request with the metadataChangeset https://gist.github.com/yccheok/2676fc3383116945ca1fac3bbf5a3b1f The `saveToGoogleDrive` returns true. Still, same problem occurs if you download -> save -> download again. It still get the cached content. I'm still in the process of producing a minimal workable example to demonstrate the problem. – Cheok Yan Cheng Aug 15 '16 at 16:53
  • @Shailendra Helo, I manage to produce a minimal workable example, to demonstrate this problem. Do you mind to take a look? Is there any mistake I had made in the source code, or is this bug in API? Thank you very much. – Cheok Yan Cheng Aug 17 '16 at 15:39
  • @StefanHaustein Helo, I manage to produce a minimal workable example, to demonstrate this problem. Do you mind to take a look in it? Thank you very much. – Cheok Yan Cheng Aug 17 '16 at 15:39
  • Did you file a bug here: https://code.google.com/a/google.com/p/apps-api-issues/ ? This will make sure this is routed to the right team. – Stefan Haustein Aug 17 '16 at 20:38
  • @StefanHaustein I hope I had explained the issues in a clear manner - https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4727 – Cheok Yan Cheng Aug 18 '16 at 14:24
  • What Android version and device are you running? I tested your demo on Nexus 5 6.0.1 MOB30Y and it is absolutely alright. Did you try to run app on other Drive account? – pr0gramist Aug 19 '16 at 00:51
  • @PoprostuRonin I'm using Nexus 5 6.0.1 MOB30Y too. Are you updated with latest Google Play Services? What if you try to click on "Save 123.." and "Save 456.." alternatively multiple times, before clicking on "LOAD LAST SAVED FILE"? Will you get 123.TXT with content "456" (Or 456.TXT with content "123") ? – Cheok Yan Cheng Aug 19 '16 at 03:49
  • @PoprostuRonin May I know, do you only have 1 Google account, or multiple Google accounts in your Nexus 5? – Cheok Yan Cheng Aug 19 '16 at 03:50
  • I have Play services version 9.4.52. Here is a video, me trying to reproduce the bug: https://drive.google.com/open?id=0BwVbx1E4ikgURDhRa2JXX2FQWFU I tried the same without WiFi and even with *Restrict background data* it didn't break. – pr0gramist Aug 19 '16 at 12:14
  • I also tried on multiple accounts the bug did not appear. – pr0gramist Aug 19 '16 at 14:59
  • @PoprostuRonin I'm really appreciate your input. This is how it looks like under my device - https://youtu.be/kuIHoi4A1c0 I realize, not all users will hit with this problem. For instance, I had tested with another Nexus 6, Google Play Services 9.4.52 (440-127739847). The problem doesn't appear – Cheok Yan Cheng Aug 19 '16 at 18:34
  • @PoprostuRonin I had compiled an APK for testing purpose - https://github.com/yccheok/google-drive-bug/releases/download/1.0/demo.apk – Cheok Yan Cheng Aug 19 '16 at 18:34
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/121373/discussion-between-poprosturonin-and-cheok-yan-cheng). – pr0gramist Aug 19 '16 at 18:41
  • Are you logging an error on driveContents.commit? If so, that would indicate that the commit wasn't successful, which would explain the outdated content. Perhaps try committing before you change the metadata. – Eric Koleda Aug 19 '16 at 18:59
  • Hi @EricKoleda pretty confirm the commit is success. As if it fails, it will return false to the caller. Caller will in turn showing error toast. – Cheok Yan Cheng Aug 19 '16 at 19:02
  • What a rotten design overwriting saved data. Yeah that's not going to delete everything someday is it when it fails? – danny117 Aug 22 '16 at 19:07
  • 1
    Hi Cheok, I too tried to reproduce but couldn't. Can you try performing the operations in airplane mode? i.e. go offline, save 123, save 456, load and verify if it's 456 or not. If that also produces incorrect result, even the local writes are failing & thus could be related to device storage. Try looking into adb logcat to see if any error messages are reported while saving the file. – Shailendra Aug 24 '16 at 07:54
  • @CheokYanCheng sorry that my answer did not help you. –  Aug 25 '16 at 10:13
  • @CheokYanCheng I'm seeing a similar issue, see here: http://stackoverflow.com/questions/39183265/google-drive-android-not-returning-latest-version-of-the-file. There's an important thing, don't know how it affects, though - I have 2 accounts on my Nexus device and I can use either of the in my app to access google drive. – khusrav Aug 27 '16 at 16:45
  • @Shailendra In my case, I tried the airplane mode. When in the mode, the sync (push the file and pull it back) works fine, same version retrieved. But, when I turn off the airplane mode, some older version is pulled. – khusrav Sep 02 '16 at 20:53
  • @CheokYanCheng I tried your program on my device, it works ok. Nexus 4 Android 5.1 – khusrav Sep 03 '16 at 15:54

1 Answers1

1
  1. Search on Google Drive is slow. Why not use properties of the base folder to store id of the zip file? https://developers.google.com/drive/v2/web/properties
  2. File names on Google Drive are not unique, you can upload multiple files with same names. The File ID returned by Google, however, is unique.
ashishb
  • 534
  • 7
  • 17