| /* |
| * 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 android.content.Context; |
| import android.content.SharedPreferences; |
| import android.util.Pair; |
| import androidx.annotation.VisibleForTesting; |
| import com.google.android.libraries.mobiledatadownload.SilentFeedback; |
| import com.google.android.libraries.mobiledatadownload.TimeSource; |
| import com.google.android.libraries.mobiledatadownload.annotations.InstanceId; |
| import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor; |
| import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; |
| import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil; |
| import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupsMetadataUtil; |
| import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupsMetadataUtil.GroupKeyDeserializationException; |
| import com.google.android.libraries.mobiledatadownload.internal.util.ProtoLiteUtil; |
| import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil; |
| import com.google.common.base.Optional; |
| 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.DataFileGroupInternal; |
| import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; |
| import com.google.mobiledatadownload.internal.MetadataProto.GroupKeyProperties; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| import javax.inject.Inject; |
| import org.checkerframework.checker.nullness.compatqual.NullableType; |
| |
| /** Stores and provides access to file group metadata using SharedPreferences. */ |
| @CheckReturnValue |
| public final class SharedPreferencesFileGroupsMetadata implements FileGroupsMetadata { |
| |
| private static final String TAG = "SharedPreferencesFileGroupsMetadata"; |
| private static final String MDD_FILE_GROUPS = FileGroupsMetadataUtil.MDD_FILE_GROUPS; |
| private static final String MDD_FILE_GROUP_KEY_PROPERTIES = |
| FileGroupsMetadataUtil.MDD_FILE_GROUP_KEY_PROPERTIES; |
| |
| // TODO(b/144033163): Migrate the Garbage Collector File to PDS. |
| @VisibleForTesting static final String MDD_GARBAGE_COLLECTION_FILE = "gms_icing_mdd_garbage_file"; |
| |
| private final Context context; |
| private final TimeSource timeSource; |
| private final SilentFeedback silentFeedback; |
| private final Optional<String> instanceId; |
| private final Executor sequentialControlExecutor; |
| |
| @Inject |
| SharedPreferencesFileGroupsMetadata( |
| @ApplicationContext Context context, |
| TimeSource timeSource, |
| SilentFeedback silentFeedback, |
| @InstanceId Optional<String> instanceId, |
| @SequentialControlExecutor Executor sequentialControlExecutor) { |
| this.context = context; |
| this.timeSource = timeSource; |
| this.silentFeedback = silentFeedback; |
| this.instanceId = instanceId; |
| this.sequentialControlExecutor = sequentialControlExecutor; |
| } |
| |
| @Override |
| public ListenableFuture<Void> init() { |
| return Futures.immediateVoidFuture(); |
| } |
| |
| @Override |
| public ListenableFuture<@NullableType DataFileGroupInternal> read(GroupKey groupKey) { |
| String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey, context); |
| |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId); |
| DataFileGroupInternal fileGroup = |
| SharedPreferencesUtil.readProto(prefs, serializedGroupKey, DataFileGroupInternal.parser()); |
| |
| return Futures.immediateFuture(fileGroup); |
| } |
| |
| @Override |
| public ListenableFuture<Boolean> write(GroupKey groupKey, DataFileGroupInternal fileGroup) { |
| String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey, context); |
| |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId); |
| return Futures.immediateFuture( |
| SharedPreferencesUtil.writeProto(prefs, serializedGroupKey, fileGroup)); |
| } |
| |
| @Override |
| public ListenableFuture<Boolean> remove(GroupKey groupKey) { |
| String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey, context); |
| |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId); |
| return Futures.immediateFuture(SharedPreferencesUtil.removeProto(prefs, serializedGroupKey)); |
| } |
| |
| @Override |
| public ListenableFuture<@NullableType GroupKeyProperties> readGroupKeyProperties( |
| GroupKey groupKey) { |
| String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey, context); |
| |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences( |
| context, MDD_FILE_GROUP_KEY_PROPERTIES, instanceId); |
| GroupKeyProperties groupKeyProperties = |
| SharedPreferencesUtil.readProto(prefs, serializedGroupKey, GroupKeyProperties.parser()); |
| |
| return Futures.immediateFuture(groupKeyProperties); |
| } |
| |
| @Override |
| public ListenableFuture<Boolean> writeGroupKeyProperties( |
| GroupKey groupKey, GroupKeyProperties groupKeyProperties) { |
| String serializedGroupKey = FileGroupsMetadataUtil.getSerializedGroupKey(groupKey, context); |
| |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences( |
| context, MDD_FILE_GROUP_KEY_PROPERTIES, instanceId); |
| return Futures.immediateFuture( |
| SharedPreferencesUtil.writeProto(prefs, serializedGroupKey, groupKeyProperties)); |
| } |
| |
| @Override |
| public ListenableFuture<List<GroupKey>> getAllGroupKeys() { |
| List<GroupKey> groupKeyList = new ArrayList<>(); |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId); |
| SharedPreferences.Editor editor = null; |
| for (String serializedGroupKey : prefs.getAll().keySet()) { |
| try { |
| GroupKey newFileKey = FileGroupsMetadataUtil.deserializeGroupKey(serializedGroupKey); |
| groupKeyList.add(newFileKey); |
| } catch (GroupKeyDeserializationException e) { |
| LogUtil.e(e, "Failed to deserialize groupKey:" + serializedGroupKey); |
| silentFeedback.send(e, "Failed to deserialize groupKey"); |
| // TODO(b/128850000): Refactor this code to a single corruption handling task during |
| // maintenance. |
| // Remove the corrupted file metadata and the related SharedFile metadata will be deleted |
| // in next maintenance task. |
| if (editor == null) { |
| editor = prefs.edit(); |
| } |
| editor.remove(serializedGroupKey); |
| LogUtil.d("%s: Deleting null file group ", TAG); |
| continue; |
| } |
| } |
| if (editor != null) { |
| editor.commit(); |
| } |
| return Futures.immediateFuture(groupKeyList); |
| } |
| |
| @Override |
| public ListenableFuture<List<Pair<GroupKey, DataFileGroupInternal>>> getAllFreshGroups() { |
| return Futures.transformAsync( |
| getAllGroupKeys(), |
| groupKeyList -> { |
| List<ListenableFuture<@NullableType DataFileGroupInternal>> groupReadFutures = |
| new ArrayList<>(); |
| for (GroupKey key : groupKeyList) { |
| groupReadFutures.add(read(key)); |
| } |
| return Futures.whenAllComplete(groupReadFutures) |
| .callAsync( |
| () -> { |
| List<Pair<GroupKey, DataFileGroupInternal>> retrievedGroups = new ArrayList<>(); |
| for (int i = 0; i < groupKeyList.size(); i++) { |
| GroupKey key = groupKeyList.get(i); |
| DataFileGroupInternal group = Futures.getDone(groupReadFutures.get(i)); |
| if (group == null) { |
| continue; |
| } |
| retrievedGroups.add(Pair.create(key, group)); |
| } |
| return Futures.immediateFuture(retrievedGroups); |
| }, |
| sequentialControlExecutor); |
| }, |
| sequentialControlExecutor); |
| } |
| |
| @Override |
| public ListenableFuture<Boolean> removeAllGroupsWithKeys(List<GroupKey> keys) { |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId); |
| SharedPreferences.Editor editor = prefs.edit(); |
| for (GroupKey key : keys) { |
| LogUtil.d("%s: Removing group %s %s", TAG, key.getGroupName(), key.getOwnerPackage()); |
| SharedPreferencesUtil.removeProto(editor, key); |
| } |
| return Futures.immediateFuture(editor.commit()); |
| } |
| |
| @Override |
| public ListenableFuture<List<DataFileGroupInternal>> getAllStaleGroups() { |
| return Futures.immediateFuture( |
| FileGroupsMetadataUtil.getAllStaleGroups( |
| FileGroupsMetadataUtil.getGarbageCollectorFile(context, instanceId))); |
| } |
| |
| @Override |
| public ListenableFuture<Boolean> addStaleGroup(DataFileGroupInternal fileGroup) { |
| LogUtil.d("%s: Adding file group %s", TAG, fileGroup.getGroupName()); |
| |
| long currentTimeSeconds = timeSource.currentTimeMillis() / 1000; |
| fileGroup = |
| FileGroupUtil.setStaleExpirationDate( |
| fileGroup, currentTimeSeconds + fileGroup.getStaleLifetimeSecs()); |
| |
| List<DataFileGroupInternal> fileGroups = new ArrayList<>(); |
| fileGroups.add(fileGroup); |
| |
| return writeStaleGroups(fileGroups); |
| } |
| |
| @Override |
| public ListenableFuture<Boolean> writeStaleGroups(List<DataFileGroupInternal> fileGroups) { |
| File garbageCollectorFile = getGarbageCollectorFile(); |
| FileOutputStream outputStream; |
| try { |
| outputStream = new FileOutputStream(garbageCollectorFile, /* append */ true); |
| } catch (FileNotFoundException e) { |
| LogUtil.e("File %s not found while writing.", garbageCollectorFile.getAbsolutePath()); |
| return Futures.immediateFuture(false); |
| } |
| |
| try { |
| // tail_crc == false, means that each message has its own crc |
| ByteBuffer buf = ProtoLiteUtil.dumpIntoBuffer(fileGroups, false /*tail crc*/); |
| if (buf != null) { |
| outputStream.getChannel().write(buf); |
| } |
| outputStream.close(); |
| } catch (IOException e) { |
| LogUtil.e("IOException occurred while writing file groups."); |
| return Futures.immediateFuture(false); |
| } |
| return Futures.immediateFuture(true); |
| } |
| |
| @VisibleForTesting |
| File getGarbageCollectorFile() { |
| return FileGroupsMetadataUtil.getGarbageCollectorFile(context, instanceId); |
| } |
| |
| // TODO(b/124072754): Change to package private once all code is refactored. |
| @Override |
| public ListenableFuture<Void> removeAllStaleGroups() { |
| getGarbageCollectorFile().delete(); |
| return Futures.immediateVoidFuture(); |
| } |
| |
| @Override |
| public ListenableFuture<Void> clear() { |
| SharedPreferences prefs = |
| SharedPreferencesUtil.getSharedPreferences(context, MDD_FILE_GROUPS, instanceId); |
| prefs.edit().clear().commit(); |
| |
| SharedPreferences activatedGroupPrefs = |
| SharedPreferencesUtil.getSharedPreferences( |
| context, MDD_FILE_GROUP_KEY_PROPERTIES, instanceId); |
| activatedGroupPrefs.edit().clear().commit(); |
| |
| return removeAllStaleGroups(); |
| } |
| } |