Actuellement, j'utilise API Android de Google Drive pour stocker les données de mes applications Android, pour Dossier d'applications Google Drive .
Voici ce que je fais lorsque je sauvegarde les données de mon application
- Génère une somme de contrôle pour le fichier zip local actuel.
- Rechercher dans Dossier d'applications Google Drive pour voir s'il y a un fichier zip App Folder existant.
- Si c'est le cas, écrasez le contenu du fichier zip du Dossier des Applications existant, avec les fichiers zip locaux actuels. De même, nous renommerons le nom du fichier zip du Dossier des Applis existant, avec la dernière somme de contrôle.
- S'il n'y a pas de fichier zip App Folder existant, générez un nouveau fichier zip App Folder, avec le contenu du fichier zip local. Nous utiliserons la dernière somme de contrôle comme nom de fichier zip de l'App Folder.
Voici le code qui effectue les opérations susmentionnées.
Générer un nouveau fichier zip du dossier d'application, ou mettre à jour le fichier zip du dossier d'application existant.
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();
}
}
}
Recherchez le fichier zip du dossier d'applications existant
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;
}
Le problème survient lors du chargement des données de l'application. Imaginez les opérations suivantes
- Télécharger les données zip vers Dossier d'applications Google Drive pour la première fois. La somme de contrôle est
12345
. Le nom de fichier utilisé est...checksum=12345...zip
- Recherche de données zip à partir de Dossier d'applications Google Drive . Capable de trouver le fichier avec le nom de fichier
...checksum=12345...zip
. Téléchargez le contenu. Vérifiez que la somme de contrôle du contenu est12345
aussi. - Ecraser les nouvelles données zip sur les données existantes Dossier d'applications Google Drive fichier. La somme de contrôle des nouvelles données zip est
67890
. Le fichier zip du dossier d'applications existant est renommé en...checksum=67890...zip
- Recherche de données zip à partir de Dossier d'applications Google Drive . Capable de trouver le fichier avec le nom de fichier
...checksum=67890...zip
. Cependant, après avoir téléchargé le contenu, la somme de contrôle du contenu est toujours ancienne.12345
!
Télécharger le fichier zip du dossier de l'application
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();
}
}
D'abord, j'ai pensé
Status status = Drive.DriveApi.requestSync(googleApiClient).await()
ne fait pas bien le travail. Il échoue dans la plupart des situations, avec un message d'erreur Sync request rate limit exceeded.
En fait, la limite dure imposée dans requestSync
Cette API n'est donc pas particulièrement utile. Android Google Play / Drive Api
Cependant, même lorsque requestSync
succès, loadFromGoogleDrive
ne peut toujours obtenir que le dernier nom de fichier, mais le contenu de la somme de contrôle est périmé.
Je suis sûr à 100% loadFromGoogleDrive
me renvoie un contenu de données en cache, avec les observations suivantes.
- J'installe un
DownloadProgressListener
surdriveFile.open
, bytesDownloaded est 0 et bytesExpected est -1. - Si j'utilise Google Drive Rest API avec les éléments suivants code de bureau Je peux trouver le dernier nom de fichier avec le contenu correct de la somme de contrôle.
- Si je désinstalle mon application Android et la réinstalle à nouveau,
loadFromGoogleDrive
sera capable d'obtenir le dernier nom de fichier avec le contenu correct de la somme de contrôle.
Existe-t-il un moyen robuste d'éviter de toujours charger les données de l'application en cache depuis Google Drive ?
J'ai réussi à produire une démo. Voici les étapes pour reproduire ce problème.
Étape 1 : Télécharger le code source
https://github.com/yccheok/google-drive-bug
Étape 2 : Configuration dans la console API
Étape 3 : Appuyez sur le bouton SAUVEGARDER "123.TXT" AVEC LE CONTENU "123".
Un fichier portant le nom "123.TXT" et le contenu "123" sera créé dans le dossier de l'application.
Étape 4 : Appuyez sur le bouton SAUVEGARDER "456.TXT" AVEC LE CONTENU "456".
Le fichier précédent sera renommé en "456.TXT", avec un contenu mis à jour en "456".
Étape 5 : Appuyez sur le bouton CHARGER LE DERNIER FICHIER SAUVEGARDÉ
Le fichier dont le nom est "456.TXT" a été trouvé, mais le contenu précédent en cache "123" est lu. J'attendais le contenu "456".
Notez que, si nous
- Désinstallez l'application de démonstration.
- Réinstallez l'application de démonstration.
- Appuyez sur le bouton LOAD LAST SAVED FILE, un fichier portant le nom "456.TXT" et le contenu "456" est trouvé.
J'avais soumis un rapport officiel sur les problèmes - https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4727
Autres informations
Voici comment cela se présente sous mon appareil - http://youtu.be/kuIHoi4A1c0
Je réalise que tous les utilisateurs ne rencontreront pas ce problème. Par exemple, j'avais testé avec un autre Nexus 6, Google Play Services 9.4.52 (440-127739847). Le problème n'apparaît pas.
J'avais compilé un APK pour le tester - https://github.com/yccheok/google-drive-bug/releases/download/1.0/demo.apk