darksaga
darksaga

Reputation: 2186

Google Drive Change Subscriptions not working?

I'm trying to get 'Change Subscriptions' to work using the Drive API for Android, but been unsuccessful so far.

Here the simple use case:

As far as I understand, this is exactly what 'Change Subscriptions' are supposed to do for me. I'm using play services revision 27.

The problem I have:

A 'file content change' (or some other file event) made locally on one device is never properly propagated to the all other devices that subscribed to the same file.

Does anyone know of any solutions to this issue, or can point my to what I'm doing wrong?


I've written some simple testcode (see below), that only needs a connected googleApiClient, here's what I tested:

1.

device 1 creates a new testfile calling testFileWriteNew() and adds a change subscription to this file using testFileAddAndRemoveSubscription(), the expected log output:

testfile.txt created, driveId=DriveId:CAESABi0AyDAu9XZhVMoAA== resourceId=null
onCompletion; driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYtAMgwLvV2YVTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
STATUS_SUCCESS 
added subscription to testfile.txt, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYtAMgwLvV2YVTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU

2.

device 2 adds a change subscription to the same file using testFileAddAndRemoveSubscription(), the expected log output:

added subscription to testfile.txt, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU

As expected, the driveId is different on both devices, but the resourceId is the same 0B-sshen4iTFAN0htekFYNExuSEU, so that same 'cloud' file is referenced

3.

If I update the file with some new data via testFileUpdate I get the following on device 1:

testfile.txt updated, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYtAMgwLvV2YVTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU

and device 2:

testfile.txt updated, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU

4.

Unfortunately, the 'change of content' in the onChange method of the service is only triggered locally. A changed done by device 1 never reaches device 2 and vice versa. If I update the file using device 2 I see the following log on device 2 coming from the service:

onChange; driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
contentChanged
onChange; driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
metadataChanged

but I never see the onChange method being triggered on device 1, if device 2 triggered a change, which I would expect.


Code:

private boolean testFileWriteNew() {
    final DriveFolder folderRoot = Drive.DriveApi.getRootFolder(mGoogleApiClient);
    DriveContentsResult contentsResult = Drive.DriveApi.newDriveContents(mGoogleApiClient).await();
    if (!contentsResult.getStatus().isSuccess()) {
        return false;
    }
    DriveContents originalContents = contentsResult.getDriveContents();
    OutputStream os = originalContents.getOutputStream();

    try {
        os.write(String.valueOf(System.currentTimeMillis()).getBytes());
        MetadataChangeSet originalMetadata = new MetadataChangeSet.Builder().setTitle("testfile.txt").setMimeType("text/plain").build();
        // create the file in root
        DriveFolder.DriveFileResult fileResult = folderRoot.createFile(mGoogleApiClient, originalMetadata, originalContents, new ExecutionOptions.Builder().setNotifyOnCompletion(true).build()).await();
        if (!fileResult.getStatus().isSuccess()) {
            return false;
        }
        // check 'locally created' file, not yet synced to drive
        DriveResource.MetadataResult metadataResult = fileResult.getDriveFile().getMetadata(mGoogleApiClient).await();
        if (!metadataResult.getStatus().isSuccess()) {
            return false;
        }
        Log.d(TAG, "testfile.txt created, driveId=" + metadataResult.getMetadata().getDriveId().encodeToString() + " resourceId=" + metadataResult.getMetadata().getDriveId().getResourceId());
        return true;
    } catch (IOException ioe) {
        return false;
    }
}


private boolean testFileUpdate() {
    final DriveFolder folderRoot = Drive.DriveApi.getRootFolder(mGoogleApiClient);

    // find testfile
    DriveId testFile = null;
    MetadataBufferResult folderFilesSyncFolder = folderRoot.listChildren(mGoogleApiClient).await();
    if (!folderFilesSyncFolder.getStatus().isSuccess()) {
        return false;
    } else {
        MetadataBuffer bufferMetaData = folderFilesSyncFolder.getMetadataBuffer();
        for(int i = 0; i < bufferMetaData.getCount(); ++i) {
            final Metadata data = bufferMetaData.get(i);
            if(!data.isFolder() && !data.isTrashed() && data.isEditable() && data.getTitle().equalsIgnoreCase("testfile.txt")) {
                testFile = data.getDriveId();
                break;
            }
        }
        bufferMetaData.release();
    }

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

    // update testfile
    DriveFile file = Drive.DriveApi.getFile(mGoogleApiClient, testFile);
    DriveContentsResult driveContentsResult = file.open(mGoogleApiClient, DriveFile.MODE_WRITE_ONLY, null).await();
    if (!driveContentsResult.getStatus().isSuccess()) {
        return false;
    }
    DriveContents originalContents = driveContentsResult.getDriveContents();
    OutputStream os = originalContents.getOutputStream();
    try {
        os.write(String.valueOf(System.currentTimeMillis()).getBytes());
        // commit changes
        com.google.android.gms.common.api.Status status = originalContents.commit(mGoogleApiClient, null).await();
        if(!status.isSuccess()) {
            return false;

        }
        Log.d(TAG, "testfile.txt updated, driveId=" + file.getDriveId().encodeToString() + " resourceId=" + file.getDriveId().getResourceId());
        return true;
    } catch (IOException ioe) {
        return false;
    }
}

