• 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.common.util.concurrent.Futures.getDone;
19 import static com.google.common.util.concurrent.Futures.immediateFuture;
20 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
21 
22 import android.content.Context;
23 import android.content.SharedPreferences;
24 import androidx.annotation.VisibleForTesting;
25 import com.google.android.libraries.mobiledatadownload.SilentFeedback;
26 import com.google.android.libraries.mobiledatadownload.TimeSource;
27 import com.google.android.libraries.mobiledatadownload.annotations.InstanceId;
28 import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor;
29 import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
30 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
31 import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
32 import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupsMetadataUtil;
33 import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupsMetadataUtil.GroupKeyDeserializationException;
34 import com.google.android.libraries.mobiledatadownload.internal.util.ProtoLiteUtil;
35 import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil;
36 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
37 import com.google.common.base.Optional;
38 import com.google.common.util.concurrent.ListenableFuture;
39 import com.google.errorprone.annotations.CheckReturnValue;
40 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
41 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
42 import com.google.mobiledatadownload.internal.MetadataProto.GroupKeyProperties;
43 import java.io.File;
44 import java.io.FileNotFoundException;
45 import java.io.FileOutputStream;
46 import java.io.IOException;
47 import java.nio.ByteBuffer;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.concurrent.Executor;
51 import javax.inject.Inject;
52 import org.checkerframework.checker.nullness.compatqual.NullableType;
53 
54 /** Stores and provides access to file group metadata using SharedPreferences. */
55 @CheckReturnValue
56 public final class SharedPreferencesFileGroupsMetadata implements FileGroupsMetadata {
57 
58   private static final String TAG = "SharedPreferencesFileGroupsMetadata";
59   private static final String MDD_FILE_GROUPS = FileGroupsMetadataUtil.MDD_FILE_GROUPS;
60   private static final String MDD_FILE_GROUP_KEY_PROPERTIES =
61       FileGroupsMetadataUtil.MDD_FILE_GROUP_KEY_PROPERTIES;
62 
63   // TODO(b/144033163): Migrate the Garbage Collector File to PDS.
64   @VisibleForTesting static final String MDD_GARBAGE_COLLECTION_FILE = "gms_icing_mdd_garbage_file";
65 
66   private final Context context;
67   private final TimeSource timeSource;
68   private final SilentFeedback silentFeedback;
69   private final Optional<String> instanceId;
70   private final Executor sequentialControlExecutor;
71 
72   @Inject
SharedPreferencesFileGroupsMetadata( @pplicationContext Context context, TimeSource timeSource, SilentFeedback silentFeedback, @InstanceId Optional<String> instanceId, @SequentialControlExecutor Executor sequentialControlExecutor)73   SharedPreferencesFileGroupsMetadata(
74       @ApplicationContext Context context,
75       TimeSource timeSource,
76       SilentFeedback silentFeedback,
77       @InstanceId Optional<String> instanceId,
78       @SequentialControlExecutor Executor sequentialControlExecutor) {
79     this.context = context;
80     this.timeSource = timeSource;
81     this.silentFeedback = silentFeedback;
82     this.instanceId = instanceId;
83     this.sequentialControlExecutor = sequentialControlExecutor;
84   }
85 
86   @Override
init()87   public ListenableFuture<Void> init() {
88     return immediateVoidFuture();
89   }
90 
91   @Override
read(GroupKey groupKey)92   public ListenableFuture<@NullableType DataFileGroupInternal> read(GroupKey groupKey) {
93     String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey);
94 
95     SharedPreferences prefs =
96         SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId);
97     DataFileGroupInternal fileGroup =
98         SharedPreferencesUtil.readProto(prefs, serializedGroupKey, DataFileGroupInternal.parser());
99 
100     return immediateFuture(fileGroup);
101   }
102 
103   @Override
write(GroupKey groupKey, DataFileGroupInternal fileGroup)104   public ListenableFuture<Boolean> write(GroupKey groupKey, DataFileGroupInternal fileGroup) {
105     String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey);
106 
107     SharedPreferences prefs =
108         SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId);
109     return immediateFuture(SharedPreferencesUtil.writeProto(prefs, serializedGroupKey, fileGroup));
110   }
111 
112   @Override
remove(GroupKey groupKey)113   public ListenableFuture<Boolean> remove(GroupKey groupKey) {
114     String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey);
115 
116     SharedPreferences prefs =
117         SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId);
118     return immediateFuture(SharedPreferencesUtil.removeProto(prefs, serializedGroupKey));
119   }
120 
121   @Override
readGroupKeyProperties( GroupKey groupKey)122   public ListenableFuture<@NullableType GroupKeyProperties> readGroupKeyProperties(
123       GroupKey groupKey) {
124     String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey);
125 
126     SharedPreferences prefs =
127         SharedPreferencesUtil.getSharedPreferences(
128             context, MDD_FILE_GROUP_KEY_PROPERTIES, instanceId);
129     GroupKeyProperties groupKeyProperties =
130         SharedPreferencesUtil.readProto(prefs, serializedGroupKey, GroupKeyProperties.parser());
131 
132     return immediateFuture(groupKeyProperties);
133   }
134 
135   @Override
writeGroupKeyProperties( GroupKey groupKey, GroupKeyProperties groupKeyProperties)136   public ListenableFuture<Boolean> writeGroupKeyProperties(
137       GroupKey groupKey, GroupKeyProperties groupKeyProperties) {
138     String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey);
139 
140     SharedPreferences prefs =
141         SharedPreferencesUtil.getSharedPreferences(
142             context, MDD_FILE_GROUP_KEY_PROPERTIES, instanceId);
143     return immediateFuture(
144         SharedPreferencesUtil.writeProto(prefs, serializedGroupKey, groupKeyProperties));
145   }
146 
147   @Override
getAllGroupKeys()148   public ListenableFuture<List<GroupKey>> getAllGroupKeys() {
149     List<GroupKey> groupKeyList = new ArrayList<>();
150     SharedPreferences prefs =
151         SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId);
152     SharedPreferences.Editor editor = null;
153     for (String serializedGroupKey : prefs.getAll().keySet()) {
154       try {
155         GroupKey newFileKey = FileGroupsMetadataUtil.deserializeGroupKey(serializedGroupKey);
156         groupKeyList.add(newFileKey);
157       } catch (GroupKeyDeserializationException e) {
158         LogUtil.e(e, "Failed to deserialize groupKey:" + serializedGroupKey);
159         silentFeedback.send(e, "Failed to deserialize groupKey");
160         // TODO(b/128850000): Refactor this code to a single corruption handling task during
161         // maintenance.
162         // Remove the corrupted file metadata and the related SharedFile metadata will be deleted
163         // in next maintenance task.
164         if (editor == null) {
165           editor = prefs.edit();
166         }
167         editor.remove(serializedGroupKey);
168         LogUtil.d("%s: Deleting null file group ", TAG);
169         continue;
170       }
171     }
172     if (editor != null) {
173       editor.commit();
174     }
175     return immediateFuture(groupKeyList);
176   }
177 
178   @Override
getAllFreshGroups()179   public ListenableFuture<List<GroupKeyAndGroup>> getAllFreshGroups() {
180     return PropagatedFutures.transformAsync(
181         getAllGroupKeys(),
182         groupKeyList -> {
183           List<ListenableFuture<@NullableType DataFileGroupInternal>> groupReadFutures =
184               new ArrayList<>();
185           for (GroupKey key : groupKeyList) {
186             groupReadFutures.add(read(key));
187           }
188           return PropagatedFutures.whenAllComplete(groupReadFutures)
189               .callAsync(
190                   () -> {
191                     List<GroupKeyAndGroup> retrievedGroups = new ArrayList<>();
192                     for (int i = 0; i < groupKeyList.size(); i++) {
193                       GroupKey key = groupKeyList.get(i);
194                       DataFileGroupInternal group = getDone(groupReadFutures.get(i));
195                       if (group == null) {
196                         continue;
197                       }
198                       retrievedGroups.add(GroupKeyAndGroup.create(key, group));
199                     }
200                     return immediateFuture(retrievedGroups);
201                   },
202                   sequentialControlExecutor);
203         },
204         sequentialControlExecutor);
205   }
206 
207   @Override
208   public ListenableFuture<Boolean> removeAllGroupsWithKeys(List<GroupKey> keys) {
209     SharedPreferences prefs =
210         SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId);
211     SharedPreferences.Editor editor = prefs.edit();
212     for (GroupKey key : keys) {
213       LogUtil.d("%s: Removing group %s %s", TAG, key.getGroupName(), key.getOwnerPackage());
214       SharedPreferencesUtil.removeProto(editor, key);
215     }
216     return immediateFuture(editor.commit());
217   }
218 
219   @Override
220   public ListenableFuture<List<DataFileGroupInternal>> getAllStaleGroups() {
221     return immediateFuture(
222         FileGroupsMetadataUtil.getAllStaleGroups(
223             FileGroupsMetadataUtil.getGarbageCollectorFile(context, instanceId)));
224   }
225 
226   @Override
227   public ListenableFuture<Boolean> addStaleGroup(DataFileGroupInternal fileGroup) {
228     LogUtil.d("%s: Adding file group %s", TAG, fileGroup.getGroupName());
229 
230     long currentTimeSeconds = timeSource.currentTimeMillis() / 1000;
231     fileGroup =
232         FileGroupUtil.setStaleExpirationDate(
233             fileGroup, currentTimeSeconds + fileGroup.getStaleLifetimeSecs());
234 
235     List<DataFileGroupInternal> fileGroups = new ArrayList<>();
236     fileGroups.add(fileGroup);
237 
238     return writeStaleGroups(fileGroups);
239   }
240 
241   @Override
242   public ListenableFuture<Boolean> writeStaleGroups(List<DataFileGroupInternal> fileGroups) {
243     File garbageCollectorFile = getGarbageCollectorFile();
244     FileOutputStream outputStream;
245     try {
246       outputStream = new FileOutputStream(garbageCollectorFile, /* append */ true);
247     } catch (FileNotFoundException e) {
248       LogUtil.e("File %s not found while writing.", garbageCollectorFile.getAbsolutePath());
249       return immediateFuture(false);
250     }
251 
252     try {
253       // tail_crc == false, means that each message has its own crc
254       ByteBuffer buf = ProtoLiteUtil.dumpIntoBuffer(fileGroups, false /*tail crc*/);
255       if (buf != null) {
256         outputStream.getChannel().write(buf);
257       }
258       outputStream.close();
259     } catch (IOException e) {
260       LogUtil.e("IOException occurred while writing file groups.");
261       return immediateFuture(false);
262     }
263     return immediateFuture(true);
264   }
265 
266   @VisibleForTesting
267   File getGarbageCollectorFile() {
268     return FileGroupsMetadataUtil.getGarbageCollectorFile(context, instanceId);
269   }
270 
271   // TODO(b/124072754): Change to package private once all code is refactored.
272   @Override
273   public ListenableFuture<Void> removeAllStaleGroups() {
274     getGarbageCollectorFile().delete();
275     return immediateVoidFuture();
276   }
277 
278   @Override
279   public ListenableFuture<Void> clear() {
280     SharedPreferences prefs =
281         SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId);
282     prefs.edit().clear().commit();
283 
284     SharedPreferences activatedGroupPrefs =
285         SharedPreferencesUtil.getSharedPreferences(
286             context, MDD_FILE_GROUP_KEY_PROPERTIES, instanceId);
287     activatedGroupPrefs.edit().clear().commit();
288 
289     return removeAllStaleGroups();
290   }
291 }
292