• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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