如何避免始终从Google云端硬盘加载缓存的应用数据

Che*_*eng 58 android google-drive-api google-drive-android-api

目前,我正在使用Google Drive Android API将我的Android应用数据存储到Google云端硬盘应用文件夹.

这是我在保存应用程序数据时正在做的事情

  1. 为当前本地zip文件生成校验和.
  2. Google云端硬盘应用文件夹中搜索,查看是否存在现有的应用文件夹zip文件.
  3. 如果有,用当前本地zip文件覆盖现有App Folder zip文件的内容.此外,我们将使用最新的校验和重命名现有的App Folder zip文件名.
  4. 如果没有现有的App Folder zip文件,请生成一个新的App Folder zip文件,其中包含本地zip文件的内容.我们将使用最新的校验和作为App Folder zip文件名.

这是执行上述操作的代码.

生成新的App Folder zip文件,或更新现有的App Folder zip文件

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

搜索现有的App Folder zip文件

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;
}
Run Code Online (Sandbox Code Playgroud)

在加载应用程序数据期间出现问题.想象一下以下操作

  1. 首次将zip数据上传到Google云端硬盘应用文件夹.校验和是12345.正在使用的文件名是...checksum=12345...zip
  2. Google云端硬盘应用文件夹中搜索zip数据.能够找到带文件名的文件...checksum=12345...zip.下载内容.验证内容的校验和也是12345如此.
  3. 将新的zip数据覆盖到现有的Google云端硬盘应用文件夹文件中.新的zip数据校验和是67890.现有的应用程序文件夹zip文件已重命名为...checksum=67890...zip
  4. Google云端硬盘应用文件夹中搜索zip数据.能够找到带文件名的文件...checksum=67890...zip.但是,下载内容后,内容的校验和仍然是旧的12345!

下载App Folder zip文件

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

首先,我想

Status status = Drive.DriveApi.requestSync(googleApiClient).await()
Run Code Online (Sandbox Code Playgroud)

不能很好地完成这项工作.它在大多数情况下失败,出现错误信息Sync request rate limit exceeded.事实上,强加的硬限制requestSync,使得API不是特别有用 - Android Google Play/Drive Api


但是,即使requestSync成功,loadFromGoogleDrive仍然只能获取最新的文件名,但过时的校验和内容.

我100%肯定loadFromGoogleDrive会给我一个缓存的数据内容,并提供以下观察结果.

  1. 我安装了一个DownloadProgressListenerin driveFile.open,bytesDownloaded是0,bytesExpected是-1.
  2. 如果我使用Google Drive Rest API,使用以下桌面代码,我可以找到具有正确校验和内容的最新文件名.
  3. 如果我卸载我的Android应用并再次重新安装,loadFromGoogleDrive将能够获得具有正确校验和内容的最新文件名.

有没有强大的方法,以避免始终从Google云端硬盘加载缓存的应用数据?


我设法制作一个演示.以下是重现此问题的步骤.

第1步:下载源代码

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

第2步:在API控制台中进行设置

在此输入图像描述

第3步:按下按钮SAVE"123.TXT"WITH CONTENT"123"

在此输入图像描述

文件名为"123.TXT"的文件,内容"123"将在app文件夹中创建.

第4步:按下按钮SAVE"456.TXT"WITH CONTENT"456"

在此输入图像描述

之前的文件将重命名为"456.TXT",内容更新为"456"

第5步:按下按钮LOAD LAST SAVED FILE

在此输入图像描述

找到文件名为"456.TXT"的文件,但读取先前缓存的内容"123".我期待内容"456".

请注意,如果我们

  1. 卸载演示应用.
  2. 重新安装演示应用程序.
  3. 按下LOAD LAST SAVED FILE按钮,文件名为"456.TXT",内容为"456".

我已正式提交问题报告 - https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4727


其他信息

这就是我的设备下的样子 - http://youtu.be/kuIHoi4A1c0

我意识到,并非所有用户都会遇到这个问题.例如,我曾使用另一款Nexus 6,Google Play Services 9.4.52(440-127739847)进行测试.问题没有出现.

我编译了一个APK用于测试目的 - https://github.com/yccheok/google-drive-bug/releases/download/1.0/demo.apk

ash*_*shb 1

  1. Google 云端硬盘上的搜索速度很慢。为什么不使用基本文件夹的属性来存储 zip 文件的 ID? https://developers.google.com/drive/v2/web/properties
  2. Google Drive 上的文件名并不唯一,您可以上传多个同名文件。然而,Google 返回的文件 ID 是唯一的。