[android] Googleドライブからキャッシュされたアプリデータを常に読み込まないようにする方法



Answers

Question

現在、 AndroidドライブのデータをGoogleドライブのApp Folderに保存するためにGoogle Drive Android APIを使用しています

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

  1. 現在のローカルzipファイルのチェックサムを生成します。
  2. GoogleドライブのApp Folderで検索して、既存のApp Folder zipファイルがあるかどうかを確認します。
  3. 存在する場合は、既存のApp Folder zipファイルの内容を現在のローカルzipファイルで上書きします。 また、最新のチェックサムで、既存のApp Folder zipファイル名の名前を変更します。
  4. 既存のApp Folder zipファイルがない場合は、ローカルのzipファイルの内容を含む新しいApp Folder 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();
        }
    }
}

既存の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://.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://.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ドライブのApp Folderにアップロードします 。 チェックサムは12345です。 使用されているファイル名は...checksum=12345...zip
  2. GoogleドライブのApp Folderからzipデータを検索します。 ファイル名が...checksum=12345...zipのファイルを見つけることができます。 コンテンツをダウンロードします。 コンテンツのチェックサムも12345ことを確認します。
  3. 新しいZipデータを既存のGoogleドライブのApp Folderファイルに上書きします。 新しいZIPデータチェックサムは67890です。 既存のappフォルダのzipファイルの名前が...checksum=67890...zip
  4. GoogleドライブのApp Folderからzipデータを検索します。 filename ...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();
    }
}

まず、私は思った

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

仕事をうまくやっていません。 ほとんどの場合、 Sync request rate limit exceeded.エラーメッセージが表示されて失敗します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ドライブレストAPIを使用している場合、次のデスクトップコードを使用すると、正しいチェックサムコンテンツを持つ最新のファイル名を見つけることができます。
  3. Androidアプリをアンインストールしてもう一度再インストールすると、 loadFromGoogleDriveは正しいチェックサムの内容で最新のファイル名を取得できます。

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

私はデモを制作しています。 この問題を再現する手順は次のとおりです。

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

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

ステップ2:APIコンソールでの設定

ステップ3:「123.TXT」を保存し、コンテンツ「123」を保存する。

ファイル名が "123.TXT"、内容が "123"のファイルは、アプリケーションフォルダに作成されます。

ステップ4:「456.TXT」と「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サービス9.4.52(440-127739847)でテストしました。 問題は表示されません。

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






Links