1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.internal; 17 18 import static com.google.android.libraries.mobiledatadownload.internal.MddConstants.SPLIT_CHAR; 19 import static com.google.android.libraries.mobiledatadownload.internal.util.SharedFilesMetadataUtil.MDD_SHARED_FILES; 20 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 21 22 import android.content.Context; 23 import android.content.SharedPreferences; 24 25 import androidx.annotation.VisibleForTesting; 26 27 import com.google.android.libraries.mobiledatadownload.Flags; 28 import com.google.android.libraries.mobiledatadownload.SilentFeedback; 29 import com.google.android.libraries.mobiledatadownload.annotations.InstanceId; 30 import com.google.android.libraries.mobiledatadownload.internal.Migrations.FileKeyVersion; 31 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 32 import com.google.android.libraries.mobiledatadownload.internal.util.SharedFilesMetadataUtil; 33 import com.google.android.libraries.mobiledatadownload.internal.util.SharedFilesMetadataUtil.FileKeyDeserializationException; 34 import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil; 35 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 36 import com.google.common.base.Optional; 37 import com.google.common.base.Splitter; 38 import com.google.common.collect.ImmutableMap; 39 import com.google.common.collect.ImmutableSet; 40 import com.google.common.util.concurrent.Futures; 41 import com.google.common.util.concurrent.ListenableFuture; 42 import com.google.errorprone.annotations.CheckReturnValue; 43 import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey; 44 import com.google.mobiledatadownload.internal.MetadataProto.SharedFile; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 49 import javax.inject.Inject; 50 51 /** 52 * Stores and provides access to shared file metadata using SharedPreferences. 53 * 54 * <p>Synchronization on this class depends on the fact that MDD Control Flow are executed on a 55 * SequentialExecutor. 56 */ 57 @CheckReturnValue 58 public final class SharedPreferencesSharedFilesMetadata implements SharedFilesMetadata { 59 60 private static final String TAG = "SharedFilesMetadata"; 61 62 @VisibleForTesting 63 static final String PREFS_KEY_NEXT_FILE_NAME_OLD = "next_file_name"; 64 @VisibleForTesting 65 static final String PREFS_KEY_NEXT_FILE_NAME = "next_file_name_v2"; 66 67 private final Context context; 68 private final SilentFeedback silentFeedback; 69 private final Optional<String> instanceId; 70 private final Flags flags; 71 72 @Inject SharedPreferencesSharedFilesMetadata( @pplicationContext Context context, SilentFeedback silentFeedback, @InstanceId Optional<String> instanceId, Flags flags)73 public SharedPreferencesSharedFilesMetadata( 74 @ApplicationContext Context context, 75 SilentFeedback silentFeedback, 76 @InstanceId Optional<String> instanceId, 77 Flags flags) { 78 this.context = context; 79 this.silentFeedback = silentFeedback; 80 this.instanceId = instanceId; 81 this.flags = flags; 82 } 83 84 @Override init()85 public ListenableFuture<Boolean> init() { 86 // Migrate to the new file key. 87 if (!Migrations.isMigratedToNewFileKey(context)) { 88 LogUtil.d("%s Device isn't migrated to new file key, clear and set migration.", TAG); 89 Migrations.setMigratedToNewFileKey(context, true); 90 Migrations.setCurrentVersion(context, 91 FileKeyVersion.getVersion(flags.fileKeyVersion())); 92 return Futures.immediateFuture(false); 93 } 94 return Futures.immediateFuture(upgradeToNewVersion()); 95 } 96 97 /** 98 * Sequentially upgrade FileKey version to FeatureFlags.fileKeyVersion 99 * 100 * @return false if any upgrade fails which will result in clearing of all meta data, true on 101 * successful upgrade. 102 */ upgradeToNewVersion()103 private boolean upgradeToNewVersion() { 104 final FileKeyVersion targetVersion = FileKeyVersion.getVersion(flags.fileKeyVersion()); 105 final FileKeyVersion currentVersion = Migrations.getCurrentVersion(context, silentFeedback); 106 107 if (targetVersion.value == currentVersion.value) { 108 return true; 109 } 110 111 if (targetVersion.value < currentVersion.value) { 112 // We don't support downgrading file key version. Clear everything. 113 LogUtil.e( 114 "%s Cannot migrate back from value %s to %s. Clear everything!", 115 TAG, currentVersion, targetVersion); 116 silentFeedback.send( 117 new Exception( 118 "Downgraded file key from " + currentVersion + " to " + targetVersion 119 + "."), 120 "FileKey migrations unexpected downgrade."); 121 Migrations.setCurrentVersion(context, targetVersion); 122 return false; 123 } 124 125 // Migrate one version at a time one by one 126 try { 127 for (int nextVersion = currentVersion.value + 1; 128 nextVersion <= targetVersion.value; 129 nextVersion++) { 130 if (upgradeTo(FileKeyVersion.getVersion(nextVersion))) { 131 Migrations.setCurrentVersion(context, FileKeyVersion.getVersion(nextVersion)); 132 } else { 133 // If migration to next version fail, we will clear all data and set the 134 // currentVersion 135 // to targetVersion (phFileKeyVersion) 136 return false; 137 } 138 } 139 } finally { 140 if (Migrations.getCurrentVersion(context, silentFeedback).value 141 != targetVersion.value) { 142 if (!Migrations.setCurrentVersion(context, targetVersion)) { 143 LogUtil.e( 144 "Failed to commit migration version to disk. Fail to set target " 145 + "version to " 146 + targetVersion 147 + "."); 148 silentFeedback.send( 149 new Exception("Fail to set target version " + targetVersion + "."), 150 "Failed to commit migration version to disk."); 151 } 152 } 153 } 154 155 return true; 156 } 157 upgradeTo(FileKeyVersion targetVersion)158 private boolean upgradeTo(FileKeyVersion targetVersion) { 159 switch (targetVersion) { 160 case ADD_DOWNLOAD_TRANSFORM: 161 return migrateToAddDownloadTransform(); 162 case USE_CHECKSUM_ONLY: 163 return migrateToDedupOnChecksumOnly(); 164 default: 165 throw new UnsupportedOperationException( 166 "Upgrade to version " + targetVersion.name() + "not supported!"); 167 } 168 } 169 170 /** A one off method that is called when we migrate key to add download transform. */ migrateToAddDownloadTransform()171 private boolean migrateToAddDownloadTransform() { 172 LogUtil.d("%s: Starting migration to add download transform", TAG); 173 SharedPreferences prefs = 174 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 175 SharedPreferences.Editor editor = prefs.edit(); 176 for (String serializedFileKey : prefs.getAll().keySet()) { 177 178 // Remove the data that we are unable to read or parse. 179 NewFileKey newFileKey; 180 try { 181 newFileKey = 182 SharedFilesMetadataUtil.deserializeNewFileKey( 183 serializedFileKey, context, silentFeedback); 184 } catch (FileKeyDeserializationException e) { 185 LogUtil.e( 186 "%s Failed to deserialize file key %s, remove and continue.", TAG, 187 serializedFileKey); 188 silentFeedback.send(e, "Failed to deserialize file key, remove and continue."); 189 editor.remove(serializedFileKey); 190 continue; 191 } 192 SharedFile sharedFile = 193 SharedPreferencesUtil.readProto(prefs, serializedFileKey, SharedFile.parser()); 194 if (sharedFile == null) { 195 LogUtil.e("%s: Unable to read sharedFile from shared preferences.", TAG); 196 editor.remove(serializedFileKey); 197 continue; 198 } 199 200 // Remove the old key and write the new one. 201 SharedPreferencesUtil.removeProto(editor, serializedFileKey); 202 SharedPreferencesUtil.writeProto( 203 editor, 204 SharedFilesMetadataUtil.serializeNewFileKeyWithDownloadTransform(newFileKey), 205 sharedFile); 206 } 207 208 if (!editor.commit()) { 209 LogUtil.e("Failed to commit migration metadata to disk"); 210 silentFeedback.send( 211 new Exception("Migrate to DownloadTransform failed."), 212 "Failed to commit migration metadata to disk."); 213 return false; 214 } 215 216 return true; 217 } 218 219 /** A one off method that is called when we migrate key to contain checksum and 220 * allowedReaders. */ migrateToDedupOnChecksumOnly()221 private boolean migrateToDedupOnChecksumOnly() { 222 LogUtil.d("%s: Starting migration to dedup on checksum only", TAG); 223 SharedPreferences prefs = 224 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 225 SharedPreferences.Editor editor = prefs.edit(); 226 for (String serializedFileKey : prefs.getAll().keySet()) { 227 228 // Remove the data that we are unable to read or parse. 229 NewFileKey newFileKey; 230 try { 231 newFileKey = 232 SharedFilesMetadataUtil.deserializeNewFileKey( 233 serializedFileKey, context, silentFeedback); 234 } catch (FileKeyDeserializationException e) { 235 LogUtil.e( 236 "%s Failed to deserialize file key %s, remove and continue.", TAG, 237 serializedFileKey); 238 silentFeedback.send(e, "Failed to deserialize file key, remove and continue."); 239 editor.remove(serializedFileKey); 240 continue; 241 } 242 243 SharedFile sharedFile = 244 SharedPreferencesUtil.readProto(prefs, serializedFileKey, SharedFile.parser()); 245 if (sharedFile == null) { 246 LogUtil.e("%s: Unable to read sharedFile from shared preferences.", TAG); 247 editor.remove(serializedFileKey); 248 continue; 249 } 250 251 // Remove the old key and write the new one. 252 SharedPreferencesUtil.removeProto(editor, serializedFileKey); 253 SharedPreferencesUtil.writeProto( 254 editor, 255 SharedFilesMetadataUtil.serializeNewFileKeyWithChecksumOnly(newFileKey), 256 sharedFile); 257 } 258 259 if (!editor.commit()) { 260 LogUtil.e("Failed to commit migration metadata to disk"); 261 silentFeedback.send( 262 new Exception("Migrate to ChecksumOnly failed."), 263 "Failed to commit migration metadata to disk."); 264 return false; 265 } 266 267 return true; 268 } 269 270 @SuppressWarnings("nullness") 271 @Override read(NewFileKey newFileKey)272 public ListenableFuture<SharedFile> read(NewFileKey newFileKey) { 273 return PropagatedFutures.transform( 274 readAll(ImmutableSet.of(newFileKey)), 275 sharedFiles -> sharedFiles.get(newFileKey), 276 directExecutor()); 277 } 278 279 @Override readAll( ImmutableSet<NewFileKey> newFileKeys)280 public ListenableFuture<ImmutableMap<NewFileKey, SharedFile>> readAll( 281 ImmutableSet<NewFileKey> newFileKeys) { 282 SharedPreferences prefs = 283 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 284 ImmutableMap.Builder<NewFileKey, SharedFile> sharedFileMapBuilder = ImmutableMap.builder(); 285 for (NewFileKey newFileKey : newFileKeys) { 286 String serializedFileKey = 287 SharedFilesMetadataUtil.getSerializedFileKey(newFileKey, context, 288 silentFeedback); 289 SharedFile sharedFile = 290 SharedPreferencesUtil.readProto(prefs, serializedFileKey, SharedFile.parser()); 291 if (sharedFile != null) { 292 sharedFileMapBuilder.put(newFileKey, sharedFile); 293 } 294 } 295 return Futures.immediateFuture(sharedFileMapBuilder.build()); 296 } 297 298 @Override write(NewFileKey newFileKey, SharedFile sharedFile)299 public ListenableFuture<Boolean> write(NewFileKey newFileKey, SharedFile sharedFile) { 300 String serializedFileKey = 301 SharedFilesMetadataUtil.getSerializedFileKey(newFileKey, context, silentFeedback); 302 303 SharedPreferences prefs = 304 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 305 return Futures.immediateFuture( 306 SharedPreferencesUtil.writeProto(prefs, serializedFileKey, sharedFile)); 307 } 308 309 @Override remove(NewFileKey newFileKey)310 public ListenableFuture<Boolean> remove(NewFileKey newFileKey) { 311 String serializedFileKey = 312 SharedFilesMetadataUtil.getSerializedFileKey(newFileKey, context, silentFeedback); 313 314 SharedPreferences prefs = 315 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 316 return Futures.immediateFuture(SharedPreferencesUtil.removeProto(prefs, serializedFileKey)); 317 } 318 319 @Override getAllFileKeys()320 public ListenableFuture<List<NewFileKey>> getAllFileKeys() { 321 List<NewFileKey> newFileKeyList = new ArrayList<>(); 322 SharedPreferences prefs = 323 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 324 SharedPreferences.Editor editor = null; 325 for (String serializedFileKey : prefs.getAll().keySet()) { 326 try { 327 NewFileKey newFileKey = 328 SharedFilesMetadataUtil.deserializeNewFileKey( 329 serializedFileKey, context, silentFeedback); 330 newFileKeyList.add(newFileKey); 331 } catch (FileKeyDeserializationException e) { 332 LogUtil.e(e, "Failed to deserialize newFileKey:" + serializedFileKey); 333 silentFeedback.send( 334 e, 335 "Failed to deserialize newFileKey, unexpected key size: %d", 336 Splitter.on(SPLIT_CHAR).splitToList(serializedFileKey).size()); 337 // TODO(b/128850000): Refactor this code to a single corruption handling task during 338 // maintenance. 339 // Remove the corrupted file metadata and the related FileGroup metadata will be deleted 340 // in next maintenance task. 341 if (editor == null) { 342 editor = prefs.edit(); 343 } 344 editor.remove(serializedFileKey); 345 continue; 346 } 347 } 348 if (editor != null) { 349 editor.commit(); 350 } 351 return Futures.immediateFuture(newFileKeyList); 352 } 353 354 @Override clear()355 public ListenableFuture<Void> clear() { 356 SharedPreferences prefs = 357 SharedPreferencesUtil.getSharedPreferences(context, MDD_SHARED_FILES, instanceId); 358 prefs.edit().clear().commit(); 359 return Futures.immediateFuture(null); 360 } 361 } 362