В настоящее время я использую Google Android API Android для хранения данных своих Android-приложений в папку приложений Google Диска.
Это то, что я делаю при сохранении данных своих приложений
- Создайте контрольную сумму для текущего локального zip файла.
- Искать в папке приложения Google Диска, чтобы узнать, существует ли существующий zip файл приложения.
- Если есть, перезапишите содержимое существующего zip файла App Folder с текущими локальными zip файлами. Кроме того, мы переименуем существующее имя файла zip файла App Folder с последней контрольной суммой.
- Если нет существующего zip файла App Folder, сгенерируйте новый zip файл App Folder с локальным содержимым zip файла. Мы будем использовать последнюю контрольную сумму как имя файла ZIP-папки приложения Folder.
Здесь код, который выполняет вышеупомянутые операции.
Создать новый 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/info/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/info/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 Диска в первый раз. Контрольная сумма
12345
. Используемое имя файла...checksum=12345...zip
- Поиск zip-данных из папки приложений Google Диска. Возможность найти файл с именем файла
...checksum=12345...zip
. Загрузите контент. Проверьте контрольную сумму содержимого12345
. - Перезаписать новые данные zip в существующий файл папки приложения Google Диска. Новая контрольная сумма данных zip составляет
67890
. Существующий zip файл папки приложения переименован в...checksum=67890...zip
- Поиск zip-данных из папки приложений Google Диска. Возможность найти файл с именем файла
...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 Rest Rest API со следующим код рабочего стола, я могу найти последнее имя файла с правильным содержимым контрольной суммы.
- Если я удалю свое приложение для Android и снова заново установлю,
loadFromGoogleDrive
сможет получить последнее имя файла с правильным содержимым контрольной суммы.
Есть ли какой-либо надежный способ избежать загрузки данных кэшированных приложений с Google Диска?
Мне удалось создать демоверсию. Вот шаги для воспроизведения этой проблемы.
Шаг 1: Загрузите исходный код
https://github.com/yccheok/google-drive-bug
Шаг 2: Настройка в консоли API
Шаг 3: Нажмите кнопку SAVE "123.TXT" С СОДЕРЖАНИЕМ "123"
В папке приложения будет создан файл с именем файла "123.TXT", содержимое "123" .
Шаг 4: Нажмите кнопку SAVE "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