/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.mobiledatadownload.internal; import static com.google.android.libraries.mobiledatadownload.internal.MddConstants.SPLIT_CHAR; import static com.google.android.libraries.mobiledatadownload.internal.util.SharedFilesMetadataUtil.MDD_SHARED_FILES; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.VisibleForTesting; import com.google.android.libraries.mobiledatadownload.Flags; import com.google.android.libraries.mobiledatadownload.SilentFeedback; import com.google.android.libraries.mobiledatadownload.annotations.InstanceId; import com.google.android.libraries.mobiledatadownload.internal.Migrations.FileKeyVersion; import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; import com.google.android.libraries.mobiledatadownload.internal.util.SharedFilesMetadataUtil; import com.google.android.libraries.mobiledatadownload.internal.util.SharedFilesMetadataUtil.FileKeyDeserializationException; import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil; import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CheckReturnValue; import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey; import com.google.mobiledatadownload.internal.MetadataProto.SharedFile; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; /** * Stores and provides access to shared file metadata using SharedPreferences. * *

Synchronization on this class depends on the fact that MDD Control Flow are executed on a * SequentialExecutor. */ @CheckReturnValue public final class SharedPreferencesSharedFilesMetadata implements SharedFilesMetadata { private static final String TAG = "SharedFilesMetadata"; @VisibleForTesting static final String PREFS_KEY_NEXT_FILE_NAME_OLD = "next_file_name"; @VisibleForTesting static final String PREFS_KEY_NEXT_FILE_NAME = "next_file_name_v2"; private final Context context; private final SilentFeedback silentFeedback; private final Optional instanceId; private final Flags flags; @Inject public SharedPreferencesSharedFilesMetadata( @ApplicationContext Context context, SilentFeedback silentFeedback, @InstanceId Optional instanceId, Flags flags) { this.context = context; this.silentFeedback = silentFeedback; this.instanceId = instanceId; this.flags = flags; } @Override public ListenableFuture init() { // Migrate to the new file key. if (!Migrations.isMigratedToNewFileKey(context)) { LogUtil.d("%s Device isn't migrated to new file key, clear and set migration.", TAG); Migrations.setMigratedToNewFileKey(context, true); Migrations.setCurrentVersion(context, FileKeyVersion.getVersion(flags.fileKeyVersion())); return Futures.immediateFuture(false); } return Futures.immediateFuture(upgradeToNewVersion()); } /** * Sequentially upgrade FileKey version to FeatureFlags.fileKeyVersion * * @return false if any upgrade fails which will result in clearing of all meta data, true on * successful upgrade. */ private boolean upgradeToNewVersion() { final FileKeyVersion targetVersion = FileKeyVersion.getVersion(flags.fileKeyVersion()); final FileKeyVersion currentVersion = Migrations.getCurrentVersion(context, silentFeedback); if (targetVersion.value == currentVersion.value) { return true; } if (targetVersion.value < currentVersion.value) { // We don't support downgrading file key version. Clear everything. LogUtil.e( "%s Cannot migrate back from value %s to %s. Clear everything!", TAG, currentVersion, targetVersion); silentFeedback.send( new Exception( "Downgraded file key from " + currentVersion + " to " + targetVersion + "."), "FileKey migrations unexpected downgrade."); Migrations.setCurrentVersion(context, targetVersion); return false; } // Migrate one version at a time one by one try { for (int nextVersion = currentVersion.value + 1; nextVersion <= targetVersion.value; nextVersion++) { if (upgradeTo(FileKeyVersion.getVersion(nextVersion))) { Migrations.setCurrentVersion(context, FileKeyVersion.getVersion(nextVersion)); } else { // If migration to next version fail, we will clear all data and set the // currentVersion // to targetVersion (phFileKeyVersion) return false; } } } finally { if (Migrations.getCurrentVersion(context, silentFeedback).value != targetVersion.value) { if (!Migrations.setCurrentVersion(context, targetVersion)) { LogUtil.e( "Failed to commit migration version to disk. Fail to set target " + "version to " + targetVersion + "."); silentFeedback.send( new Exception("Fail to set target version " + targetVersion + "."), "Failed to commit migration version to disk."); } } } return true; } private boolean upgradeTo(FileKeyVersion targetVersion) { switch (targetVersion) { case ADD_DOWNLOAD_TRANSFORM: return migrateToAddDownloadTransform(); case USE_CHECKSUM_ONLY: return migrateToDedupOnChecksumOnly(); default: throw new UnsupportedOperationException( "Upgrade to version " + targetVersion.name() + "not supported!"); } } /** A one off method that is called when we migrate key to add download transform. */ private boolean migrateToAddDownloadTransform() { LogUtil.d("%s: Starting migration to add download transform", TAG); SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); SharedPreferences.Editor editor = prefs.edit(); for (String serializedFileKey : prefs.getAll().keySet()) { // Remove the data that we are unable to read or parse. NewFileKey newFileKey; try { newFileKey = SharedFilesMetadataUtil.deserializeNewFileKey( serializedFileKey, context, silentFeedback); } catch (FileKeyDeserializationException e) { LogUtil.e( "%s Failed to deserialize file key %s, remove and continue.", TAG, serializedFileKey); silentFeedback.send(e, "Failed to deserialize file key, remove and continue."); editor.remove(serializedFileKey); continue; } SharedFile sharedFile = SharedPreferencesUtil.readProto(prefs, serializedFileKey, SharedFile.parser()); if (sharedFile == null) { LogUtil.e("%s: Unable to read sharedFile from shared preferences.", TAG); editor.remove(serializedFileKey); continue; } // Remove the old key and write the new one. SharedPreferencesUtil.removeProto(editor, serializedFileKey); SharedPreferencesUtil.writeProto( editor, SharedFilesMetadataUtil.serializeNewFileKeyWithDownloadTransform(newFileKey), sharedFile); } if (!editor.commit()) { LogUtil.e("Failed to commit migration metadata to disk"); silentFeedback.send( new Exception("Migrate to DownloadTransform failed."), "Failed to commit migration metadata to disk."); return false; } return true; } /** A one off method that is called when we migrate key to contain checksum and * allowedReaders. */ private boolean migrateToDedupOnChecksumOnly() { LogUtil.d("%s: Starting migration to dedup on checksum only", TAG); SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); SharedPreferences.Editor editor = prefs.edit(); for (String serializedFileKey : prefs.getAll().keySet()) { // Remove the data that we are unable to read or parse. NewFileKey newFileKey; try { newFileKey = SharedFilesMetadataUtil.deserializeNewFileKey( serializedFileKey, context, silentFeedback); } catch (FileKeyDeserializationException e) { LogUtil.e( "%s Failed to deserialize file key %s, remove and continue.", TAG, serializedFileKey); silentFeedback.send(e, "Failed to deserialize file key, remove and continue."); editor.remove(serializedFileKey); continue; } SharedFile sharedFile = SharedPreferencesUtil.readProto(prefs, serializedFileKey, SharedFile.parser()); if (sharedFile == null) { LogUtil.e("%s: Unable to read sharedFile from shared preferences.", TAG); editor.remove(serializedFileKey); continue; } // Remove the old key and write the new one. SharedPreferencesUtil.removeProto(editor, serializedFileKey); SharedPreferencesUtil.writeProto( editor, SharedFilesMetadataUtil.serializeNewFileKeyWithChecksumOnly(newFileKey), sharedFile); } if (!editor.commit()) { LogUtil.e("Failed to commit migration metadata to disk"); silentFeedback.send( new Exception("Migrate to ChecksumOnly failed."), "Failed to commit migration metadata to disk."); return false; } return true; } @SuppressWarnings("nullness") @Override public ListenableFuture read(NewFileKey newFileKey) { return PropagatedFutures.transform( readAll(ImmutableSet.of(newFileKey)), sharedFiles -> sharedFiles.get(newFileKey), directExecutor()); } @Override public ListenableFuture> readAll( ImmutableSet newFileKeys) { SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); ImmutableMap.Builder sharedFileMapBuilder = ImmutableMap.builder(); for (NewFileKey newFileKey : newFileKeys) { String serializedFileKey = SharedFilesMetadataUtil.getSerializedFileKey(newFileKey, context, silentFeedback); SharedFile sharedFile = SharedPreferencesUtil.readProto(prefs, serializedFileKey, SharedFile.parser()); if (sharedFile != null) { sharedFileMapBuilder.put(newFileKey, sharedFile); } } return Futures.immediateFuture(sharedFileMapBuilder.build()); } @Override public ListenableFuture write(NewFileKey newFileKey, SharedFile sharedFile) { String serializedFileKey = SharedFilesMetadataUtil.getSerializedFileKey(newFileKey, context, silentFeedback); SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); return Futures.immediateFuture( SharedPreferencesUtil.writeProto(prefs, serializedFileKey, sharedFile)); } @Override public ListenableFuture remove(NewFileKey newFileKey) { String serializedFileKey = SharedFilesMetadataUtil.getSerializedFileKey(newFileKey, context, silentFeedback); SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); return Futures.immediateFuture(SharedPreferencesUtil.removeProto(prefs, serializedFileKey)); } @Override public ListenableFuture> getAllFileKeys() { List newFileKeyList = new ArrayList<>(); SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); SharedPreferences.Editor editor = null; for (String serializedFileKey : prefs.getAll().keySet()) { try { NewFileKey newFileKey = SharedFilesMetadataUtil.deserializeNewFileKey( serializedFileKey, context, silentFeedback); newFileKeyList.add(newFileKey); } catch (FileKeyDeserializationException e) { LogUtil.e(e, "Failed to deserialize newFileKey:" + serializedFileKey); silentFeedback.send( e, "Failed to deserialize newFileKey, unexpected key size: %d", Splitter.on(SPLIT_CHAR).splitToList(serializedFileKey).size()); // TODO(b/128850000): Refactor this code to a single corruption handling task during // maintenance. // Remove the corrupted file metadata and the related FileGroup metadata will be deleted // in next maintenance task. if (editor == null) { editor = prefs.edit(); } editor.remove(serializedFileKey); continue; } } if (editor != null) { editor.commit(); } return Futures.immediateFuture(newFileKeyList); } @Override public ListenableFuture clear() { SharedPreferences prefs = SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); prefs.edit().clear().commit(); return Futures.immediateFuture(null); } }