草庐IT

android - 如何避免总是从 Google Drive 加载缓存的应用数据

coder 2023-05-08 原文

目前,我正在使用 Google Drive Android API 将我的 Android 应用程序数据存储到 Google Drive 应用程序文件夹。

这就是我在保存应用程序数据时所做的

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

  • 这是执行上述操作的代码。

    生成新的应用程序文件夹 zip 文件,或更新现有的应用程序文件夹 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();
            }
        }
    }
    

    搜索现有的应用程序文件夹 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;
    }
    

    问题发生在加载应用程序数据期间。想象以下操作
  • 首次将 zip 数据上传到 Google Drive 应用程序文件夹。校验和是 12345 。正在使用的文件名是 ...checksum=12345...zip
  • 从 Google Drive 应用文件夹中搜索 zip 数据。能够找到文件名为 ...checksum=12345...zip 的文件。下载内容。验证内容的校验和也是 12345
  • 将新的 zip 数据覆盖到现有的 Google Drive 应用文件夹文件。新的 zip 数据校验和是 67890 。现有的应用程序文件夹 zip 文件重命名为 ...checksum=67890...zip
  • 从 Google Drive 应用文件夹中搜索 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 仍然只能获取最新的文件名,但校验和内容已过期。

    我 100% 确定 loadFromGoogleDrive 正在向我返回缓存的数据内容,并具有以下观察结果。
  • 我在 DownloadProgressListener 中安装了一个 driveFile.open,bytesDownloaded 为 0,bytesExpected 为 -1。
  • 如果我使用 Google Drive Rest API ,使用以下 desktop code ,我可以找到具有正确校验和内容的最新文件名。
  • 如果我卸载我的 Android 应用程序并重新安装,loadFromGoogleDrive 将能够获得具有正确校验和内容的最新文件名。

  • 有没有什么可靠的方法可以避免总是从 Google Drive 加载缓存的应用程序数据?

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

    第一步:下载源代码

    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”。

    请注意,如果我们
  • 卸载演示应用程序。
  • 重新安装演示应用程序。
  • 按下按钮 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

    最佳答案

  • 在 Google Drive 上搜索速度很慢。
    为什么不使用基本文件夹的属性来存储 zip 文件的 ID?
    https://developers.google.com/drive/v2/web/properties
  • Google Drive 上的文件名不是唯一的,您可以上传多个同名的文件。但是,Google 返回的文件 ID 是唯一的。
  • 关于android - 如何避免总是从 Google Drive 加载缓存的应用数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38934769/

    有关android - 如何避免总是从 Google Drive 加载缓存的应用数据的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

      给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

    5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

      我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

    6. ruby - 将差异补丁应用于字符串/文件 - 2

      对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

    7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

      我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

    8. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

      我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

    9. ruby - 如何指定 Rack 处理程序 - 2

      Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

    10. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

      在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

    随机推荐