web-dev-qa-db-ja.com

キャッシュされたアプリデータをGoogleドライブから常にロードしないようにする方法

現在、私は Google Drive Android API 、私のAndroidアプリデータをGoogleドライブアプリフォルダ

これは、アプリケーションデータを保存するときに行っていることです

  1. 現在のローカルZipファイルのチェックサムを生成します。
  2. Google Drive App Folderを検索して、既存のApp FolderZipファイルがあるかどうかを確認します。
  3. 存在する場合は、既存のApp FolderZipファイルの内容を現在のローカルZipファイルで上書きします。また、既存のApp FolderZipファイル名の名前を最新のチェックサムで変更します。
  4. 既存のAppFolder Zipファイルがない場合は、ローカルZipファイルのコンテンツを含む新しいApp FolderZipファイルを生成します。最新のチェックサムをAppFolderZipファイル名として使用します。

上記の操作を実行するコードは次のとおりです。

新しいAppFolder Zipファイルを生成するか、既存のApp FolderZipファイルを更新します

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();
        }
    }
}

既存のアプリフォルダの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;
}

アプリケーションデータのロード中に問題が発生します。次の操作を想像してみてください

  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

アプリフォルダの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();
    }
}

まず、思った

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

うまくいきません。ほとんどの状況で失敗し、エラーメッセージSync request rate limit exceeded.が表示されます。実際、requestSyncに課せられたハード制限により、そのAPIは特に有用ではなくなります- Android Google Play/Drive Api ==


ただし、requestSyncが成功した場合でも、loadFromGoogleDriveは最新のファイル名しか取得できませんが、チェックサムの内容は古くなっています。

loadFromGoogleDriveがキャッシュされたデータコンテンツを返していると100%確信しています。次の観察結果があります。

  1. driveFile.openDownloadProgressListenerをインストールします。bytesDownloadedは0、bytesExpectedは-1です。
  2. Google Drive Rest API を使用し、次の デスクトップコード を使用すると、正しいチェックサムコンテンツを含む最新のファイル名を見つけることができます。
  3. Androidアプリをアンインストールして再度インストールすると、loadFromGoogleDriveは正しいチェックサムコンテンツを含む最新のファイル名を取得できます。

キャッシュされたアプリデータを常にGoogleドライブからロードしないようにするための堅牢な方法はありますか?


なんとかデモを制作しました。この問題を再現する手順は次のとおりです。

ステップ1:ソースコードをダウンロードする

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

ステップ2:APIコンソールでのセットアップ

enter image description here

ステップ3:ボタンSAVE "123.TXT" WITH CONTENT "123"を押す

enter image description here

ファイル名が「123.TXT」、コンテンツが「123」のファイルがアプリフォルダーに作成されます。

ステップ4:ボタンSAVE "456.TXT" WITH CONTENT "456"を押す

enter image description here

以前のファイルの名前は「456.TXT」に変更され、コンテンツは「456」に更新されます。

ステップ5:ボタンを押す最後に保存したファイルをロード

enter image description here

ファイル名「456.TXT」のファイルが見つかりましたが、以前にキャッシュされたコンテンツ「123」が読み取られます。コンテンツ「456」を期待していました。

注意してください、

  1. デモアプリをアンインストールします。
  2. デモアプリを再インストールします。
  3. [最後に保存したファイルをロード]ボタンを押すと、ファイル名が「456.TXT」でコンテンツが「456」のファイルが見つかります。

問題レポートを正式に提出しました- https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4727


他の情報

これは私のデバイスの下でどのように見えるかです--- http://youtu.be/kuIHoi4A1c

すべてのユーザーがこの問題にぶつかるわけではないことを私は理解しています。たとえば、別のNexus 6、Google Play開発者サービス9.4.52(440-127739847)でテストしました。問題は発生しません。

テスト目的でAPKをコンパイルしました- https://github.com/yccheok/google-drive-bug/releases/download/1.0/demo.apk

58
Cheok Yan Cheng
  1. Googleドライブでの検索は遅いです。ベースフォルダのプロパティを使用してZipファイルのIDを保存してみませんか? https://developers.google.com/drive/v2/web/properties
  2. Googleドライブのファイル名は一意ではありません。同じ名前の複数のファイルをアップロードできます。ただし、Googleから返されるファイルIDは一意です。
1
ashishb