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