private boolean testFileAddAndRemoveSubscription(boolean subscribe) {
    final DriveFolder folderRoot = Drive.DriveApi.getRootFolder(mGoogleApiClient);

    // find testfile
    DriveId testFile = null;
    MetadataBufferResult folderFilesSyncFolder = folderRoot.listChildren(mGoogleApiClient).await();
    if (!folderFilesSyncFolder.getStatus().isSuccess()) {
        return false;
    } else {
        MetadataBuffer bufferMetaData = folderFilesSyncFolder.getMetadataBuffer();
        for(int i = 0; i < bufferMetaData.getCount(); ++i) {
            final Metadata data = bufferMetaData.get(i);
            if(!data.isFolder() && !data.isTrashed() && data.isEditable() && data.getTitle().equalsIgnoreCase("testfile.txt")) {
                testFile = data.getDriveId();
                break;
            }
        }
        bufferMetaData.release();
    }

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

    // subscribe & unsubscribe
    DriveFile file = Drive.DriveApi.getFile(mGoogleApiClient, testFile);
    if(subscribe) {
        com.google.android.gms.common.api.Status status = file.addChangeSubscription(mGoogleApiClient).await();
        if(!status.isSuccess()) {
            return false;
        }
        Log.d(TAG, "added subscription to testfile.txt, driveId=" + file.getDriveId().encodeToString() + " resourceId=" + file.getDriveId().getResourceId());
        return true;
    } else {
        com.google.android.gms.common.api.Status status = file.removeChangeSubscription(mGoogleApiClient).await();
        if(!status.isSuccess()) {
            return false;
        }
        Log.d(TAG, "removed subscription from testfile.txt, driveId=" + file.getDriveId().encodeToString() + " resourceId=" + file.getDriveId().getResourceId());
        return true;
    }
}

And here the service class:

public class ChangeService extends DriveEventService {

    // TAG
    private static final String TAG = ChangeService.class.getSimpleName();

    @Override
    public void onChange(ChangeEvent event) {
        final DriveId driveId = event.getDriveId();
        Log.e(TAG, "onChange; driveId=" + driveId.encodeToString() + " resourceId=" + driveId.getResourceId());
        if(event.hasContentChanged())       { Log.e(TAG, "contentChanged"); }
        else if(event.hasMetadataChanged()) { Log.e(TAG, "metadataChanged"); }
        else if(event.hasBeenDeleted())     { Log.e(TAG, "beenDeleted"); }
    }

    @Override
    public void onCompletion(CompletionEvent event) {
        final DriveId driveId = event.getDriveId();
        Log.e(TAG, "onCompletion; driveId=" + driveId.encodeToString() + " resourceId=" + driveId.getResourceId());
        switch (event.getStatus()) {
          case CompletionEvent.STATUS_CONFLICT:  Log.e(TAG, "STATUS_CONFLICT");  break;
          case CompletionEvent.STATUS_FAILURE:   Log.e(TAG, "STATUS_FAILURE");   break;
          case CompletionEvent.STATUS_SUCCESS:   Log.e(TAG, "STATUS_SUCCESS ");  break;
          case CompletionEvent.STATUS_CANCELED:  Log.e(TAG, "STATUS_CANCELED "); break;
        }
        event.dismiss();
    }

}

Upvotes: 4

Views: 554

Answers (1)

seanpj
seanpj

Reputation: 6755

I believe, you are falling into the same trap as many of us did before. I too originally assumed that the 'DriveEventService' takes care of notifications between multiple devices running under the same account. I tried and failed miserably, see here (and notice the resounding silence - since April 2014). I was always getting events on a single device only. So, I actually realized that Change Events work only locally within the GooPlaySvcs instance.

This was more or less confirmed by a comment from Steve Bazyl in this unrelated answer (please read including the 'ORIGINAL POST' paragraph), confirming my theory that both 'Change Events' and 'Completion Events' are local (Completion Events report result of network action - like http response).

So to answer your question. after fighting this for awhile, I had to develop a different strategy:

1/ perform GDAA action (create, update)
2/ wait for a Completion Event indicating your mod has been promoted to the Drive
3/ broadcast GCM message that include ResourceId (not DriveId !) plus optional data (up to 4K) to the registered participants.
4/ 'Registered participants' react to the message and download updated metadata/content, resolving the conflicts.

This solution is from summer 2014 and there may be some other pre-packaged solutions from Google since. I'd be happy myself to hear from people who know if there is more elegant solution.

Quite frankly, I don't understand what is this and this for, if the Completion Events do not timely reflect (notify of) the update from another device.

Good Luck

Upvotes: 1

Related Questions